Skip to content

Commit 3188e04

Browse files
authored
Use sync.Pool for eval func objects (#8054)
These did not show up in escape analysis as they weren't pointers. I guess you could say they... escaped the analysis. But some pprof exploration revealed these were heavy allocation sources, and just like that, we have almost 2 million allocations less running the `regal lint bundle` benchmark. And a nice reduction in `B/op` too! Also in this PR a simple change to the regexp built-in functions, where a `RWMutex` is now used to reduce blocking, which also showed up in the pprof data (although with marginal impact). **Before** ``` 756176916 ns/op 2614292780 B/op 53440051 allocs/op ``` **After** ``` 737606792 ns/op 2469976856 B/op 51680605 allocs/op ``` Signed-off-by: Anders Eknert <[email protected]>
1 parent e00c4b4 commit 3188e04

File tree

2 files changed

+114
-60
lines changed

2 files changed

+114
-60
lines changed

v1/topdown/eval.go

Lines changed: 92 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,17 @@ type eval struct {
119119
defined bool
120120
}
121121

122-
type evp struct {
123-
pool sync.Pool
124-
}
122+
type (
123+
evp struct {
124+
pool sync.Pool
125+
}
126+
evfp struct {
127+
pool sync.Pool
128+
}
129+
evbp struct {
130+
pool sync.Pool
131+
}
132+
)
125133

126134
func (ep *evp) Put(e *eval) {
127135
ep.pool.Put(e)
@@ -131,14 +139,52 @@ func (ep *evp) Get() *eval {
131139
return ep.pool.Get().(*eval)
132140
}
133141

134-
var evalPool = evp{
135-
pool: sync.Pool{
136-
New: func() any {
137-
return &eval{}
138-
},
139-
},
142+
func (ep *evfp) Put(e *evalFunc) {
143+
if e != nil {
144+
e.e, e.terms, e.ir = nil, nil, nil
145+
ep.pool.Put(e)
146+
}
147+
}
148+
149+
func (ep *evfp) Get() *evalFunc {
150+
return ep.pool.Get().(*evalFunc)
151+
}
152+
153+
func (ep *evbp) Put(e *evalBuiltin) {
154+
if e != nil {
155+
e.e, e.bi, e.bctx, e.f, e.terms = nil, nil, nil, nil, nil
156+
ep.pool.Put(e)
157+
}
158+
}
159+
160+
func (ep *evbp) Get() *evalBuiltin {
161+
return ep.pool.Get().(*evalBuiltin)
140162
}
141163

164+
var (
165+
evalPool = &evp{
166+
pool: sync.Pool{
167+
New: func() any {
168+
return &eval{}
169+
},
170+
},
171+
}
172+
evalFuncPool = &evfp{
173+
pool: sync.Pool{
174+
New: func() any {
175+
return &evalFunc{}
176+
},
177+
},
178+
}
179+
evalBuiltinPool = &evbp{
180+
pool: sync.Pool{
181+
New: func() any {
182+
return &evalBuiltin{}
183+
},
184+
},
185+
}
186+
)
187+
142188
func (e *eval) Run(iter evalIterator) error {
143189
if !e.traceEnabled {
144190
// avoid function literal escaping to heap if we don't need the trace
@@ -401,9 +447,11 @@ func (e *eval) evalExpr(iter evalIterator) error {
401447
}
402448
return nil
403449
}
404-
expr := e.query[e.index]
405450

406-
e.traceEval(expr)
451+
expr := e.query[e.index]
452+
if e.traceEnabled {
453+
e.traceEval(expr)
454+
}
407455

408456
if len(expr.With) > 0 {
409457
return e.evalWith(iter)
@@ -521,7 +569,7 @@ func (e *eval) evalStep(iter evalIterator) error {
521569
// generateVar inlined here to avoid extra allocations in hot path
522570
rterm := ast.VarTerm(e.fmtVarTerm())
523571
err = e.unify(terms, rterm, func() error {
524-
if e.saveSet.Contains(rterm, e.bindings) {
572+
if e.saveSet != nil && e.saveSet.Contains(rterm, e.bindings) {
525573
return e.saveExpr(ast.NewExpr(rterm), e.bindings, func() error {
526574
return iter(e)
527575
})
@@ -888,7 +936,6 @@ func (e *eval) evalNotPartialSupport(negationID uint64, expr *ast.Expr, unknowns
888936
}
889937

890938
func (e *eval) evalCall(terms []*ast.Term, iter unifyIterator) error {
891-
892939
ref := terms[0].Value.(ast.Ref)
893940

894941
mock, mocked := e.functionMocks.Get(ref)
@@ -912,8 +959,8 @@ func (e *eval) evalCall(terms []*ast.Term, iter unifyIterator) error {
912959

913960
if ref[0].Equal(ast.DefaultRootDocument) {
914961
if mocked {
915-
f := e.compiler.TypeEnv.Get(ref).(*types.Function)
916-
return e.evalCallValue(f.Arity(), terms, mock, iter)
962+
arity := e.compiler.TypeEnv.GetByRef(ref).(*types.Function).Arity()
963+
return e.evalCallValue(arity, terms, mock, iter)
917964
}
918965

919966
var ir *ast.IndexResult
@@ -928,11 +975,13 @@ func (e *eval) evalCall(terms []*ast.Term, iter unifyIterator) error {
928975
return err
929976
}
930977

931-
eval := evalFunc{
932-
e: e,
933-
terms: terms,
934-
ir: ir,
935-
}
978+
eval := evalFuncPool.Get()
979+
defer evalFuncPool.Put(eval)
980+
981+
eval.e = e
982+
eval.terms = terms
983+
eval.ir = ir
984+
936985
return eval.eval(iter)
937986
}
938987

@@ -991,13 +1040,14 @@ func (e *eval) evalCall(terms []*ast.Term, iter unifyIterator) error {
9911040
}
9921041
}
9931042

994-
eval := evalBuiltin{
995-
e: e,
996-
bi: bi,
997-
bctx: bctx,
998-
f: f,
999-
terms: terms[1:],
1000-
}
1043+
eval := evalBuiltinPool.Get()
1044+
defer evalBuiltinPool.Put(eval)
1045+
1046+
eval.e = e
1047+
eval.bi = bi
1048+
eval.bctx = bctx
1049+
eval.f = f
1050+
eval.terms = terms[1:]
10011051

10021052
return eval.eval(iter)
10031053
}
@@ -1054,7 +1104,9 @@ func (e *eval) biunify(a, b *ast.Term, b1, b2 *bindings, iter unifyIterator) err
10541104
case ast.Var, ast.Ref, *ast.ArrayComprehension:
10551105
return e.biunifyValues(a, b, b1, b2, iter)
10561106
case *ast.Array:
1057-
return e.biunifyArrays(vA, vB, b1, b2, iter)
1107+
if vA.Len() == vB.Len() {
1108+
return e.biunifyArraysRec(vA, vB, b1, b2, iter, 0)
1109+
}
10581110
}
10591111
case ast.Object:
10601112
switch vB := b.Value.(type) {
@@ -1069,13 +1121,6 @@ func (e *eval) biunify(a, b *ast.Term, b1, b2 *bindings, iter unifyIterator) err
10691121
return nil
10701122
}
10711123

1072-
func (e *eval) biunifyArrays(a, b *ast.Array, b1, b2 *bindings, iter unifyIterator) error {
1073-
if a.Len() != b.Len() {
1074-
return nil
1075-
}
1076-
return e.biunifyArraysRec(a, b, b1, b2, iter, 0)
1077-
}
1078-
10791124
func (e *eval) biunifyArraysRec(a, b *ast.Array, b1, b2 *bindings, iter unifyIterator, idx int) error {
10801125
if idx == a.Len() {
10811126
return iter()
@@ -2052,8 +2097,7 @@ type evalFunc struct {
20522097
terms []*ast.Term
20532098
}
20542099

2055-
func (e evalFunc) eval(iter unifyIterator) error {
2056-
2100+
func (e *evalFunc) eval(iter unifyIterator) error {
20572101
if e.ir.Empty() {
20582102
return nil
20592103
}
@@ -2065,13 +2109,13 @@ func (e evalFunc) eval(iter unifyIterator) error {
20652109
argCount = len(e.ir.Default.Head.Args)
20662110
}
20672111

2068-
if len(e.ir.Else) > 0 && e.e.unknown(e.e.query[e.e.index], e.e.bindings) {
2069-
// Partial evaluation of ordered rules is not supported currently. Save the
2070-
// expression and continue. This could be revisited in the future.
2071-
return e.e.saveCall(argCount, e.terms, iter)
2072-
}
2073-
20742112
if e.e.partial() {
2113+
if len(e.ir.Else) > 0 && e.e.unknown(e.e.query[e.e.index], e.e.bindings) {
2114+
// Partial evaluation of ordered rules is not supported currently. Save the
2115+
// expression and continue. This could be revisited in the future.
2116+
return e.e.saveCall(argCount, e.terms, iter)
2117+
}
2118+
20752119
var mustGenerateSupport bool
20762120

20772121
if defRule := e.ir.Default; defRule != nil {
@@ -2109,7 +2153,7 @@ func (e evalFunc) eval(iter unifyIterator) error {
21092153
return e.evalValue(iter, argCount, e.ir.EarlyExit)
21102154
}
21112155

2112-
func (e evalFunc) evalValue(iter unifyIterator, argCount int, findOne bool) error {
2156+
func (e *evalFunc) evalValue(iter unifyIterator, argCount int, findOne bool) error {
21132157
var cacheKey ast.Ref
21142158
if !e.e.partial() {
21152159
var hit bool
@@ -2194,7 +2238,7 @@ func (e evalFunc) evalValue(iter unifyIterator, argCount int, findOne bool) erro
21942238
})
21952239
}
21962240

2197-
func (e evalFunc) evalCache(argCount int, iter unifyIterator) (ast.Ref, bool, error) {
2241+
func (e *evalFunc) evalCache(argCount int, iter unifyIterator) (ast.Ref, bool, error) {
21982242
plen := len(e.terms)
21992243
if plen == argCount+2 { // func name + output = 2
22002244
plen -= 1
@@ -2226,7 +2270,7 @@ func (e evalFunc) evalCache(argCount int, iter unifyIterator) (ast.Ref, bool, er
22262270
return cacheKey, false, nil
22272271
}
22282272

2229-
func (e evalFunc) evalOneRule(iter unifyIterator, rule *ast.Rule, args []*ast.Term, cacheKey ast.Ref, prev *ast.Term, findOne bool) (*ast.Term, error) {
2273+
func (e *evalFunc) evalOneRule(iter unifyIterator, rule *ast.Rule, args []*ast.Term, cacheKey ast.Ref, prev *ast.Term, findOne bool) (*ast.Term, error) {
22302274
child := evalPool.Get()
22312275
defer evalPool.Put(child)
22322276

@@ -2288,7 +2332,7 @@ func (e evalFunc) evalOneRule(iter unifyIterator, rule *ast.Rule, args []*ast.Te
22882332
return result, err
22892333
}
22902334

2291-
func (e evalFunc) partialEvalSupport(declArgsLen int, iter unifyIterator) error {
2335+
func (e *evalFunc) partialEvalSupport(declArgsLen int, iter unifyIterator) error {
22922336
path := e.e.namespaceRef(e.terms[0].Value.(ast.Ref))
22932337

22942338
if !e.e.saveSupport.Exists(path) {
@@ -2316,7 +2360,7 @@ func (e evalFunc) partialEvalSupport(declArgsLen int, iter unifyIterator) error
23162360
return e.e.saveCall(declArgsLen, append([]*ast.Term{term}, e.terms[1:]...), iter)
23172361
}
23182362

2319-
func (e evalFunc) partialEvalSupportRule(rule *ast.Rule, path ast.Ref) error {
2363+
func (e *evalFunc) partialEvalSupportRule(rule *ast.Rule, path ast.Ref) error {
23202364
child := evalPool.Get()
23212365
defer evalPool.Put(child)
23222366

v1/topdown/regex.go

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,15 @@ import (
1616
"github.com/open-policy-agent/opa/v1/topdown/builtins"
1717
)
1818

19-
const regexCacheMaxSize = 100
20-
const regexInterQueryValueCacheHits = "rego_builtin_regex_interquery_value_cache_hits"
19+
const (
20+
regexCacheMaxSize = 100
21+
regexInterQueryValueCacheHits = "rego_builtin_regex_interquery_value_cache_hits"
22+
)
2123

22-
var regexpCacheLock = sync.Mutex{}
23-
var regexpCache map[string]*regexp.Regexp
24+
var (
25+
regexpCacheLock = sync.RWMutex{}
26+
regexpCache = make(map[string]*regexp.Regexp)
27+
)
2428

2529
func builtinRegexIsValid(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
2630
if s, err := builtins.StringOperand(operands[0].Value, 1); err == nil {
@@ -103,7 +107,8 @@ func builtinRegexSplit(bctx BuiltinContext, operands []*ast.Term, iter func(*ast
103107
func getRegexp(bctx BuiltinContext, pat string) (*regexp.Regexp, error) {
104108
if bctx.InterQueryBuiltinValueCache != nil {
105109
// TODO: Use named cache
106-
val, ok := bctx.InterQueryBuiltinValueCache.Get(ast.String(pat))
110+
var key ast.Value = ast.String(pat)
111+
val, ok := bctx.InterQueryBuiltinValueCache.Get(key)
107112
if ok {
108113
res, valid := val.(*regexp.Regexp)
109114
if !valid {
@@ -120,42 +125,48 @@ func getRegexp(bctx BuiltinContext, pat string) (*regexp.Regexp, error) {
120125
if err != nil {
121126
return nil, err
122127
}
123-
bctx.InterQueryBuiltinValueCache.Insert(ast.String(pat), re)
128+
bctx.InterQueryBuiltinValueCache.Insert(key, re)
124129
return re, nil
125130
}
126131

127-
regexpCacheLock.Lock()
128-
defer regexpCacheLock.Unlock()
132+
regexpCacheLock.RLock()
129133
re, ok := regexpCache[pat]
134+
numCached := len(regexpCache)
135+
regexpCacheLock.RUnlock()
130136
if !ok {
131137
var err error
132138
re, err = regexp.Compile(pat)
133139
if err != nil {
134140
return nil, err
135141
}
136-
if len(regexpCache) >= regexCacheMaxSize {
142+
143+
regexpCacheLock.Lock()
144+
if numCached >= regexCacheMaxSize {
137145
// Delete a (semi-)random key to make room for the new one.
138146
for k := range regexpCache {
139147
delete(regexpCache, k)
140148
break
141149
}
142150
}
143151
regexpCache[pat] = re
152+
regexpCacheLock.Unlock()
144153
}
145154
return re, nil
146155
}
147156

148157
func getRegexpTemplate(pat string, delimStart, delimEnd byte) (*regexp.Regexp, error) {
149-
regexpCacheLock.Lock()
150-
defer regexpCacheLock.Unlock()
158+
regexpCacheLock.RLock()
151159
re, ok := regexpCache[pat]
160+
regexpCacheLock.RUnlock()
152161
if !ok {
153162
var err error
154163
re, err = compileRegexTemplate(pat, delimStart, delimEnd)
155164
if err != nil {
156165
return nil, err
157166
}
167+
regexpCacheLock.Lock()
158168
regexpCache[pat] = re
169+
regexpCacheLock.Unlock()
159170
}
160171
return re, nil
161172
}
@@ -264,7 +275,6 @@ func builtinRegexReplace(bctx BuiltinContext, operands []*ast.Term, iter func(*a
264275
}
265276

266277
func init() {
267-
regexpCache = map[string]*regexp.Regexp{}
268278
RegisterBuiltinFunc(ast.RegexIsValid.Name, builtinRegexIsValid)
269279
RegisterBuiltinFunc(ast.RegexMatch.Name, builtinRegexMatch)
270280
RegisterBuiltinFunc(ast.RegexMatchDeprecated.Name, builtinRegexMatch)

0 commit comments

Comments
 (0)