internal/impl: add MessageState to every generated message
We define MessageState, which is essentially an atomically set *MessageInfo.
By nesting this as the first field in every generated message, we can
implement the reflective methods on a *MessageState when obtained by
unsafe casting a concrete message pointer as a *MessageState.
The MessageInfo held by MessageState provides additional Go type information
to interpret the memory that comes after the contents of the MessageState.
Since we are nesting a MessageState in every message,
the memory use of every message instance grows by 8B.
On average, the body of ProtoReflect grows from 133B to 202B (+50%).
However, this is offset by XXX_Methods, which is 108B and
will be removed in a future CL. Taking into account the eventual removal
of XXX_Methods, this is a net reduction of 25%.
name old time/op new time/op delta
Name/Value-4 70.3ns ± 2% 17.5ns ± 6% -75.08% (p=0.000 n=10+10)
Name/Nil-4 70.6ns ± 3% 33.4ns ± 2% -52.66% (p=0.000 n=10+10)
name old alloc/op new alloc/op delta
Name/Value-4 16.0B ± 0% 0.0B -100.00% (p=0.000 n=10+10)
Name/Nil-4 16.0B ± 0% 0.0B -100.00% (p=0.000 n=10+10)
name old allocs/op new allocs/op delta
Name/Value-4 1.00 ± 0% 0.00 -100.00% (p=0.000 n=10+10)
Name/Nil-4 1.00 ± 0% 0.00 -100.00% (p=0.000 n=10+10)
Change-Id: I92bd58dc681c57c92612fd5ba7fc066aea34e95a
Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/185460
Reviewed-by: Damien Neil <dneil@google.com>
diff --git a/internal/impl/message_test.go b/internal/impl/message_test.go
index af7196d..f2fd290 100644
--- a/internal/impl/message_test.go
+++ b/internal/impl/message_test.go
@@ -8,7 +8,9 @@
"fmt"
"math"
"reflect"
+ "runtime"
"strings"
+ "sync"
"testing"
cmp "github.com/google/go-cmp/cmp"
@@ -23,6 +25,7 @@
"google.golang.org/protobuf/reflect/prototype"
proto2_20180125 "google.golang.org/protobuf/internal/testprotos/legacy/proto2.v1.0.0-20180125-92554152"
+ testpb "google.golang.org/protobuf/internal/testprotos/test"
"google.golang.org/protobuf/types/descriptorpb"
)
@@ -1435,3 +1438,71 @@
}
return strings.Join(ss, ".")
}
+
+// The MessageState implementation makes the assumption that when a
+// concrete message is unsafe casted as a *MessageState, the Go GC does
+// not reclaim the memory for the remainder of the concrete message.
+func TestUnsafeAssumptions(t *testing.T) {
+ if !pimpl.UnsafeEnabled {
+ t.Skip()
+ }
+
+ var wg sync.WaitGroup
+ for i := 0; i < 10; i++ {
+ wg.Add(1)
+ go func() {
+ var ms [10]pref.Message
+
+ // Store the message only in its reflective form.
+ // Trigger the GC after each iteration.
+ for j := 0; j < 10; j++ {
+ ms[j] = (&testpb.TestAllTypes{
+ OptionalInt32: scalar.Int32(int32(j)),
+ OptionalFloat: scalar.Float32(float32(j)),
+ RepeatedInt32: []int32{int32(j)},
+ RepeatedFloat: []float32{float32(j)},
+ DefaultInt32: scalar.Int32(int32(j)),
+ DefaultFloat: scalar.Float32(float32(j)),
+ }).ProtoReflect()
+ runtime.GC()
+ }
+
+ // Convert the reflective form back into a concrete form.
+ // Verify that the values written previously are still the same.
+ for j := 0; j < 10; j++ {
+ switch m := ms[j].Interface().(*testpb.TestAllTypes); {
+ case m.GetOptionalInt32() != int32(j):
+ case m.GetOptionalFloat() != float32(j):
+ case m.GetRepeatedInt32()[0] != int32(j):
+ case m.GetRepeatedFloat()[0] != float32(j):
+ case m.GetDefaultInt32() != int32(j):
+ case m.GetDefaultFloat() != float32(j):
+ default:
+ continue
+ }
+ t.Error("memory corrupted detected")
+ }
+ defer wg.Done()
+ }()
+ }
+ wg.Wait()
+}
+
+func BenchmarkName(b *testing.B) {
+ var sink pref.FullName
+ b.Run("Value", func(b *testing.B) {
+ b.ReportAllocs()
+ m := new(descriptorpb.FileDescriptorProto)
+ for i := 0; i < b.N; i++ {
+ sink = m.ProtoReflect().Descriptor().FullName()
+ }
+ })
+ b.Run("Nil", func(b *testing.B) {
+ b.ReportAllocs()
+ m := (*descriptorpb.FileDescriptorProto)(nil)
+ for i := 0; i < b.N; i++ {
+ sink = m.ProtoReflect().Descriptor().FullName()
+ }
+ })
+ runtime.KeepAlive(sink)
+}