Skip to content

Commit

Permalink
proto: short-circuit Equal when inputs are identical
Browse files Browse the repository at this point in the history
I added benchmarks (measured on Intel(R) Xeon(R) CPU E5-1650 v4 @ 3.60GHz) that show the difference:

name                                  old time/op  new time/op  delta
EqualWithSmallEmpty-12                 241ns ± 6%   242ns ± 6%     ~     (p=0.796 n=10+10)
EqualWithIdenticalPtrEmpty-12          241ns ± 3%     7ns ± 4%  -97.19%  (p=0.000 n=10+10)
EqualWithLargeEmpty-12                2.68µs ± 3%  2.59µs ± 3%   -3.27%  (p=0.000 n=10+10)
EqualWithDeeplyNestedEqual-12         73.9µs ± 3%  71.8µs ± 1%   -2.91%   (p=0.000 n=10+9)
EqualWithDeeplyNestedDifferent-12     20.0µs ± 5%  19.4µs ± 5%   -3.06%  (p=0.029 n=10+10)
EqualWithDeeplyNestedIdenticalPtr-12  73.9µs ± 4%   0.0µs ± 2%  -99.99%  (p=0.000 n=10+10)

Change-Id: I1b83fa477d6432eafd355b322f507cf90b9a6751
Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/411377
Reviewed-by: Lasse Folger <[email protected]>
Reviewed-by: Joseph Tsai <[email protected]>
Reviewed-by: Michael Stapelberg <[email protected]>
  • Loading branch information
dimitar-asenov authored and stapelberg committed Jun 15, 2022
1 parent 784c482 commit 380c339
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 0 deletions.
4 changes: 4 additions & 0 deletions proto/equal.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ func Equal(x, y Message) bool {
if x == nil || y == nil {
return x == nil && y == nil
}
if reflect.TypeOf(x).Kind() == reflect.Pointer && x == y {
// Avoid an expensive comparison if both inputs are identical pointers.
return true
}
mx := x.ProtoReflect()
my := y.ProtoReflect()
if mx.IsValid() != my.IsValid() {
Expand Down
105 changes: 105 additions & 0 deletions proto/equal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"testing"

"google.golang.org/protobuf/encoding/prototext"
"google.golang.org/protobuf/internal/pragma"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/testing/protopack"

Expand All @@ -17,6 +18,13 @@ import (
)

func TestEqual(t *testing.T) {
identicalPtrPb := &testpb.TestAllTypes{MapStringString: map[string]string{"a": "b", "c": "d"}}

type incomparableMessage struct {
*testpb.TestAllTypes
pragma.DoNotCompare
}

tests := []struct {
x, y proto.Message
eq bool
Expand Down Expand Up @@ -55,6 +63,34 @@ func TestEqual(t *testing.T) {
eq: false,
},

// Identical input pointers
{
x: identicalPtrPb,
y: identicalPtrPb,
eq: true,
},

// Incomparable types. The top-level types are not actually directly
// compared (which would panic), but rather the comparison happens on the
// objects returned by ProtoReflect(). These tests are here just to ensure
// that any short-circuit checks do not accidentally try to compare
// incomparable top-level types.
{
x: incomparableMessage{TestAllTypes: identicalPtrPb},
y: incomparableMessage{TestAllTypes: identicalPtrPb},
eq: true,
},
{
x: identicalPtrPb,
y: incomparableMessage{TestAllTypes: identicalPtrPb},
eq: true,
},
{
x: identicalPtrPb,
y: &incomparableMessage{TestAllTypes: identicalPtrPb},
eq: true,
},

// Proto2 scalars.
{
x: &testpb.TestAllTypes{OptionalInt32: proto.Int32(1)},
Expand Down Expand Up @@ -562,3 +598,72 @@ func TestEqual(t *testing.T) {
}
}
}

func BenchmarkEqualWithSmallEmpty(b *testing.B) {
x := &testpb.ForeignMessage{}
y := &testpb.ForeignMessage{}

b.ResetTimer()
for i := 0; i < b.N; i++ {
proto.Equal(x, y)
}
}

func BenchmarkEqualWithIdenticalPtrEmpty(b *testing.B) {
x := &testpb.ForeignMessage{}

b.ResetTimer()
for i := 0; i < b.N; i++ {
proto.Equal(x, x)
}
}

func BenchmarkEqualWithLargeEmpty(b *testing.B) {
x := &testpb.TestAllTypes{}
y := &testpb.TestAllTypes{}

b.ResetTimer()
for i := 0; i < b.N; i++ {
proto.Equal(x, y)
}
}

func makeNested(depth int) *testpb.TestAllTypes {
if depth <= 0 {
return nil
}
return testpb.TestAllTypes_builder{
OptionalNestedMessage: testpb.TestAllTypes_NestedMessage_builder{
Corecursive: makeNested(depth - 1),
}.Build(),
}.Build()
}

func BenchmarkEqualWithDeeplyNestedEqual(b *testing.B) {
x := makeNested(20)
y := makeNested(20)

b.ResetTimer()
for i := 0; i < b.N; i++ {
proto.Equal(x, y)
}
}

func BenchmarkEqualWithDeeplyNestedDifferent(b *testing.B) {
x := makeNested(20)
y := makeNested(21)

b.ResetTimer()
for i := 0; i < b.N; i++ {
proto.Equal(x, y)
}
}

func BenchmarkEqualWithDeeplyNestedIdenticalPtr(b *testing.B) {
x := makeNested(20)

b.ResetTimer()
for i := 0; i < b.N; i++ {
proto.Equal(x, x)
}
}

0 comments on commit 380c339

Please sign in to comment.