reflect: optimize Value.IsZero

If a struct or array is comparable, then we can leverage rtype.equal,
which is almost always faster than what Go reflection can achieve.

As a secondary optimization, pre-compute Value.Len and Value.NumField
outside of the loop conditional.

Performance:

	name                       old time/op  new time/op  delta
	IsZero/ArrayComparable      136ns ± 4%    16ns ± 1%  -88.28%  (p=0.008 n=5+5)
	IsZero/ArrayIncomparable    197ns ±10%   123ns ± 1%  -37.74%  (p=0.008 n=5+5)
	IsZero/StructComparable    26.4ns ± 0%   9.6ns ± 1%  -63.68%  (p=0.016 n=4+5)
	IsZero/StructIncomparable  43.5ns ± 1%  27.8ns ± 1%  -36.21%  (p=0.008 n=5+5)

The incomparable types gain a performance boost since
they are generally constructed from nested comparable types.

Change-Id: If2c1929f8bb1b5b19306ef0c69f3c95a27d4b60d
Reviewed-on: https://go-review.googlesource.com/c/go/+/411478
Reviewed-by: Dan Kortschak <dan@kortschak.io>
Run-TryBot: Joseph Tsai <joetsai@digital-static.net>
Reviewed-by: Than McIntosh <thanm@google.com>
Reviewed-by: Keith Randall <khr@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
This commit is contained in:
Joe Tsai 2022-06-09 18:32:53 -07:00 committed by Joseph Tsai
parent 1a8dfadbfe
commit 1ab6b790be
2 changed files with 55 additions and 7 deletions

View File

@ -47,6 +47,8 @@ type T struct {
d *int
}
var _ = T{} == T{} // tests depend on T being comparable
type pair struct {
i any
s string
@ -1364,9 +1366,14 @@ func TestIsZero(t *testing.T) {
{uintptr(128), false},
// Array
{Zero(TypeOf([5]string{})).Interface(), true},
{[5]string{"", "", "", "", ""}, true},
{[5]string{}, true},
{[5]string{"", "", "", "a", ""}, false},
{[5]string{}, true}, // comparable array
{[5]string{"", "", "", "a", ""}, false}, // comparable array
{[1]*int{}, true}, // direct pointer array
{[1]*int{new(int)}, false}, // direct pointer array
{[3][]int{}, true}, // incomparable array
{[3][]int{{1}}, false}, // incomparable array
{[1 << 12]byte{}, true},
{[1 << 12]byte{1}, false},
// Chan
{(chan string)(nil), true},
{make(chan string), false},
@ -1393,8 +1400,12 @@ func TestIsZero(t *testing.T) {
{"", true},
{"not-zero", false},
// Structs
{T{}, true},
{T{123, 456.75, "hello", &_i}, false},
{T{}, true}, // comparable struct
{T{123, 456.75, "hello", &_i}, false}, // comparable struct
{struct{ p *int }{}, true}, // direct pointer struct
{struct{ p *int }{new(int)}, false}, // direct pointer struct
{struct{ s []int }{}, true}, // incomparable struct
{struct{ s []int }{[]int{1}}, false}, // incomparable struct
// UnsafePointer
{(unsafe.Pointer)(nil), true},
{(unsafe.Pointer)(new(int)), false},
@ -1426,6 +1437,25 @@ func TestIsZero(t *testing.T) {
}()
}
func BenchmarkIsZero(b *testing.B) {
source := ValueOf(struct {
ArrayComparable [4]T
ArrayIncomparable [4]_Complex
StructComparable T
StructIncomparable _Complex
}{})
for i := 0; i < source.NumField(); i++ {
name := source.Type().Field(i).Name
value := source.Field(i)
b.Run(name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
sink = value.IsZero()
}
})
}
}
func TestInterfaceExtraction(t *testing.T) {
var s struct {
W io.Writer

View File

@ -1579,7 +1579,16 @@ func (v Value) IsZero() bool {
c := v.Complex()
return math.Float64bits(real(c)) == 0 && math.Float64bits(imag(c)) == 0
case Array:
for i := 0; i < v.Len(); i++ {
// If the type is comparable, then compare directly with zero.
if v.typ.equal != nil && v.typ.size <= maxZero {
if v.flag&flagIndir == 0 {
return v.ptr == nil
}
return v.typ.equal(v.ptr, unsafe.Pointer(&zeroVal[0]))
}
n := v.Len()
for i := 0; i < n; i++ {
if !v.Index(i).IsZero() {
return false
}
@ -1590,7 +1599,16 @@ func (v Value) IsZero() bool {
case String:
return v.Len() == 0
case Struct:
for i := 0; i < v.NumField(); i++ {
// If the type is comparable, then compare directly with zero.
if v.typ.equal != nil && v.typ.size <= maxZero {
if v.flag&flagIndir == 0 {
return v.ptr == nil
}
return v.typ.equal(v.ptr, unsafe.Pointer(&zeroVal[0]))
}
n := v.NumField()
for i := 0; i < n; i++ {
if !v.Field(i).IsZero() {
return false
}