diff --git a/benchmarks_test.go b/benchmarks_test.go index 797f5a0..562d3bc 100644 --- a/benchmarks_test.go +++ b/benchmarks_test.go @@ -60,3 +60,68 @@ func BenchmarkAppend(b *testing.B) { } } } + +func BenchmarkCombine(b *testing.B) { + b.Run("inline 1", func(b *testing.B) { + var x error + for i := 0; i < b.N; i++ { + Combine(x) + } + }) + + b.Run("inline 2", func(b *testing.B) { + var x, y error + for i := 0; i < b.N; i++ { + Combine(x, y) + } + }) + + b.Run("inline 3 no error", func(b *testing.B) { + var x, y, z error + for i := 0; i < b.N; i++ { + Combine(x, y, z) + } + }) + + b.Run("inline 3 one error", func(b *testing.B) { + var x, y, z error + z = fmt.Errorf("failed") + for i := 0; i < b.N; i++ { + Combine(x, y, z) + } + }) + + b.Run("inline 3 multiple errors", func(b *testing.B) { + var x, y, z error + z = fmt.Errorf("failed3") + y = fmt.Errorf("failed2") + x = fmt.Errorf("failed") + for i := 0; i < b.N; i++ { + Combine(x, y, z) + } + }) + + b.Run("slice 100 no errors", func(b *testing.B) { + errs := make([]error, 100) + for i := 0; i < b.N; i++ { + Combine(errs...) + } + }) + + b.Run("slice 100 one error", func(b *testing.B) { + errs := make([]error, 100) + errs[len(errs)-1] = fmt.Errorf("failed") + for i := 0; i < b.N; i++ { + Combine(errs...) + } + }) + + b.Run("slice 100 multi error", func(b *testing.B) { + errs := make([]error, 100) + errs[0] = fmt.Errorf("failed1") + errs[len(errs)-1] = fmt.Errorf("failed2") + for i := 0; i < b.N; i++ { + Combine(errs...) + } + }) +} diff --git a/error.go b/error.go index 13a020a..f45af14 100644 --- a/error.go +++ b/error.go @@ -372,6 +372,14 @@ func inspect(errors []error) (res inspectResult) { // fromSlice converts the given list of errors into a single error. func fromSlice(errors []error) error { + // Don't pay to inspect small slices. + switch len(errors) { + case 0: + return nil + case 1: + return errors[0] + } + res := inspect(errors) switch res.Count { case 0: @@ -381,8 +389,13 @@ func fromSlice(errors []error) error { return errors[res.FirstErrorIdx] case len(errors): if !res.ContainsMultiError { - // already flat - return &multiError{errors: errors} + // Error list is flat. Make a copy of it + // Otherwise "errors" escapes to the heap + // unconditionally for all other cases. + // This lets us optimize for the "no errors" case. + out := make([]error, len(errors)) + copy(out, errors) + return &multiError{errors: out} } } diff --git a/error_test.go b/error_test.go index 3dd4884..c053167 100644 --- a/error_test.go +++ b/error_test.go @@ -97,6 +97,12 @@ func TestCombine(t *testing.T) { " - bar", wantSingleline: "foo; bar", }, + { + giveErrors: []error{nil, nil, errors.New("great sadness"), nil}, + wantError: errors.New("great sadness"), + wantMultiline: "great sadness", + wantSingleline: "great sadness", + }, { giveErrors: []error{ errors.New("foo"), @@ -273,6 +279,14 @@ func TestCombineDoesNotModifySlice(t *testing.T) { assert.Nil(t, errors[1], 3) } +func TestCombineGoodCaseNoAlloc(t *testing.T) { + errs := make([]error, 10) + allocs := testing.AllocsPerRun(100, func() { + Combine(errs...) + }) + assert.Equal(t, 0.0, allocs) +} + func TestAppend(t *testing.T) { tests := []struct { left error