Skip to content
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

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

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
99 changes: 79 additions & 20 deletions gnovm/pkg/gnolang/nodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,26 +145,58 @@
// 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
)

func (ga GnoAttribute) String() string {
switch ga {
case ATTR_PREPROCESSED:
return "ATTR_PREPROCESSED"
case ATTR_PREDEFINED:
return "ATTR_PREDEFINED"
case ATTR_TYPE_VALUE:
return "ATTR_TYPE_VALUE"
case ATTR_TYPEOF_VALUE:
return "ATTR_TYPEOF_VALUE"
case ATTR_IOTA:
return "ATTR_IOTA"
case ATTR_LOOP_DEFINES:
return "ATTR_LOOP_DEFINES" // []Name defined within loops.
case ATTR_LOOP_USES:
return "ATTR_LOOP_USES" // []Name loop defines actually used.
case ATTR_SHIFT_RHS:
return "ATTR_SHIFT_RHS"
case ATTR_LAST_BLOCK_STMT:
return "ATTR_LAST_BLOCK_STMT"
case ATTR_GLOBAL:
return "ATTR_GLOBAL"
default:
panic(fmt.Sprintf("Unknown attribute: %d", ga))

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

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/nodes.go#L163-L186

Added lines #L163 - L186 were 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 []*attrKV // not persisted
}

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

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

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

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/nodes.go#L227

Added line #L227 was not covered by tests
return ok
}

// 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) {
for i, kv := range attr.data {
if kv.key == key {
return kv.value, i, true
}
}
return nil, -1, false
}

type attrKV struct {
key GnoAttribute
value any
}

func (attr *Attributes) SetAttribute(key GnoAttribute, value interface{}) {
if attr.data == nil {
attr.data = make(map[GnoAttribute]interface{})
for _, kv := range attr.data {
if kv.key == key {
kv.value = value
return
}
}
attr.data[key] = value

attr.data = append(attr.data, &attrKV{key, value})
}

func (attr *Attributes) DelAttribute(key GnoAttribute) {
if debug && attr.data == nil {
panic("should not happen, attribute is expected to be non-empty.")
_, index, _ := attr.getAttribute(key)
if index < 0 {
return
}

if index == 0 {
attr.data = attr.data[1:]
} else if index == len(attr.data)-1 {
attr.data = attr.data[:len(attr.data)-1]
} else {
attr.data = append(attr.data[:index], attr.data[index+1:]...)

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

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/nodes.go#L274

Added line #L274 was not covered by tests
}
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)
attrs.GetAttribute(key)
}
}

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