Skip to content

Commit 971ae75

Browse files
authored
Improve serialization speed of enums without data in C# (#2762)
1 parent 2f1d99c commit 971ae75

8 files changed

Lines changed: 153 additions & 27 deletions

File tree

crates/bindings-csharp/BSATN.Codegen/Type.cs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,11 @@ public static TypeUse Parse(ISymbol member, ITypeSymbol typeSymbol, DiagReporter
7373
Parse(member, named.TypeArguments[0], diag)
7474
),
7575
_ => named.IsValueType
76-
? new ValueUse(type, typeInfo)
76+
? (
77+
named.TypeKind == Microsoft.CodeAnalysis.TypeKind.Enum
78+
? new EnumUse(type, typeInfo)
79+
: new ValueUse(type, typeInfo)
80+
)
7781
: new ReferenceUse(type, typeInfo),
7882
},
7983
_ => throw new InvalidOperationException($"Unsupported type {type}"),
@@ -113,7 +117,32 @@ public abstract string EqualsStatement(
113117
}
114118

115119
/// <summary>
116-
/// A use of a value type.
120+
/// A use of an enum type.
121+
/// (This is a C# enum, not one of our tagged enums.)
122+
/// </summary>
123+
/// <param name="Type"></param>
124+
/// <param name="TypeInfo"></param>
125+
public record EnumUse(string Type, string TypeInfo) : TypeUse(Type, TypeInfo)
126+
{
127+
// We just use `==` here, rather than `.Equals`, because
128+
// C# enums don't provide a `bool Equals(Self other)`, and
129+
// using `.Equals(object other)` allocates, which we want to avoid.
130+
//
131+
// We could instead generate custom .Equals for enums -- except that requires
132+
// partial enums, and I'm not sure such things exist.
133+
public override string EqualsStatement(
134+
string inVar1,
135+
string inVar2,
136+
string outVar,
137+
int level = 0
138+
) => $"var {outVar} = {inVar1} == {inVar2};";
139+
140+
public override string GetHashCodeStatement(string inVar, string outVar, int level = 0) =>
141+
$"var {outVar} = {inVar}.GetHashCode();";
142+
}
143+
144+
/// <summary>
145+
/// A use of a value type (that is not an enum).
117146
/// </summary>
118147
/// <param name="Type"></param>
119148
/// <param name="TypeInfo"></param>

crates/bindings-csharp/BSATN.Runtime.Tests/Tests.cs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,51 @@ public override int GetHashCode([DisallowNull] IEnumerable<T> obj)
656656
}
657657
}
658658

659+
[Type]
660+
enum Banana
661+
{
662+
Cavendish,
663+
LadyFinger,
664+
RedBanana,
665+
Manzano,
666+
BlueJava,
667+
GreenPlantain,
668+
YellowPlantain,
669+
PisangRaja,
670+
}
671+
672+
[Fact]
673+
public static void EnumSerializationWorks()
674+
{
675+
var serializer = new Enum<Banana>();
676+
var bananas = new Banana[]
677+
{
678+
Banana.Cavendish,
679+
Banana.LadyFinger,
680+
Banana.RedBanana,
681+
Banana.Manzano,
682+
Banana.BlueJava,
683+
Banana.GreenPlantain,
684+
Banana.YellowPlantain,
685+
Banana.PisangRaja,
686+
};
687+
for (var i = 0; i < bananas.Length; i++)
688+
{
689+
var stream = new MemoryStream();
690+
var writer = new BinaryWriter(stream);
691+
var banana = bananas[i];
692+
serializer.Write(writer, banana);
693+
694+
stream.Seek(0, SeekOrigin.Begin);
695+
var tag = new BinaryReader(stream).ReadByte();
696+
Assert.Equal(tag, i);
697+
698+
stream.Seek(0, SeekOrigin.Begin);
699+
var newBanana = serializer.Read(new BinaryReader(stream));
700+
Assert.Equal(banana, newBanana);
701+
}
702+
}
703+
659704
[Fact]
660705
public static void GeneratedNestedListEqualsWorks()
661706
{
@@ -775,4 +820,45 @@ [new BasicEnum.X(1), null],
775820
);
776821
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
777822
}
823+
824+
[Type]
825+
partial struct ContainsEnum
826+
{
827+
public Banana TheBanana;
828+
public int BananaCount;
829+
}
830+
831+
static readonly Gen<(Banana, int)> GenContainsEnum = Gen.Select(
832+
Gen.Enum<Banana>(),
833+
Gen.Int[0, 3]
834+
);
835+
static readonly Gen<((Banana, int), (Banana, int))> GenTwoContainsEnum = Gen.Select(
836+
GenContainsEnum,
837+
GenContainsEnum
838+
);
839+
840+
[Fact]
841+
public static void GeneratedEnumEqualsWorks()
842+
{
843+
GenTwoContainsEnum.Sample(
844+
example =>
845+
{
846+
var ((b1, c1), (b2, c2)) = example;
847+
var struct1 = new ContainsEnum { TheBanana = b1, BananaCount = c1 };
848+
var struct2 = new ContainsEnum { TheBanana = b2, BananaCount = c2 };
849+
850+
if ((b1, c1) == (b2, c2))
851+
{
852+
Assert.True(struct1.Equals(struct2));
853+
Assert.Equal(struct1, struct2);
854+
}
855+
else
856+
{
857+
Assert.False(struct1.Equals(struct2));
858+
Assert.NotEqual(struct1, struct2);
859+
}
860+
},
861+
iter: 10_000
862+
);
863+
}
778864
}

crates/bindings-csharp/BSATN.Runtime/BSATN/Runtime.cs

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -118,24 +118,37 @@ public interface IReadWrite<T>
118118
}
119119

120120
/// <summary>
121-
/// Present for backwards-compatibility reasons, but no longer used.
122-
/// The auto-generated serialization code for enum now reads/writes
123-
/// the tag byte directly to the wire. This avoids calling into reflective code.
121+
/// Serializer for enums.
124122
/// </summary>
125123
/// <typeparam name="T"></typeparam>
126124
public readonly struct Enum<T> : IReadWrite<T>
127125
where T : struct, Enum
128126
{
129-
private static readonly ulong NumVariants;
127+
/// <summary>
128+
/// Map from tag -> value, implemented as an array.
129+
/// Note: the [Type] macro rejects enums with explicitly set values (see Codegen.Tests),
130+
/// so this array is guaranteed to be continuous and indexed starting from 0.
131+
/// </summary>
132+
private static readonly T[] TagToValue = Enum.GetValues(typeof(T)).Cast<T>().ToArray();
130133

131-
static Enum()
134+
public T Read(BinaryReader reader)
132135
{
133-
NumVariants = (ulong)Enum.GetValues(typeof(T)).Length;
136+
var tag = reader.ReadByte();
137+
try
138+
{
139+
return TagToValue[tag];
140+
}
141+
catch
142+
{
143+
throw new ArgumentOutOfRangeException(
144+
$"Tag {tag} is out of range of enum {typeof(T).Name}"
145+
);
146+
}
134147
}
135148

136-
private static T Validate(T value)
149+
public void Write(BinaryWriter writer, T value)
137150
{
138-
// Previously this was: `if (!Enum.IsDefined(typeof(T), value))`.
151+
// Previously this was: `if (Enum.IsDefined(typeof(T), value))`.
139152
// This was quite expensive because:
140153
// 1. It uses reflection
141154
// 2. It allocates
@@ -144,24 +157,22 @@ private static T Validate(T value)
144157
// However, enum values are guaranteed to be sequential and zero based.
145158
// Hence we only ever need to do an upper bound check.
146159
// See `SpacetimeDB.Type.ParseEnum` for the syntax analysis.
147-
148-
// Later note: this STILL uses reflection. We've deprecated this class entirely
149-
// because of this.
150-
if (Convert.ToUInt64(value) >= NumVariants)
160+
//
161+
// Note: this actually still uses reflection and allocates.
162+
// It's hard to figure out how to avoid this without custom-generating a writer for each enum type.
163+
var tag = Convert.ToByte(value);
164+
if (tag < TagToValue.Length)
165+
{
166+
writer.Write(tag);
167+
}
168+
else
151169
{
152170
throw new ArgumentOutOfRangeException(
153-
nameof(value),
154-
$"Value {value} is out of range of enum {typeof(T).Name}"
171+
$"Value {value} is out of range for enum {typeof(T).Name}"
155172
);
156173
}
157-
return value;
158174
}
159175

160-
public T Read(BinaryReader reader) => Validate((T)Enum.ToObject(typeof(T), reader.ReadByte()));
161-
162-
public void Write(BinaryWriter writer, T value) =>
163-
writer.Write(Convert.ToByte(Validate(value)));
164-
165176
public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
166177
registrar.RegisterType<T>(
167178
(_) =>

crates/bindings-csharp/Codegen.Tests/fixtures/client/snapshots/Type#PublicTable.verified.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ public bool Equals(PublicTable that)
255255
var ___eqConnectionIdField = this.ConnectionIdField.Equals(that.ConnectionIdField);
256256
var ___eqCustomStructField = this.CustomStructField.Equals(that.CustomStructField);
257257
var ___eqCustomClassField = this.CustomClassField.Equals(that.CustomClassField);
258-
var ___eqCustomEnumField = this.CustomEnumField.Equals(that.CustomEnumField);
258+
var ___eqCustomEnumField = this.CustomEnumField == that.CustomEnumField;
259259
var ___eqCustomTaggedEnumField =
260260
this.CustomTaggedEnumField == null
261261
? that.CustomTaggedEnumField == null

crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#TestUniqueNotEquatable.verified.cs

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Type#TestUnsupportedType.verified.cs

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#PublicTable.verified.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ public bool Equals(PublicTable that)
279279
this.CustomClassField == null
280280
? that.CustomClassField == null
281281
: this.CustomClassField.Equals(that.CustomClassField);
282-
var ___eqCustomEnumField = this.CustomEnumField.Equals(that.CustomEnumField);
282+
var ___eqCustomEnumField = this.CustomEnumField == that.CustomEnumField;
283283
var ___eqCustomTaggedEnumField =
284284
this.CustomTaggedEnumField == null
285285
? that.CustomTaggedEnumField == null

crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Type#CustomNestedClass.verified.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ public bool Equals(CustomNestedClass? that)
152152
this.NestedNullableClass == null
153153
? that.NestedNullableClass == null
154154
: this.NestedNullableClass.Equals(that.NestedNullableClass);
155-
var ___eqNestedEnum = this.NestedEnum.Equals(that.NestedEnum);
155+
var ___eqNestedEnum = this.NestedEnum == that.NestedEnum;
156156
var ___eqNestedNullableEnum = this.NestedNullableEnum.Equals(that.NestedNullableEnum);
157157
var ___eqNestedTaggedEnum =
158158
this.NestedTaggedEnum == null

0 commit comments

Comments
 (0)