Skip to content

Commit cea7d2e

Browse files
jstroemabhinav
andauthored
Combine: Optimize for all nil (#55)
The current implementation of Combine returns the input slice in the returned multierr. This causes it to escape to the heap unconditionally. Optimizing for the no-errors case, we can copy the slice in that case and keep it on the stack. ``` name old time/op new time/op delta Combine/inline_1-8 17.7ns ± 0% 2.1ns ± 0% -88.20% (p=0.008 n=5+5) Combine/inline_2-8 21.0ns ± 0% 4.4ns ± 1% -79.03% (p=0.008 n=5+5) Combine/inline_3_no_error-8 24.4ns ± 0% 5.1ns ± 1% -79.22% (p=0.016 n=4+5) Combine/inline_3_one_error-8 24.8ns ± 0% 5.4ns ± 1% -78.42% (p=0.008 n=5+5) Combine/inline_3_multiple_errors-8 44.3ns ± 0% 54.9ns ± 0% +23.80% (p=0.008 n=5+5) Combine/slice_100_no_errors-8 72.9ns ± 0% 73.4ns ± 1% +0.68% (p=0.008 n=5+5) Combine/slice_100_one_error-8 74.5ns ± 0% 74.8ns ± 0% ~ (p=0.056 n=5+5) Combine/slice_100_multi_error-8 193ns ± 0% 193ns ± 0% ~ (p=0.127 n=5+5) name old alloc/op new alloc/op delta Combine/inline_1-8 16.0B ± 0% 0.0B -100.00% (p=0.008 n=5+5) Combine/inline_2-8 32.0B ± 0% 0.0B -100.00% (p=0.008 n=5+5) Combine/inline_3_no_error-8 48.0B ± 0% 0.0B -100.00% (p=0.008 n=5+5) Combine/inline_3_one_error-8 48.0B ± 0% 0.0B -100.00% (p=0.008 n=5+5) Combine/inline_3_multiple_errors-8 80.0B ± 0% 80.0B ± 0% ~ (all equal) Combine/slice_100_no_errors-8 0.00B 0.00B ~ (all equal) Combine/slice_100_one_error-8 0.00B 0.00B ~ (all equal) Combine/slice_100_multi_error-8 64.0B ± 0% 64.0B ± 0% ~ (all equal) name old allocs/op new allocs/op delta Combine/inline_1-8 1.00 ± 0% 0.00 -100.00% (p=0.008 n=5+5) Combine/inline_2-8 1.00 ± 0% 0.00 -100.00% (p=0.008 n=5+5) Combine/inline_3_no_error-8 1.00 ± 0% 0.00 -100.00% (p=0.008 n=5+5) Combine/inline_3_one_error-8 1.00 ± 0% 0.00 -100.00% (p=0.008 n=5+5) Combine/inline_3_multiple_errors-8 2.00 ± 0% 2.00 ± 0% ~ (all equal) Combine/slice_100_no_errors-8 0.00 0.00 ~ (all equal) Combine/slice_100_one_error-8 0.00 0.00 ~ (all equal) Combine/slice_100_multi_error-8 2.00 ± 0% 2.00 ± 0% ~ (all equal) ``` Co-authored-by: Abhinav Gupta <abg@uber.com>
1 parent d49c2ba commit cea7d2e

File tree

3 files changed

+94
-2
lines changed

3 files changed

+94
-2
lines changed

benchmarks_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,68 @@ func BenchmarkAppend(b *testing.B) {
6060
}
6161
}
6262
}
63+
64+
func BenchmarkCombine(b *testing.B) {
65+
b.Run("inline 1", func(b *testing.B) {
66+
var x error
67+
for i := 0; i < b.N; i++ {
68+
Combine(x)
69+
}
70+
})
71+
72+
b.Run("inline 2", func(b *testing.B) {
73+
var x, y error
74+
for i := 0; i < b.N; i++ {
75+
Combine(x, y)
76+
}
77+
})
78+
79+
b.Run("inline 3 no error", func(b *testing.B) {
80+
var x, y, z error
81+
for i := 0; i < b.N; i++ {
82+
Combine(x, y, z)
83+
}
84+
})
85+
86+
b.Run("inline 3 one error", func(b *testing.B) {
87+
var x, y, z error
88+
z = fmt.Errorf("failed")
89+
for i := 0; i < b.N; i++ {
90+
Combine(x, y, z)
91+
}
92+
})
93+
94+
b.Run("inline 3 multiple errors", func(b *testing.B) {
95+
var x, y, z error
96+
z = fmt.Errorf("failed3")
97+
y = fmt.Errorf("failed2")
98+
x = fmt.Errorf("failed")
99+
for i := 0; i < b.N; i++ {
100+
Combine(x, y, z)
101+
}
102+
})
103+
104+
b.Run("slice 100 no errors", func(b *testing.B) {
105+
errs := make([]error, 100)
106+
for i := 0; i < b.N; i++ {
107+
Combine(errs...)
108+
}
109+
})
110+
111+
b.Run("slice 100 one error", func(b *testing.B) {
112+
errs := make([]error, 100)
113+
errs[len(errs)-1] = fmt.Errorf("failed")
114+
for i := 0; i < b.N; i++ {
115+
Combine(errs...)
116+
}
117+
})
118+
119+
b.Run("slice 100 multi error", func(b *testing.B) {
120+
errs := make([]error, 100)
121+
errs[0] = fmt.Errorf("failed1")
122+
errs[len(errs)-1] = fmt.Errorf("failed2")
123+
for i := 0; i < b.N; i++ {
124+
Combine(errs...)
125+
}
126+
})
127+
}

error.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,14 @@ func inspect(errors []error) (res inspectResult) {
372372

373373
// fromSlice converts the given list of errors into a single error.
374374
func fromSlice(errors []error) error {
375+
// Don't pay to inspect small slices.
376+
switch len(errors) {
377+
case 0:
378+
return nil
379+
case 1:
380+
return errors[0]
381+
}
382+
375383
res := inspect(errors)
376384
switch res.Count {
377385
case 0:
@@ -381,8 +389,13 @@ func fromSlice(errors []error) error {
381389
return errors[res.FirstErrorIdx]
382390
case len(errors):
383391
if !res.ContainsMultiError {
384-
// already flat
385-
return &multiError{errors: errors}
392+
// Error list is flat. Make a copy of it
393+
// Otherwise "errors" escapes to the heap
394+
// unconditionally for all other cases.
395+
// This lets us optimize for the "no errors" case.
396+
out := make([]error, len(errors))
397+
copy(out, errors)
398+
return &multiError{errors: out}
386399
}
387400
}
388401

error_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ func TestCombine(t *testing.T) {
9797
" - bar",
9898
wantSingleline: "foo; bar",
9999
},
100+
{
101+
giveErrors: []error{nil, nil, errors.New("great sadness"), nil},
102+
wantError: errors.New("great sadness"),
103+
wantMultiline: "great sadness",
104+
wantSingleline: "great sadness",
105+
},
100106
{
101107
giveErrors: []error{
102108
errors.New("foo"),
@@ -273,6 +279,14 @@ func TestCombineDoesNotModifySlice(t *testing.T) {
273279
assert.Nil(t, errors[1], 3)
274280
}
275281

282+
func TestCombineGoodCaseNoAlloc(t *testing.T) {
283+
errs := make([]error, 10)
284+
allocs := testing.AllocsPerRun(100, func() {
285+
Combine(errs...)
286+
})
287+
assert.Equal(t, 0.0, allocs)
288+
}
289+
276290
func TestAppend(t *testing.T) {
277291
tests := []struct {
278292
left error

0 commit comments

Comments
 (0)