Skip to content

Commit ed8a3ca

Browse files
kgeckhartlucix-aws
andauthored
Reduce fmt.Sprintf allocations in query encoding (#2919)
* 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]>
1 parent d5773a9 commit ed8a3ca

File tree

5 files changed

+88
-26
lines changed

5 files changed

+88
-26
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"id": "fd3c62c5-c1cf-48de-a223-ea0fdf4136c9",
3+
"type": "feature",
4+
"description": "Reduce allocations in query encoding.",
5+
"modules": [
6+
"."
7+
]
8+
}

aws/protocol/query/array.go

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package query
22

33
import (
4-
"fmt"
54
"net/url"
5+
"strconv"
66
)
77

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

37+
if !flat {
38+
// This uses string concatenation in place of fmt.Sprintf as fmt.Sprintf has a much higher resource overhead
39+
prefix = prefix + keySeparator + memberName
40+
}
41+
4842
return &Array{
4943
values: values,
5044
prefix: prefix,
51-
flat: flat,
52-
memberName: memberName,
5345
emptyValue: emptyValue,
5446
}
5547
}
@@ -63,10 +55,7 @@ func (a *Array) Value() Value {
6355

6456
// Query lists start a 1, so adjust the size first
6557
a.size++
66-
prefix := a.prefix
67-
if !a.flat {
68-
prefix = fmt.Sprintf("%s.%s", prefix, a.memberName)
69-
}
7058
// Lists can't have flat members
71-
return newValue(a.values, fmt.Sprintf("%s.%d", prefix, a.size), false)
59+
// This uses string concatenation in place of fmt.Sprintf as fmt.Sprintf has a much higher resource overhead
60+
return newValue(a.values, a.prefix+keySeparator+strconv.FormatInt(int64(a.size), 10), false)
7261
}

aws/protocol/query/object.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
package query
22

3-
import (
4-
"fmt"
5-
"net/url"
6-
)
3+
import "net/url"
74

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

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

6462
func (o *Object) keyWithValues(name string, flatValue bool) Value {
6563
if o.prefix != "" {
66-
return newAppendValue(o.values, fmt.Sprintf("%s.%s", o.prefix, name), flatValue)
64+
// This uses string concatenation in place of fmt.Sprintf as fmt.Sprintf has a much higher resource overhead
65+
return newAppendValue(o.values, o.prefix+keySeparator+name, flatValue)
6766
}
6867
return newAppendValue(o.values, name, flatValue)
6968
}

aws/protocol/query/value.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"github.com/aws/smithy-go/encoding/httpbinding"
88
)
99

10+
const keySeparator = "."
11+
1012
// Value represents a Query Value type.
1113
type Value struct {
1214
// The query values to add the value to.

aws/protocol/query/value_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package query
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
"testing"
7+
)
8+
9+
var output string
10+
11+
func Benchmark_sprintf_strings(b *testing.B) {
12+
for i := 0; i < b.N; i++ {
13+
output = fmt.Sprintf("%s.%s", "foo", "bar")
14+
}
15+
}
16+
17+
func Benchmark_concat_strings(b *testing.B) {
18+
for i := 0; i < b.N; i++ {
19+
output = "foo" + keySeparator + "bar"
20+
}
21+
}
22+
23+
func Benchmark_int_formatting(b *testing.B) {
24+
benchmarkFuncs := []struct {
25+
name string
26+
formatter func(val int32)
27+
}{
28+
{
29+
name: "array - sprintf", formatter: func(val int32) {
30+
output = fmt.Sprintf("%s.%d", "foo", val)
31+
},
32+
},
33+
{
34+
name: "array - concat strconv", formatter: func(val int32) {
35+
output = "foo" + keySeparator + strconv.FormatInt(int64(val), 10)
36+
},
37+
},
38+
{
39+
name: "map - sprintf", formatter: func(val int32) {
40+
output = fmt.Sprintf("%s.%d.%s", "foo", val, "bar")
41+
output = fmt.Sprintf("%s.%d.%s", "foo", val, "bar")
42+
},
43+
},
44+
{
45+
name: "map - concat strconv", formatter: func(val int32) {
46+
valString := strconv.FormatInt(int64(val), 10)
47+
output = "foo" + keySeparator + valString + keySeparator + "bar"
48+
output = "foo" + keySeparator + valString + keySeparator + "bar"
49+
},
50+
},
51+
}
52+
53+
sizesToTest := []int32{1, 10, 100, 250, 500, 1000}
54+
55+
for _, bm := range benchmarkFuncs {
56+
for _, size := range sizesToTest {
57+
b.Run(fmt.Sprintf("%s with %d size", bm.name, size), func(b *testing.B) {
58+
for i := 0; i < b.N; i++ {
59+
bm.formatter(size)
60+
}
61+
})
62+
}
63+
}
64+
}

0 commit comments

Comments
 (0)