From 8b854655024c48d2d5d447c2973a3b8f09d82256 Mon Sep 17 00:00:00 2001 From: Matt Warren Date: Sat, 5 Sep 2026 11:20:12 -0700 Subject: [PATCH] Reorder fielsd to possibly save memory footprint --- scratch/Program.cs | 14 ++++++++------ scratch/Scratch.csproj | 2 +- .../SimplePrimitivesUnion_generated.cs | 3 ++- src/GeneratedTypes/TagLikeUnion_generated.cs | 3 ++- .../CustomUnionGeneratorTests.cs | 10 +++++----- src/Generators/CustomUnionGenerator.cs | 16 ++++++++++++---- 6 files changed, 30 insertions(+), 18 deletions(-) diff --git a/scratch/Program.cs b/scratch/Program.cs index f10ce73..21431f6 100644 --- a/scratch/Program.cs +++ b/scratch/Program.cs @@ -1,9 +1,11 @@ -var u = new MyUnion(new A(1, 2.0f)); +using System.Runtime.CompilerServices; -Console.WriteLine(u); +var u = new MyUnion(new A(1, 2)); -if (u is A) - Console.WriteLine("I'm an A"); +Console.WriteLine($"size: {Unsafe.SizeOf()}"); + +if (u is byte) + Console.WriteLine("I'm a byte"); if (u is IFoo) Console.WriteLine("I'm an IFoo"); @@ -20,12 +22,12 @@ partial void Cases( int x, IBar bar, IFoo foo, - A a + byte y ); } public interface IFoo {} public interface IBar {} -public record struct A(int X, float Y) : IFoo; +public record struct A(int X, byte Y) : IFoo; diff --git a/scratch/Scratch.csproj b/scratch/Scratch.csproj index bcd22a0..fe8f609 100644 --- a/scratch/Scratch.csproj +++ b/scratch/Scratch.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/GeneratedTypes/SimplePrimitivesUnion_generated.cs b/src/GeneratedTypes/SimplePrimitivesUnion_generated.cs index 7040ab5..3090c87 100644 --- a/src/GeneratedTypes/SimplePrimitivesUnion_generated.cs +++ b/src/GeneratedTypes/SimplePrimitivesUnion_generated.cs @@ -12,10 +12,11 @@ #pragma warning disable CS8618 [System.Runtime.CompilerServices.Union] +[StructLayout(LayoutKind.Sequential)] public partial struct SimplePrimitivesUnion : System.Runtime.CompilerServices.IUnion { - private readonly int _kind; private readonly Overlapped _overlapped; + private readonly byte _kind; [StructLayout(LayoutKind.Explicit)] private struct Overlapped diff --git a/src/GeneratedTypes/TagLikeUnion_generated.cs b/src/GeneratedTypes/TagLikeUnion_generated.cs index 00c6f21..20d7f03 100644 --- a/src/GeneratedTypes/TagLikeUnion_generated.cs +++ b/src/GeneratedTypes/TagLikeUnion_generated.cs @@ -12,10 +12,11 @@ #pragma warning disable CS8618 [System.Runtime.CompilerServices.Union] +[StructLayout(LayoutKind.Sequential)] public partial struct TagLikeUnion : System.Runtime.CompilerServices.IUnion { - private readonly int _kind; private readonly Overlapped _overlapped; + private readonly byte _kind; [StructLayout(LayoutKind.Explicit)] private struct Overlapped diff --git a/src/Generators.Tests/CustomUnionGeneratorTests.cs b/src/Generators.Tests/CustomUnionGeneratorTests.cs index 3448363..60efcf5 100644 --- a/src/Generators.Tests/CustomUnionGeneratorTests.cs +++ b/src/Generators.Tests/CustomUnionGeneratorTests.cs @@ -48,7 +48,7 @@ public void TestOverlappablePrimitiveCases() generatedText => { // prove that the two primitives got overlapped into the overlapped field - Assert.IsTrue(generatedText.Contains("int _kind")); + Assert.IsTrue(generatedText.Contains("_kind")); Assert.IsTrue(generatedText.Contains("_overlapped")); Assert.IsFalse(generatedText.Contains("_value1")); @@ -79,7 +79,7 @@ public record struct B(float Value); ), generatedText => { - Assert.IsTrue(generatedText.Contains("int _kind")); + Assert.IsTrue(generatedText.Contains("_kind")); Assert.IsTrue(generatedText.Contains("_overlapped")); Assert.IsFalse(generatedText.Contains("_value1")); Assert.IsTrue(generatedText.Contains("A Case1")); @@ -167,7 +167,7 @@ public record struct B(float Value, int Value2, string Value3); ), generatedText => { - Assert.IsTrue(generatedText.Contains("int _kind")); + Assert.IsTrue(generatedText.Contains("_kind")); Assert.IsTrue(generatedText.Contains("_overlapped")); Assert.IsTrue(generatedText.Contains("_value1")); Assert.IsTrue(generatedText.Contains("int Case1")); @@ -225,7 +225,7 @@ public record struct F(int Value, int Value2); ), generatedText => { - Assert.IsTrue(generatedText.Contains("int _kind")); + Assert.IsTrue(generatedText.Contains("_kind")); Assert.IsTrue(generatedText.Contains("_overlapped")); Assert.IsTrue(generatedText.Contains("object? _value1")); Assert.IsTrue(generatedText.Contains("object? _value2")); @@ -256,7 +256,7 @@ public record struct B(); ), generatedText => { - Assert.IsTrue(generatedText.Contains("int _kind")); + Assert.IsTrue(generatedText.Contains("_kind")); // there is no overlapped data so the field should not exist Assert.IsFalse(generatedText.Contains("_overlapped")); // there is no data other than the tag _kind, so no value fields should exist diff --git a/src/Generators/CustomUnionGenerator.cs b/src/Generators/CustomUnionGenerator.cs index d2d6056..4f6b3ee 100644 --- a/src/Generators/CustomUnionGenerator.cs +++ b/src/Generators/CustomUnionGenerator.cs @@ -68,6 +68,7 @@ private void WriteUnionType(UnionInfo union) void WriteUnion() { _writer.WriteLine("[System.Runtime.CompilerServices.Union]"); + _writer.WriteLine($"[StructLayout(LayoutKind.Sequential)]"); _writer.WriteLine($"{union.Accessibility} partial struct {union.DeclarationName} : System.Runtime.CompilerServices.IUnion"); _writer.WriteBraceNested(() => { @@ -91,15 +92,22 @@ void WriteUnion() /// private void WriteStorageFields(UnionLayout layout) { - if (layout.TagField != null) - _writer.WriteLine($"private readonly {layout.TagField.Type.TypeName} {layout.TagField.Name};"); + // place data fields up front since they are either reference type fiels or + // likely contain reference types (otherwise they would have been overlapped) + // and will always be pointer-size aligned. + foreach (var field in layout.DataFields) + { + _writer.WriteLine($"private readonly {field.Type.TypeName} {field.Name};"); + } if (layout.OverlappedField != null) _writer.WriteLine($"private readonly {layout.OverlappedField.Type.TypeName} {layout.OverlappedField.Name};"); - foreach (var field in layout.DataFields) + // put tag field at end so it may fit inside any otherwise padding space left over from the overlapped field being less than pointer size aligned. + if (layout.TagField != null) { - _writer.WriteLine($"private readonly {field.Type.TypeName} {field.Name};"); + var type = layout.CaseLayouts.Count <= 255 ? "byte" : "ushort"; + _writer.WriteLine($"private readonly {type} {layout.TagField.Name};"); } }