mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-13 03:42:35 -06:00
ast: Optimize set/object lookups and Number comparison.
Lookups for sets and objects use their own type specific equality checks instead of relying on the general purpose Compare. This improves the performance about 25%. The duplication of code is unavoidable to avoid the allocations, unfortunately. Number comparison is now allocation free for integers. This because Int64() of json.Number uses allocation free strconv.ParseInt internally. This shows up in the additional 60% improvement of set membership benchmarks as they use integer keys. Most notable improvements in benchmarks: benchmark old ns/op new ns/op delta BenchmarkObjectLookup/5-8 37.3 31.9 -14.48% BenchmarkObjectLookup/50-8 44.7 39.7 -11.19% BenchmarkObjectLookup/500-8 46.6 41.7 -10.52% BenchmarkObjectLookup/5000-8 43.6 38.4 -11.93% BenchmarkSetIntersection/5-8 2309 928 -59.81% BenchmarkSetIntersection/50-8 24562 9121 -62.87% BenchmarkSetIntersection/500-8 260022 93373 -64.09% BenchmarkSetIntersection/5000-8 2796645 1035045 -62.99% BenchmarkSetIntersectionDifferentSize/4-8 1774 781 -55.98% BenchmarkSetIntersectionDifferentSize/50-8 1847 875 -52.63% BenchmarkSetIntersectionDifferentSize/500-8 1849 864 -53.27% BenchmarkSetIntersectionDifferentSize/5000-8 1884 890 -52.76% BenchmarkSetMembership/5-8 339 55.1 -83.75% BenchmarkSetMembership/50-8 383 65.3 -82.95% BenchmarkSetMembership/500-8 403 70.5 -82.51% BenchmarkSetMembership/5000-8 418 75.1 -82.03% BenchmarkConcurrency1-8 219100992 218699478 -0.18% BenchmarkConcurrency2-8 121519274 119096088 -1.99% BenchmarkConcurrency4-8 88765327 78934589 -11.07% BenchmarkConcurrency8-8 93624724 82432804 -11.95% BenchmarkConcurrency4Readers1Writer-8 90931759 80533879 -11.43% BenchmarkConcurrency8Writers-8 237043373 233709595 -1.41% Signed-off-by: Teemu Koponen <koponen@styra.com>
This commit is contained in:
committed by
Patrick East
parent
762e9ad078
commit
af970b7ef6
@@ -5,6 +5,7 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/big"
|
||||
)
|
||||
@@ -84,6 +85,18 @@ func Compare(a, b interface{}) int {
|
||||
}
|
||||
return 1
|
||||
case Number:
|
||||
if ai, err := json.Number(a).Int64(); err == nil {
|
||||
if bi, err := json.Number(b.(Number)).Int64(); err == nil {
|
||||
if ai == bi {
|
||||
return 0
|
||||
}
|
||||
if ai < bi {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
bigA, ok := new(big.Float).SetString(string(a))
|
||||
if !ok {
|
||||
panic("illegal value")
|
||||
|
||||
@@ -27,6 +27,9 @@ func TestCompare(t *testing.T) {
|
||||
// Numbers
|
||||
{"0", "1", -1},
|
||||
{"1", "0", 1},
|
||||
{"0", "0", 0},
|
||||
{"0", "1.5", -1},
|
||||
{"1.5", "0", 1},
|
||||
|
||||
// Object comparisons are consistent
|
||||
{`{1: 2, 3: 4}`, `{4: 3, 1: 2}`, -1},
|
||||
|
||||
+168
-4
@@ -8,6 +8,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sort"
|
||||
@@ -377,6 +378,8 @@ func (term *Term) Equal(other *Term) bool {
|
||||
return v.Equal(other.Value)
|
||||
case Boolean:
|
||||
return v.Equal(other.Value)
|
||||
case Number:
|
||||
return v.Equal(other.Value)
|
||||
case String:
|
||||
return v.Equal(other.Value)
|
||||
case Var:
|
||||
@@ -1426,8 +1429,48 @@ func (s *set) Slice() []*Term {
|
||||
|
||||
func (s *set) insert(x *Term) {
|
||||
hash := x.Hash()
|
||||
var equal func(v Value) bool
|
||||
|
||||
switch x := x.Value.(type) {
|
||||
case Null, Boolean, String, Var:
|
||||
equal = func(y Value) bool { return x == y }
|
||||
case Number:
|
||||
if xi, err := json.Number(x).Int64(); err == nil {
|
||||
equal = func(y Value) bool {
|
||||
if y, ok := y.(Number); ok {
|
||||
if yi, err := json.Number(y).Int64(); err == nil {
|
||||
return xi == yi
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
a, ok := new(big.Float).SetString(string(x))
|
||||
if !ok {
|
||||
panic("illegal value")
|
||||
}
|
||||
|
||||
equal = func(b Value) bool {
|
||||
if b, ok := b.(Number); ok {
|
||||
b, ok := new(big.Float).SetString(string(b))
|
||||
if !ok {
|
||||
panic("illegal value")
|
||||
}
|
||||
|
||||
return a.Cmp(b) == 0
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
default:
|
||||
equal = func(y Value) bool { return Compare(x, y) == 0 }
|
||||
}
|
||||
|
||||
for curr, ok := s.elems[hash]; ok; {
|
||||
if Compare(curr, x) == 0 {
|
||||
if equal(curr.Value) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1441,8 +1484,48 @@ func (s *set) insert(x *Term) {
|
||||
|
||||
func (s *set) get(x *Term) *Term {
|
||||
hash := x.Hash()
|
||||
var equal func(v Value) bool
|
||||
|
||||
switch x := x.Value.(type) {
|
||||
case Null, Boolean, String, Var:
|
||||
equal = func(y Value) bool { return x == y }
|
||||
case Number:
|
||||
if xi, err := json.Number(x).Int64(); err == nil {
|
||||
equal = func(y Value) bool {
|
||||
if y, ok := y.(Number); ok {
|
||||
if yi, err := json.Number(y).Int64(); err == nil {
|
||||
return xi == yi
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
a, ok := new(big.Float).SetString(string(x))
|
||||
if !ok {
|
||||
panic("illegal value")
|
||||
}
|
||||
|
||||
equal = func(b Value) bool {
|
||||
if b, ok := b.(Number); ok {
|
||||
b, ok := new(big.Float).SetString(string(b))
|
||||
if !ok {
|
||||
panic("illegal value")
|
||||
}
|
||||
|
||||
return a.Cmp(b) == 0
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
default:
|
||||
equal = func(y Value) bool { return Compare(x, y) == 0 }
|
||||
}
|
||||
|
||||
for curr, ok := s.elems[hash]; ok; {
|
||||
if Compare(curr, x) == 0 {
|
||||
if equal(curr.Value) {
|
||||
return curr
|
||||
}
|
||||
|
||||
@@ -1747,8 +1830,49 @@ func (obj object) String() string {
|
||||
|
||||
func (obj *object) get(k *Term) *objectElem {
|
||||
hash := k.Hash()
|
||||
|
||||
var equal func(v Value) bool
|
||||
|
||||
switch x := k.Value.(type) {
|
||||
case Null, Boolean, String, Var:
|
||||
equal = func(y Value) bool { return x == y }
|
||||
case Number:
|
||||
if xi, err := json.Number(x).Int64(); err == nil {
|
||||
equal = func(y Value) bool {
|
||||
if y, ok := y.(Number); ok {
|
||||
if yi, err := json.Number(y).Int64(); err == nil {
|
||||
return xi == yi
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
a, ok := new(big.Float).SetString(string(x))
|
||||
if !ok {
|
||||
panic("illegal value")
|
||||
}
|
||||
|
||||
equal = func(b Value) bool {
|
||||
if b, ok := b.(Number); ok {
|
||||
b, ok := new(big.Float).SetString(string(b))
|
||||
if !ok {
|
||||
panic("illegal value")
|
||||
}
|
||||
|
||||
return a.Cmp(b) == 0
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
default:
|
||||
equal = func(y Value) bool { return Compare(x, y) == 0 }
|
||||
}
|
||||
|
||||
for curr := obj.elems[hash]; curr != nil; curr = curr.next {
|
||||
if Compare(curr.key, k) == 0 {
|
||||
if equal(curr.key.Value) {
|
||||
return curr
|
||||
}
|
||||
}
|
||||
@@ -1758,8 +1882,48 @@ func (obj *object) get(k *Term) *objectElem {
|
||||
func (obj *object) insert(k, v *Term) {
|
||||
hash := k.Hash()
|
||||
head := obj.elems[hash]
|
||||
var equal func(v Value) bool
|
||||
|
||||
switch x := k.Value.(type) {
|
||||
case Null, Boolean, String, Var:
|
||||
equal = func(y Value) bool { return x == y }
|
||||
case Number:
|
||||
if xi, err := json.Number(x).Int64(); err == nil {
|
||||
equal = func(y Value) bool {
|
||||
if y, ok := y.(Number); ok {
|
||||
if yi, err := json.Number(y).Int64(); err == nil {
|
||||
return xi == yi
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
a, ok := new(big.Float).SetString(string(x))
|
||||
if !ok {
|
||||
panic("illegal value")
|
||||
}
|
||||
|
||||
equal = func(b Value) bool {
|
||||
if b, ok := b.(Number); ok {
|
||||
b, ok := new(big.Float).SetString(string(b))
|
||||
if !ok {
|
||||
panic("illegal value")
|
||||
}
|
||||
|
||||
return a.Cmp(b) == 0
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
default:
|
||||
equal = func(y Value) bool { return Compare(x, y) == 0 }
|
||||
}
|
||||
|
||||
for curr := head; curr != nil; curr = curr.next {
|
||||
if Compare(curr.key, k) == 0 {
|
||||
if equal(curr.key.Value) {
|
||||
curr.value = v
|
||||
return
|
||||
}
|
||||
|
||||
@@ -84,6 +84,40 @@ func TestInterfaceToValue(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func TestObjectInsertGetLen(t *testing.T) {
|
||||
tests := []struct {
|
||||
insert [][2]string
|
||||
expected map[string]string
|
||||
}{
|
||||
{[][2]string{{`null`, `value1`}, {`null`, `value2`}}, map[string]string{`null`: `value2`}},
|
||||
{[][2]string{{`false`, `value`}, {`true`, `value1`}, {`true`, `value2`}}, map[string]string{`false`: `value`, `true`: `value2`}},
|
||||
{[][2]string{{`0`, `value`}, {`1`, `value1`}, {`1`, `value2`}, {`1.5`, `value`}}, map[string]string{`0`: `value`, `1`: `value2`, `1.5`: `value`}},
|
||||
{[][2]string{{`"string"`, `value1`}, {`"string"`, `value2`}}, map[string]string{`"string"`: `value2`}},
|
||||
{[][2]string{{`["other"]`, `value1`}, {`["other"]`, `value2`}}, map[string]string{`["other"]`: `value2`}},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
o := NewObject()
|
||||
for _, kv := range tc.insert {
|
||||
o.Insert(MustParseTerm(kv[0]), MustParseTerm(kv[1]))
|
||||
|
||||
if v := o.Get(MustParseTerm(kv[0])); v == nil || !MustParseTerm(kv[1]).Equal(v) {
|
||||
t.Errorf("Expected the object to contain %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
if o.Len() != len(tc.expected) {
|
||||
t.Errorf("Expected the object to have %v entries", len(tc.expected))
|
||||
}
|
||||
|
||||
for k, v := range tc.expected {
|
||||
if x := o.Get(MustParseTerm(k)); x == nil || !MustParseTerm(v).Equal(x) {
|
||||
t.Errorf("Expected the object to contain %v", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestObjectSetOperations(t *testing.T) {
|
||||
|
||||
a := MustParseTerm(`{"a": "b", "c": "d"}`).Value.(Object)
|
||||
@@ -522,6 +556,41 @@ func TestSetMap(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func TestSetAddContainsLen(t *testing.T) {
|
||||
tests := []struct {
|
||||
add []string
|
||||
expected []string
|
||||
}{
|
||||
{[]string{`null`, `null`}, []string{`null`}},
|
||||
{[]string{`true`, `true`, `false`}, []string{`true`, `false`}},
|
||||
{[]string{`0`, `1`, `1`, `1.5`}, []string{`0`, `1`, `1.5`}},
|
||||
{[]string{`"string"`, `"string"`}, []string{`"string"`}},
|
||||
{[]string{`["other"]`, `["other"]`}, []string{`["other"]`}},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
s := NewSet()
|
||||
for _, v := range tc.add {
|
||||
x := MustParseTerm(v)
|
||||
s.Add(x)
|
||||
|
||||
if !s.Contains(x) {
|
||||
t.Errorf("Expected the set to contain %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
if s.Len() != len(tc.expected) {
|
||||
t.Errorf("Expected the set to have %v entries", len(tc.expected))
|
||||
}
|
||||
|
||||
for _, v := range tc.expected {
|
||||
if !s.Contains(MustParseTerm(v)) {
|
||||
t.Errorf("Expected the set to contain %v", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetOperations(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
|
||||
Reference in New Issue
Block a user