Skip to content

Commit

Permalink
perf: Reduce allocations and printfs in G-function cache. (#958)
Browse files Browse the repository at this point in the history
Previously, the G-function cache used repeated fmt.Sprintf("%v", x)
and string concatenation to generate its cache key. This resulted in
lots of unnecessary allocations, both in the process of Sprintf
and in an O(n²) string building loop. This change allocates a string
buffer and copies strings into it to reduce the number of allocations.
This solution was found to be faster than using a composite map key
(like a struct {string, string, string, int}).

Includes minor ancillary changes:

- Replaces `;` as the separator with a null character, making the cache
  safe for any string without nulls (i.e. your role could contain `;`,
  if you were the type of person who wanted to do that).
- Adds a parameterized RBAC benchmark, with longer role names,
  to better see performance results in situations where clients
  have longer strings that might require more allocations.
  • Loading branch information
thetorpedodog committed Feb 23, 2022
1 parent 8dd3259 commit a6cd905
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 8 deletions.
72 changes: 71 additions & 1 deletion model_b_test.go
Expand Up @@ -16,8 +16,9 @@ package casbin

import (
"fmt"
"github.com/casbin/casbin/v2/util"
"testing"

"github.com/casbin/casbin/v2/util"
)

func rawEnforce(sub string, obj string, act string) bool {
Expand Down Expand Up @@ -54,6 +55,75 @@ func BenchmarkRBACModel(b *testing.B) {
}
}

func BenchmarkRBACModelSizes(b *testing.B) {
cases := []struct {
name string
roles int
resources int
users int
}{
{name: "small", roles: 100, resources: 10, users: 1000},
{name: "medium", roles: 1000, resources: 100, users: 10000},
{name: "large", roles: 10000, resources: 1000, users: 100000},
}
for _, c := range cases {
c := c

e, err := NewEnforcer("examples/rbac_model.conf", false)
if err != nil {
b.Fatal(err)
}

pPolicies := make([][]string, c.roles)
for i := range pPolicies {
pPolicies[i] = []string{
fmt.Sprintf("group-has-a-very-long-name-%d", i),
fmt.Sprintf("data-has-a-very-long-name-%d", i%c.resources),
"read",
}
}
if _, err := e.AddPolicies(pPolicies); err != nil {
b.Fatal(err)
}

gPolicies := make([][]string, c.users)
for i := range gPolicies {
gPolicies[i] = []string{
fmt.Sprintf("user-has-a-very-long-name-%d", i),
fmt.Sprintf("group-has-a-very-long-name-%d", i%c.roles),
}
}
if _, err := e.AddGroupingPolicies(gPolicies); err != nil {
b.Fatal(err)
}

// Set up enforcements, alternating between things a user can access
// and things they cannot. Use 17 tests so that we get a variety of users
// and roles rather than always landing on a multiple of 2/10/whatever.
enforcements := make([][]interface{}, 17)
for i := range enforcements {
userNum := (c.users / len(enforcements)) * i
roleNum := userNum % c.roles
resourceNum := roleNum % c.resources
if i%2 == 0 {
resourceNum += 1
resourceNum %= c.resources
}
enforcements[i] = []interface{}{
fmt.Sprintf("user-has-a-very-long-name-%d", userNum),
fmt.Sprintf("data-has-a-very-long-name-%d", resourceNum),
"read",
}
}

b.Run(c.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = e.Enforce(enforcements[i%len(enforcements)]...)
}
})
}
}

func BenchmarkRBACModelSmall(b *testing.B) {
e, _ := NewEnforcer("examples/rbac_model.conf", false)

Expand Down
25 changes: 18 additions & 7 deletions util/builtin_operators.go
Expand Up @@ -329,24 +329,35 @@ func GlobMatchFunc(args ...interface{}) (interface{}, error) {
return GlobMatch(name1, name2)
}

// GenerateGFunction is the factory method of the g(_, _) function.
// GenerateGFunction is the factory method of the g(_, _[, _]) function.
func GenerateGFunction(rm rbac.RoleManager) govaluate.ExpressionFunction {
memorized := map[string]bool{}

return func(args ...interface{}) (interface{}, error) {
name1 := args[0].(string)
name2 := args[1].(string)
// Like all our other govaluate functions, all args are strings.

key := ""
for index := 0; index < len(args); index++ {
key += ";" + fmt.Sprintf("%v", args[index])
// Allocate and generate a cache key from the arguments...
total := len(args)
for _, a := range args {
aStr := a.(string)
total += len(aStr)
}
builder := strings.Builder{}
builder.Grow(total)
for _, arg := range args {
builder.WriteByte(0)
builder.WriteString(arg.(string))
}
key := builder.String()

// ...and see if we've already calculated this.
v, found := memorized[key]
if found {
return v, nil
}

// If not, do the calculation.
// There are guaranteed to be exactly 2 or 3 arguments.
name1, name2 := args[0].(string), args[1].(string)
if rm == nil {
v = name1 == name2
} else if len(args) == 2 {
Expand Down

0 comments on commit a6cd905

Please sign in to comment.