Skip to content

Commit fe47426

Browse files
committed
Co-allocate persistent vector replacements
1 parent eae2210 commit fe47426

3 files changed

Lines changed: 48 additions & 21 deletions

File tree

internal/persistent/vector/vector.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,13 @@ type TailStorage struct {
8888
entry tailEntry
8989
}
9090

91+
// ReplaceTailStorage reserves storage for replacing the last tail entry
92+
// without copying either a base tail or a delta chain.
93+
type ReplaceTailStorage struct {
94+
entry tailEntry
95+
base tailBase
96+
}
97+
9198
// Empty is an empty Vector.
9299
var Empty Vector = &Persistent{}
93100

@@ -243,7 +250,7 @@ func (v Persistent) AssocValue(i int, val interface{}) (Persistent, bool) {
243250
// operation does not have to materialize and copy the tail.
244251
func (v Persistent) ReplaceLastValueInto(
245252
val interface{},
246-
storage *TailStorage,
253+
storage *ReplaceTailStorage,
247254
) (Persistent, bool) {
248255
if v.count == 0 {
249256
return Persistent{}, false
@@ -256,9 +263,11 @@ func (v Persistent) ReplaceLastValueInto(
256263
v.tailDelta = &storage.entry
257264
return v, true
258265
}
259-
tail := append([]interface{}(nil), v.baseTail()...)
260-
tail[len(tail)-1] = val
261-
v.tail = newTailBase(tail)
266+
base := v.baseTail()
267+
storage.base = tailBase(base[:len(base)-1])
268+
storage.entry = tailEntry{value: val}
269+
v.tail = &storage.base
270+
v.tailDelta = &storage.entry
262271
return v, true
263272
}
264273

pkg/lang/vector.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ type (
2828
tail vector.TailStorage
2929
}
3030

31+
vectorReplaceStorage struct {
32+
Vector
33+
tail vector.ReplaceTailStorage
34+
}
35+
3136
PersistentVector = Vector
3237

3338
TransientVector struct {
@@ -100,7 +105,7 @@ func (v *Vector) AssocN(i int, val any) IPersistentVector {
100105

101106
// ReplaceLast returns a persistent vector whose final value is val.
102107
func (v *Vector) ReplaceLast(val any) *Vector {
103-
storage := &vectorUpdateStorage{}
108+
storage := &vectorReplaceStorage{}
104109
result, ok := v.vec.ReplaceLastValueInto(val, &storage.tail)
105110
if !ok {
106111
panic("can't pop an empty vector")

pkg/lang/vector_test.go

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,23 +36,36 @@ func TestLargeVectorConsCoallocatesResultAndTail(t *testing.T) {
3636
}
3737

3838
func TestVectorReplaceLastIsPersistentAndCoallocated(t *testing.T) {
39-
original := NewVector()
40-
for _, value := range []int{1, 2, 3, 4, 5} {
41-
original = original.Cons(value).(*Vector)
42-
}
43-
var result *Vector
44-
if got := testing.AllocsPerRun(1_000, func() {
45-
result = original.ReplaceLast(9)
46-
}); got != 1 {
47-
t.Fatalf("replace-last allocated %v objects per call, want 1", got)
48-
}
49-
if got := original.Nth(4); got != 5 {
50-
t.Fatalf("original final value = %v, want 5", got)
51-
}
52-
if got := result.Nth(4); got != 9 {
53-
t.Fatalf("replacement final value = %v, want 9", got)
39+
tests := []struct {
40+
name string
41+
original *Vector
42+
}{
43+
{"base tail", NewVector(1, 2, 3, 4, 5)},
44+
{"delta tail", func() *Vector {
45+
vector := NewVector()
46+
for _, value := range []int{1, 2, 3, 4, 5} {
47+
vector = vector.Cons(value).(*Vector)
48+
}
49+
return vector
50+
}()},
51+
}
52+
for _, test := range tests {
53+
t.Run(test.name, func(t *testing.T) {
54+
var result *Vector
55+
if got := testing.AllocsPerRun(1_000, func() {
56+
result = test.original.ReplaceLast(9)
57+
}); got != 1 {
58+
t.Fatalf("replace-last allocated %v objects per call, want 1", got)
59+
}
60+
if got := test.original.Nth(4); got != 5 {
61+
t.Fatalf("original final value = %v, want 5", got)
62+
}
63+
if got := result.Nth(4); got != 9 {
64+
t.Fatalf("replacement final value = %v, want 9", got)
65+
}
66+
runtime.KeepAlive(result)
67+
})
5468
}
55-
runtime.KeepAlive(result)
5669
}
5770

5871
func TestSmallVectorWithMetaKeepsInlineStorageAlive(t *testing.T) {

0 commit comments

Comments
 (0)