Skip to content

Commit

Permalink
Reduce fmt.Sprintf allocations in query encoding (#2919)
Browse files Browse the repository at this point in the history
* Pre-compute prefix when array is not flat

* Switch to string concat for object keys

* Change array key formatting and add comments about why sprintf is not used

* Add changelog entry

* Update .changelog/fd3c62c5-c1cf-48de-a223-ea0fdf4136c9.json

---------

Co-authored-by: Luc Talatinian <[email protected]>
  • Loading branch information
kgeckhart and lucix-aws authored Jan 24, 2025
1 parent d5773a9 commit ed8a3ca
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 26 deletions.
8 changes: 8 additions & 0 deletions .changelog/fd3c62c5-c1cf-48de-a223-ea0fdf4136c9.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"id": "fd3c62c5-c1cf-48de-a223-ea0fdf4136c9",
"type": "feature",
"description": "Reduce allocations in query encoding.",
"modules": [
"."
]
}
29 changes: 9 additions & 20 deletions aws/protocol/query/array.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package query

import (
"fmt"
"net/url"
"strconv"
)

// Array represents the encoding of Query lists and sets. A Query array is a
Expand All @@ -21,19 +21,8 @@ type Array struct {
// keys for each element in the list. For example, an entry might have the
// key "ParentStructure.ListName.member.MemberName.1".
//
// While this is currently represented as a string that gets added to, it
// could also be represented as a stack that only gets condensed into a
// string when a finalized key is created. This could potentially reduce
// allocations.
// When the array is not flat the prefix will contain the memberName otherwise the memberName is ignored
prefix string
// Whether the list is flat or not. A list that is not flat will produce the
// following entry to the url.Values for a given entry:
// ListName.MemberName.1=value
// A list that is flat will produce the following:
// ListName.1=value
flat bool
// The location name of the member. In most cases this should be "member".
memberName string
// Elements are stored in values, so we keep track of the list size here.
size int32
// Empty lists are encoded as "<prefix>=", if we add a value later we will
Expand All @@ -45,11 +34,14 @@ func newArray(values url.Values, prefix string, flat bool, memberName string) *A
emptyValue := newValue(values, prefix, flat)
emptyValue.String("")

if !flat {
// This uses string concatenation in place of fmt.Sprintf as fmt.Sprintf has a much higher resource overhead
prefix = prefix + keySeparator + memberName
}

return &Array{
values: values,
prefix: prefix,
flat: flat,
memberName: memberName,
emptyValue: emptyValue,
}
}
Expand All @@ -63,10 +55,7 @@ func (a *Array) Value() Value {

// Query lists start a 1, so adjust the size first
a.size++
prefix := a.prefix
if !a.flat {
prefix = fmt.Sprintf("%s.%s", prefix, a.memberName)
}
// Lists can't have flat members
return newValue(a.values, fmt.Sprintf("%s.%d", prefix, a.size), false)
// This uses string concatenation in place of fmt.Sprintf as fmt.Sprintf has a much higher resource overhead
return newValue(a.values, a.prefix+keySeparator+strconv.FormatInt(int64(a.size), 10), false)
}
11 changes: 5 additions & 6 deletions aws/protocol/query/object.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package query

import (
"fmt"
"net/url"
)
import "net/url"

// Object represents the encoding of Query structures and unions. A Query
// object is a representation of a mapping of string keys to arbitrary
Expand Down Expand Up @@ -56,14 +53,16 @@ func (o *Object) FlatKey(name string) Value {

func (o *Object) key(name string, flatValue bool) Value {
if o.prefix != "" {
return newValue(o.values, fmt.Sprintf("%s.%s", o.prefix, name), flatValue)
// This uses string concatenation in place of fmt.Sprintf as fmt.Sprintf has a much higher resource overhead
return newValue(o.values, o.prefix+keySeparator+name, flatValue)
}
return newValue(o.values, name, flatValue)
}

func (o *Object) keyWithValues(name string, flatValue bool) Value {
if o.prefix != "" {
return newAppendValue(o.values, fmt.Sprintf("%s.%s", o.prefix, name), flatValue)
// This uses string concatenation in place of fmt.Sprintf as fmt.Sprintf has a much higher resource overhead
return newAppendValue(o.values, o.prefix+keySeparator+name, flatValue)
}
return newAppendValue(o.values, name, flatValue)
}
2 changes: 2 additions & 0 deletions aws/protocol/query/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"github.com/aws/smithy-go/encoding/httpbinding"
)

const keySeparator = "."

// Value represents a Query Value type.
type Value struct {
// The query values to add the value to.
Expand Down
64 changes: 64 additions & 0 deletions aws/protocol/query/value_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package query

import (
"fmt"
"strconv"
"testing"
)

var output string

func Benchmark_sprintf_strings(b *testing.B) {
for i := 0; i < b.N; i++ {
output = fmt.Sprintf("%s.%s", "foo", "bar")
}
}

func Benchmark_concat_strings(b *testing.B) {
for i := 0; i < b.N; i++ {
output = "foo" + keySeparator + "bar"
}
}

func Benchmark_int_formatting(b *testing.B) {
benchmarkFuncs := []struct {
name string
formatter func(val int32)
}{
{
name: "array - sprintf", formatter: func(val int32) {
output = fmt.Sprintf("%s.%d", "foo", val)
},
},
{
name: "array - concat strconv", formatter: func(val int32) {
output = "foo" + keySeparator + strconv.FormatInt(int64(val), 10)
},
},
{
name: "map - sprintf", formatter: func(val int32) {
output = fmt.Sprintf("%s.%d.%s", "foo", val, "bar")
output = fmt.Sprintf("%s.%d.%s", "foo", val, "bar")
},
},
{
name: "map - concat strconv", formatter: func(val int32) {
valString := strconv.FormatInt(int64(val), 10)
output = "foo" + keySeparator + valString + keySeparator + "bar"
output = "foo" + keySeparator + valString + keySeparator + "bar"
},
},
}

sizesToTest := []int32{1, 10, 100, 250, 500, 1000}

for _, bm := range benchmarkFuncs {
for _, size := range sizesToTest {
b.Run(fmt.Sprintf("%s with %d size", bm.name, size), func(b *testing.B) {
for i := 0; i < b.N; i++ {
bm.formatter(size)
}
})
}
}
}

0 comments on commit ed8a3ca

Please sign in to comment.