Skip to content

Commit 8a7e96a

Browse files
committed
perf(gnolang): use slice not map for Attributes.data per usage performance
Noticed in profiling stdlibs/bytes that a ton of memory was being used in maps, and that's due to the conventional CS 101 that maps with O(1) lookups, insertions and deletions beat O(n) slices' performance, but when n is small, the memory bloat is not worth it and we can use slices as evidenced in profiles for which there was 30% perceptible reduction in RAM where * Before: ```shell Showing nodes accounting for 92.90MB, 83.87% of 110.76MB total Dropped 51 nodes (cum <= 0.55MB) Showing top 10 nodes out of 123 flat flat% sum% cum cum% 47.37MB 42.77% 42.77% 47.37MB 42.77% internal/runtime/maps.newarray 10.50MB 9.48% 52.25% 10.50MB 9.48% internal/runtime/maps.NewEmptyMap 8MB 7.22% 59.47% 8MB 7.22% github.com/gnolang/gno/gnovm/pkg/gnolang.(*StaticBlock).InitStaticBlock 7.51MB 6.78% 66.25% 13.03MB 11.76% github.com/gnolang/gno/gnovm/pkg/gnolang.Go2Gno 6.02MB 5.43% 71.68% 10.73MB 9.68% github.com/gnolang/gno/gnovm/pkg/gnolang.(*defaultStore).SetObject 4MB 3.61% 75.29% 4MB 3.61% github.com/gnolang/gno/gnovm/pkg/gnolang.NewBlock 3.47MB 3.13% 78.43% 3.47MB 3.13% github.com/gnolang/gno/gnovm/pkg/gnolang.(*Allocator).NewDataArray 2.52MB 2.27% 80.70% 3.52MB 3.18% github.com/gnolang/gno/gnovm/pkg/gnolang.toKeyValueExprs 2MB 1.81% 82.51% 2MB 1.81% runtime.allocm 1.51MB 1.36% 83.87% 1.51MB 1.36% runtime/pprof.(*profMap).lookup ``` ```shell Showing nodes accounting for 47.37MB, 42.77% of 110.76MB total ----------------------------------------------------------+------------- flat flat% sum% cum cum% calls calls% + context ----------------------------------------------------------+------------- 47.37MB 100% | internal/runtime/maps.newGroups 47.37MB 42.77% 42.77% 47.37MB 42.77% | internal/runtime/maps.newarray ----------------------------------------------------------+------------- 32.01MB 78.05% | github.com/gnolang/gno/gnovm/pkg/gnolang.preprocess1.func1 7MB 17.07% | github.com/gnolang/gno/gnovm/pkg/gnolang.evalConst (inline) 1.50MB 3.66% | github.com/gnolang/gno/gnovm/pkg/gnolang.constType (inline) 0.50MB 1.22% | github.com/gnolang/gno/gnovm/pkg/gnolang.tryPredefine.func1 0 0% 42.77% 41.01MB 37.03% | github.com/gnolang/gno/gnovm/pkg/gnolang.(*Attributes).SetAttribute 41.01MB 100% | runtime.mapassign_faststr ----------------------------------------------------------+------------- 4.50MB 100% | github.com/gnolang/gno/gnovm/pkg/test.(*TestOptions).runTestFiles 0 0% 42.77% 4.50MB 4.06% | github.com/gnolang/gno/gnovm/pkg/gnolang.(*Machine).RunFiles 4.50MB 100% | github.com/gnolang/gno/gnovm/pkg/gnolang.(*Machine).runFileDecls ``` and after: ```shell Showing nodes accounting for 61.99MB, 73.12% of 84.78MB total Showing top 10 nodes out of 196 flat flat% sum% cum cum% 19.50MB 23.00% 23.00% 19.50MB 23.00% github.com/gnolang/gno/gnovm/pkg/gnolang.(*Attributes).SetAttribute 12.52MB 14.76% 37.77% 18.02MB 21.26% github.com/gnolang/gno/gnovm/pkg/gnolang.Go2Gno 7.58MB 8.94% 46.70% 9.15MB 10.79% github.com/gnolang/gno/gnovm/pkg/gnolang.(*defaultStore).SetObject 5MB 5.90% 52.60% 5MB 5.90% github.com/gnolang/gno/gnovm/pkg/gnolang.(*StaticBlock).InitStaticBlock 3.47MB 4.09% 56.69% 3.47MB 4.09% github.com/gnolang/gno/gnovm/pkg/gnolang.(*Allocator).NewDataArray 3MB 3.54% 60.24% 3MB 3.54% github.com/gnolang/gno/gnovm/pkg/gnolang.NewBlock 3MB 3.54% 63.77% 3MB 3.54% github.com/gnolang/gno/gnovm/pkg/gnolang.Nx (inline) 2.77MB 3.26% 67.04% 2.77MB 3.26% bytes.growSlice 2.65MB 3.12% 70.16% 2.65MB 3.12% internal/runtime/maps.newarray 2.50MB 2.95% 73.12% 2.50MB 2.95% runtime.allocm ``` Fixes gnolang#3436
1 parent beb48e7 commit 8a7e96a

File tree

2 files changed

+97
-6
lines changed

2 files changed

+97
-6
lines changed

gnovm/pkg/gnolang/nodes.go

+40-6
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ type Attributes struct {
165165
Line int
166166
Column int
167167
Label Name
168-
data map[GnoAttribute]interface{} // not persisted
168+
data []*attrKV // not persisted
169169
}
170170

171171
func (attr *Attributes) GetLine() int {
@@ -193,28 +193,62 @@ func (attr *Attributes) SetLabel(label Name) {
193193
}
194194

195195
func (attr *Attributes) HasAttribute(key GnoAttribute) bool {
196-
_, ok := attr.data[key]
196+
_, _, ok := attr.getAttribute(key)
197197
return ok
198198
}
199199

200200
// GnoAttribute must not be user provided / arbitrary,
201201
// otherwise will create potential exploits.
202202
func (attr *Attributes) GetAttribute(key GnoAttribute) interface{} {
203-
return attr.data[key]
203+
val, _, _ := attr.getAttribute(key)
204+
return val
205+
}
206+
207+
func (attr *Attributes) getAttribute(key GnoAttribute) (any, int, bool) {
208+
for i, kv := range attr.data {
209+
if kv.key == key {
210+
return kv.value, i, true
211+
}
212+
}
213+
return nil, -1, false
214+
}
215+
216+
type attrKV struct {
217+
key GnoAttribute
218+
value any
204219
}
205220

206221
func (attr *Attributes) SetAttribute(key GnoAttribute, value interface{}) {
207222
if attr.data == nil {
208-
attr.data = make(map[GnoAttribute]interface{})
223+
attr.data = make([]*attrKV, 0, 4)
209224
}
210-
attr.data[key] = value
225+
226+
for _, kv := range attr.data {
227+
if kv.key == key {
228+
kv.value = value
229+
return
230+
}
231+
}
232+
233+
attr.data = append(attr.data, &attrKV{key, value})
211234
}
212235

213236
func (attr *Attributes) DelAttribute(key GnoAttribute) {
214237
if debug && attr.data == nil {
215238
panic("should not happen, attribute is expected to be non-empty.")
216239
}
217-
delete(attr.data, key)
240+
_, index, _ := attr.getAttribute(key)
241+
if index < 0 {
242+
return
243+
}
244+
245+
if index == 0 {
246+
attr.data = attr.data[1:]
247+
} else if index == len(attr.data)-1 {
248+
attr.data = attr.data[:len(attr.data)-1]
249+
} else {
250+
attr.data = append(attr.data[:index], attr.data[index+1:]...)
251+
}
218252
}
219253

220254
// ----------------------------------------

gnovm/pkg/gnolang/nodes_test.go

+57
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package gnolang_test
22

33
import (
4+
"fmt"
45
"math"
56
"testing"
67

@@ -42,3 +43,59 @@ func TestStaticBlock_Define2_MaxNames(t *testing.T) {
4243
// This one should panic because the maximum number of names has been reached.
4344
staticBlock.Define2(false, gnolang.Name("a"), gnolang.BoolType, gnolang.TypedValue{T: gnolang.BoolType})
4445
}
46+
47+
func TestAttributesSetGetDel(t *testing.T) {
48+
attrs := new(gnolang.Attributes)
49+
if got, want := attrs.GetAttribute("a"), (any)(nil); got != want {
50+
t.Errorf(".Get returned an unexpected value=%v, want=%v", got, want)
51+
}
52+
attrs.SetAttribute("a", 10)
53+
if got, want := attrs.GetAttribute("a"), 10; got != want {
54+
t.Errorf(".Get returned an unexpected value=%v, want=%v", got, want)
55+
}
56+
attrs.SetAttribute("a", 20)
57+
if got, want := attrs.GetAttribute("a"), 20; got != want {
58+
t.Errorf(".Get returned an unexpected value=%v, want=%v", got, want)
59+
}
60+
attrs.DelAttribute("a")
61+
if got, want := attrs.GetAttribute("a"), (any)(nil); got != want {
62+
t.Errorf(".Get returned an unexpected value=%v, want=%v", got, want)
63+
}
64+
}
65+
66+
var sink any = nil
67+
68+
func BenchmarkAttributesSetGetDel(b *testing.B) {
69+
n := 100
70+
keys := make([]gnolang.GnoAttribute, 0, n)
71+
for i := 0; i < n; i++ {
72+
keys = append(keys, gnolang.GnoAttribute(fmt.Sprintf("%d", i)))
73+
}
74+
75+
b.ReportAllocs()
76+
b.ResetTimer()
77+
78+
for i := 0; i < b.N; i++ {
79+
attrs := new(gnolang.Attributes)
80+
for j := 0; j < 100; j++ {
81+
sink = attrs.GetAttribute("a")
82+
}
83+
for j := 0; j < 100; j++ {
84+
attrs.SetAttribute("a", j)
85+
sink = attrs.GetAttribute("a")
86+
}
87+
88+
for j, key := range keys {
89+
attrs.SetAttribute(key, j)
90+
}
91+
92+
for _, key := range keys {
93+
sink = attrs.GetAttribute(key)
94+
attrs.GetAttribute(key)
95+
}
96+
}
97+
98+
if sink == nil {
99+
b.Fatal("Benchmark did not run!")
100+
}
101+
}

0 commit comments

Comments
 (0)