Skip to content

Commit b4d9337

Browse files
dominikheliasnaur
authored andcommitted
op: don't allocate for each string reference
When storing a string in an interface value that escapes, Go has to heap allocate space for the string header, as interface values can only store pointers. In text-heavy applications, this can lead to hundreds of allocations per frame due to semantic.LabelOp, the primary user of string-typed references in ops. Instead of allocating each string header individually, provide a slice of strings to store string-typed references in, and store pointers into this slice as the actual references. This only allocates when resizing the slice's backing array, and averages out to no allocations, as the backing array gets reused between calls to Ops.Reset. We introduce two new functions, Write1String and Write2String, which make use of this new slice for their last argument. We could've automated this in the existing Write1 and Write2 methods, but that would require type assertions on each call, and the vast majority of ops do not make use of strings. Signed-off-by: Dominik Honnef <[email protected]>
1 parent b9654eb commit b4d9337

File tree

5 files changed

+38
-10
lines changed

5 files changed

+38
-10
lines changed

internal/ops/ops.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,17 @@ type Ops struct {
1919
data []byte
2020
// refs hold external references for operations.
2121
refs []interface{}
22+
// stringRefs provides space for string references, pointers to which will
23+
// be stored in refs. Storing a string directly in refs would cause a heap
24+
// allocation, to store the string header in an interface value. The backing
25+
// array of stringRefs, on the other hand, gets reused between calls to
26+
// reset, making string references free on average.
27+
//
28+
// Appending to stringRefs might reallocate the backing array, which will
29+
// leave pointers to the old array in refs. This temporarily causes a slight
30+
// increase in memory usage, but this, too, amortizes away as the capacity
31+
// of stringRefs approaches its stable maximum.
32+
stringRefs []string
2233
// nextStateID is the id allocated for the next
2334
// StateOp.
2435
nextStateID int
@@ -183,8 +194,12 @@ func Reset(o *Ops) {
183194
for i := range o.refs {
184195
o.refs[i] = nil
185196
}
197+
for i := range o.stringRefs {
198+
o.stringRefs[i] = ""
199+
}
186200
o.data = o.data[:0]
187201
o.refs = o.refs[:0]
202+
o.stringRefs = o.stringRefs[:0]
188203
o.nextStateID = 0
189204
o.version++
190205
}
@@ -265,12 +280,26 @@ func Write1(o *Ops, n int, ref1 interface{}) []byte {
265280
return o.data[len(o.data)-n:]
266281
}
267282

283+
func Write1String(o *Ops, n int, ref1 string) []byte {
284+
o.data = append(o.data, make([]byte, n)...)
285+
o.stringRefs = append(o.stringRefs, ref1)
286+
o.refs = append(o.refs, &o.stringRefs[len(o.stringRefs)-1])
287+
return o.data[len(o.data)-n:]
288+
}
289+
268290
func Write2(o *Ops, n int, ref1, ref2 interface{}) []byte {
269291
o.data = append(o.data, make([]byte, n)...)
270292
o.refs = append(o.refs, ref1, ref2)
271293
return o.data[len(o.data)-n:]
272294
}
273295

296+
func Write2String(o *Ops, n int, ref1 interface{}, ref2 string) []byte {
297+
o.data = append(o.data, make([]byte, n)...)
298+
o.stringRefs = append(o.stringRefs, ref2)
299+
o.refs = append(o.refs, ref1, &o.stringRefs[len(o.stringRefs)-1])
300+
return o.data[len(o.data)-n:]
301+
}
302+
274303
func Write3(o *Ops, n int, ref1, ref2, ref3 interface{}) []byte {
275304
o.data = append(o.data, make([]byte, n)...)
276305
o.refs = append(o.refs, ref1, ref2, ref3)

io/clipboard/clipboard.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func (h ReadOp) Add(o *op.Ops) {
3030
}
3131

3232
func (h WriteOp) Add(o *op.Ops) {
33-
data := ops.Write1(&o.Internal, ops.TypeClipboardWriteLen, &h.Text)
33+
data := ops.Write1String(&o.Internal, ops.TypeClipboardWriteLen, h.Text)
3434
data[0] = byte(ops.TypeClipboardWrite)
3535
}
3636

io/key/key.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -323,8 +323,7 @@ func (h InputOp) Add(o *op.Ops) {
323323
if h.Tag == nil {
324324
panic("Tag must be non-nil")
325325
}
326-
filter := h.Keys
327-
data := ops.Write2(&o.Internal, ops.TypeKeyInputLen, h.Tag, &filter)
326+
data := ops.Write2String(&o.Internal, ops.TypeKeyInputLen, h.Tag, string(h.Keys))
328327
data[0] = byte(ops.TypeKeyInput)
329328
data[1] = byte(h.Hint)
330329
}
@@ -343,7 +342,7 @@ func (h FocusOp) Add(o *op.Ops) {
343342
}
344343

345344
func (s SnippetOp) Add(o *op.Ops) {
346-
data := ops.Write2(&o.Internal, ops.TypeSnippetLen, s.Tag, &s.Text)
345+
data := ops.Write2String(&o.Internal, ops.TypeSnippetLen, s.Tag, s.Text)
347346
data[0] = byte(ops.TypeSnippet)
348347
bo := binary.LittleEndian
349348
bo.PutUint32(data[1:], uint32(s.Range.Start))

io/router/router.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -490,11 +490,11 @@ func (q *Router) collect() {
490490
}
491491
kc.softKeyboard(op.Show)
492492
case ops.TypeKeyInput:
493-
filter := encOp.Refs[1].(*key.Set)
493+
filter := key.Set(*encOp.Refs[1].(*string))
494494
op := key.InputOp{
495495
Tag: encOp.Refs[0].(event.Tag),
496496
Hint: key.InputHint(encOp.Data[1]),
497-
Keys: *filter,
497+
Keys: filter,
498498
}
499499
a := pc.currentArea()
500500
b := pc.currentAreaBounds()
@@ -532,10 +532,10 @@ func (q *Router) collect() {
532532

533533
// Semantic ops.
534534
case ops.TypeSemanticLabel:
535-
lbl := encOp.Refs[0].(string)
535+
lbl := *encOp.Refs[0].(*string)
536536
pc.semanticLabel(lbl)
537537
case ops.TypeSemanticDesc:
538-
desc := encOp.Refs[0].(string)
538+
desc := *encOp.Refs[0].(*string)
539539
pc.semanticDesc(desc)
540540
case ops.TypeSemanticClass:
541541
class := semantic.ClassOp(encOp.Data[1])

io/semantic/semantic.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,12 @@ type SelectedOp bool
4040
type DisabledOp bool
4141

4242
func (l LabelOp) Add(o *op.Ops) {
43-
data := ops.Write1(&o.Internal, ops.TypeSemanticLabelLen, string(l))
43+
data := ops.Write1String(&o.Internal, ops.TypeSemanticLabelLen, string(l))
4444
data[0] = byte(ops.TypeSemanticLabel)
4545
}
4646

4747
func (d DescriptionOp) Add(o *op.Ops) {
48-
data := ops.Write1(&o.Internal, ops.TypeSemanticDescLen, string(d))
48+
data := ops.Write1String(&o.Internal, ops.TypeSemanticDescLen, string(d))
4949
data[0] = byte(ops.TypeSemanticDesc)
5050
}
5151

0 commit comments

Comments
 (0)