Skip to content

perf(gnolang): use slice not map for Attributes.data per usage performance #3437

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 58 additions & 21 deletions gnovm/pkg/gnolang/nodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,26 +145,54 @@
// even after preprocessing. Temporary attributes (e.g. those
// for preprocessing) are stored in .data.

type GnoAttribute string
type GnoAttribute int

const (
ATTR_PREPROCESSED GnoAttribute = "ATTR_PREPROCESSED"
ATTR_PREDEFINED GnoAttribute = "ATTR_PREDEFINED"
ATTR_TYPE_VALUE GnoAttribute = "ATTR_TYPE_VALUE"
ATTR_TYPEOF_VALUE GnoAttribute = "ATTR_TYPEOF_VALUE"
ATTR_IOTA GnoAttribute = "ATTR_IOTA"
ATTR_LOOP_DEFINES GnoAttribute = "ATTR_LOOP_DEFINES" // []Name defined within loops.
ATTR_LOOP_USES GnoAttribute = "ATTR_LOOP_USES" // []Name loop defines actually used.
ATTR_SHIFT_RHS GnoAttribute = "ATTR_SHIFT_RHS"
ATTR_LAST_BLOCK_STMT GnoAttribute = "ATTR_LAST_BLOCK_STMT"
ATTR_GLOBAL GnoAttribute = "ATTR_GLOBAL"
ATTR_PREPROCESSED GnoAttribute = iota
ATTR_PREDEFINED
ATTR_TYPE_VALUE
ATTR_TYPEOF_VALUE
ATTR_IOTA
ATTR_LOOP_DEFINES
ATTR_LOOP_USES
ATTR_SHIFT_RHS
ATTR_LAST_BLOCK_STMT
ATTR_GLOBAL
attr_dummy_terminal
)

var attr_strs = [...]string{
ATTR_PREPROCESSED: "ATTR_PREPROCESSED",
ATTR_PREDEFINED: "ATTR_PREDEFINED",
ATTR_TYPE_VALUE: "ATTR_TYPE_VALUE",
ATTR_TYPEOF_VALUE: "ATTR_TYPEOF_VALUE",
ATTR_IOTA: "ATTR_IOTA",
ATTR_LOOP_DEFINES: "ATTR_LOOP_DEFINES",
ATTR_LOOP_USES: "ATTR_LOOP_USES",
ATTR_SHIFT_RHS: "ATTR_SHIFT_RHS",
ATTR_LAST_BLOCK_STMT: "ATTR_LAST_BLOCK_STMT",
ATTR_GLOBAL: "ATTR_GLOBAL",
}

func (ga GnoAttribute) String() string {
iga := int(ga)
if iga < 0 || iga >= len(attr_strs) {
panic(fmt.Sprintf("Unknown attribute: %d", ga))

Check warning on line 180 in gnovm/pkg/gnolang/nodes.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/nodes.go#L177-L180

Added lines #L177 - L180 were not covered by tests
}

return attr_strs[iga]

Check warning on line 183 in gnovm/pkg/gnolang/nodes.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/nodes.go#L183

Added line #L183 was not covered by tests
}

type Attributes struct {
Line int
Column int
Label Name
data map[GnoAttribute]interface{} // not persisted

// TODO: if ever the number of attributes ATTR_* grows
// to say more than 100 (rough guess), you can then
// consider using a map for it instead of a slice
// Please see https://github.com/gnolang/gno/issues/3436
data [attr_dummy_terminal - ATTR_PREPROCESSED]any
}

func (attr *Attributes) GetLine() int {
Expand Down Expand Up @@ -192,28 +220,37 @@
}

func (attr *Attributes) HasAttribute(key GnoAttribute) bool {
_, ok := attr.data[key]
return ok
val, _, ok := attr.getAttribute(key)
return ok && val != nil

Check warning on line 224 in gnovm/pkg/gnolang/nodes.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/nodes.go#L223-L224

Added lines #L223 - L224 were not covered by tests
}

// GnoAttribute must not be user provided / arbitrary,
// otherwise will create potential exploits.
func (attr *Attributes) GetAttribute(key GnoAttribute) interface{} {
return attr.data[key]
val, _, _ := attr.getAttribute(key)
return val
}

func (attr *Attributes) getAttribute(key GnoAttribute) (any, int, bool) {
i := int(key)
if i < 0 || i >= len(attr.data) {
return nil, -1, false
}

Check warning on line 238 in gnovm/pkg/gnolang/nodes.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/nodes.go#L237-L238

Added lines #L237 - L238 were not covered by tests
return attr.data[i], i, true
}

func (attr *Attributes) SetAttribute(key GnoAttribute, value interface{}) {
if attr.data == nil {
attr.data = make(map[GnoAttribute]interface{})
i := int(key)
if i >= 0 && i < len(attr.data) {
attr.data[i] = value
}
attr.data[key] = value
}

func (attr *Attributes) DelAttribute(key GnoAttribute) {
if debug && attr.data == nil {
panic("should not happen, attribute is expected to be non-empty.")
i := int(key)
if i >= 0 && i < len(attr_strs) {
attr.data[i] = nil
}
delete(attr.data, key)
}

// ----------------------------------------
Expand Down
59 changes: 59 additions & 0 deletions gnovm/pkg/gnolang/nodes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,62 @@ func TestStaticBlock_Define2_MaxNames(t *testing.T) {
// This one should panic because the maximum number of names has been reached.
staticBlock.Define2(false, gnolang.Name("a"), gnolang.BoolType, gnolang.TypedValue{T: gnolang.BoolType})
}

func TestAttributesSetGetDel(t *testing.T) {
attrs := new(gnolang.Attributes)
key := gnolang.ATTR_IOTA
if got, want := attrs.GetAttribute(key), (any)(nil); got != want {
t.Errorf(".Get returned an unexpected value=%v, want=%v", got, want)
}
attrs.SetAttribute(key, 10)
if got, want := attrs.GetAttribute(key), 10; got != want {
t.Errorf(".Get returned an unexpected value=%v, want=%v", got, want)
}
attrs.SetAttribute(key, 20)
if got, want := attrs.GetAttribute(key), 20; got != want {
t.Errorf(".Get returned an unexpected value=%v, want=%v", got, want)
}
attrs.DelAttribute(key)
if got, want := attrs.GetAttribute(key), (any)(nil); got != want {
t.Errorf(".Get returned an unexpected value=%v, want=%v", got, want)
}
}

var sink any = nil

func BenchmarkAttributesSetGetDel(b *testing.B) {
n := 100
keys := make([]gnolang.GnoAttribute, 0, n)
for i := 0; i < n; i++ {
keys = append(keys, gnolang.GnoAttribute(i))
}
attrCommon := gnolang.ATTR_TYPEOF_VALUE

b.ReportAllocs()
b.ResetTimer()

for i := 0; i < b.N; i++ {
attrs := new(gnolang.Attributes)
for j := 0; j < 100; j++ {
sink = attrs.GetAttribute(attrCommon)
}
for j := 0; j < 100; j++ {
attrs.SetAttribute(attrCommon, j)
sink = attrs.GetAttribute(attrCommon)
}

for j, key := range keys {
attrs.SetAttribute(key, j)
}

for _, key := range keys {
sink = attrs.GetAttribute(key)
}

sink = attrs
}

if sink == nil {
b.Fatal("Benchmark did not run!")
}
}
Loading