util/multierr: optimize New for nil cases (#6750)

Consider the following pattern:

	err1 := foo()
	err2 := bar()
	err3 := baz()
	return multierr.New(err1, err2, err3)

If err1, err2, and err3 are all nil, then multierr.New should not allocate.
Thus, modify the logic of New to count the number of distinct error values
and allocate the exactly needed slice. This also speeds up non-empty error
situation since repeatedly growing with append is slow.

Performance:

	name         old time/op    new time/op    delta
	Empty-24       41.8ns ± 2%     6.4ns ± 1%   -84.73%  (p=0.000 n=10+10)
	NonEmpty-24     120ns ± 3%      69ns ± 1%   -42.01%  (p=0.000 n=9+10)

	name         old alloc/op   new alloc/op   delta
	Empty-24        64.0B ± 0%      0.0B       -100.00%  (p=0.000 n=10+10)
	NonEmpty-24      168B ± 0%       88B ± 0%   -47.62%  (p=0.000 n=10+10)

	name         old allocs/op  new allocs/op  delta
	Empty-24         1.00 ± 0%      0.00       -100.00%  (p=0.000 n=10+10)
	NonEmpty-24      3.00 ± 0%      2.00 ± 0%   -33.33%  (p=0.000 n=10+10)

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
This commit is contained in:
Joe Tsai 2022-12-14 18:17:38 -08:00 committed by GitHub
parent 55b24009f7
commit 350aab05e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 43 additions and 13 deletions

View File

@ -53,7 +53,31 @@ func (e Error) Unwrap() []error {
// If the resulting slice has length 1, New returns that error.
// If the resulting slice has length > 1, New returns that slice as an Error.
func New(errs ...error) error {
dst := make([]error, 0, len(errs))
// First count the number of errors to avoid allocating.
var n int
var errFirst error
for _, e := range errs {
switch e := e.(type) {
case nil:
continue
case Error:
n += len(e.errs)
if errFirst == nil && len(e.errs) > 0 {
errFirst = e.errs[0]
}
default:
n++
if errFirst == nil {
errFirst = e
}
}
}
if n <= 1 {
return errFirst // nil if n == 0
}
// More than one error, allocate slice and construct the multi-error.
dst := make([]error, 0, n)
for _, e := range errs {
switch e := e.(type) {
case nil:
@ -64,18 +88,6 @@ func New(errs ...error) error {
dst = append(dst, e)
}
}
// dst has been filtered and splatted.
switch len(dst) {
case 0:
return nil
case 1:
return dst[0]
}
// Zero any remaining elements of dst left over from errs, for GC.
tail := dst[len(dst):cap(dst)]
for i := range tail {
tail[i] = nil
}
return Error{errs: dst}
}

View File

@ -7,6 +7,7 @@ package multierr_test
import (
"errors"
"fmt"
"io"
"testing"
qt "github.com/frankban/quicktest"
@ -106,3 +107,20 @@ func TestRange(t *testing.T) {
return x.Error() == y.Error()
})), want)
}
var sink error
func BenchmarkEmpty(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
sink = multierr.New(nil, nil, nil, multierr.Error{})
}
}
func BenchmarkNonEmpty(b *testing.B) {
merr := multierr.New(io.ErrShortBuffer, io.ErrNoProgress)
b.ReportAllocs()
for i := 0; i < b.N; i++ {
sink = multierr.New(io.ErrUnexpectedEOF, merr, io.ErrClosedPipe)
}
}