From 5eafec2ad70ce65c7a5be17cf50d6e353458f12b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Sat, 29 Aug 2026 14:15:45 -0400 Subject: [PATCH 1/3] Use hash-based string table reservations Replace per-insertion tree ordering with hash-based deduplication and restore ordinal order once before suffix sorting. This preserves object bytes while reducing reservation CPU. Add COFF coverage for suffix sharing, duplicate names, UTF-8 boundaries, section and symbol interactions, repeated flushes, and determinism. --- .../ObjectWriter/StringTableBuilder.cs | 11 +- .../CoffStringTableTests.cs | 255 ++++++++++++++++++ .../ILCompiler.Compiler.Tests.csproj | 1 + .../ILCompiler.Compiler.csproj | 2 + 4 files changed, 265 insertions(+), 4 deletions(-) create mode 100644 src/coreclr/tools/aot/ILCompiler.Compiler.Tests/CoffStringTableTests.cs diff --git a/src/coreclr/tools/Common/Compiler/ObjectWriter/StringTableBuilder.cs b/src/coreclr/tools/Common/Compiler/ObjectWriter/StringTableBuilder.cs index db5a9a9406e004..00a033df90f10a 100644 --- a/src/coreclr/tools/Common/Compiler/ObjectWriter/StringTableBuilder.cs +++ b/src/coreclr/tools/Common/Compiler/ObjectWriter/StringTableBuilder.cs @@ -7,7 +7,6 @@ using System.Diagnostics; using System.IO; using System.Text; -using System.Linq; using Internal.Text; namespace ILCompiler.ObjectWriter @@ -15,7 +14,7 @@ namespace ILCompiler.ObjectWriter internal class StringTableBuilder { private readonly MemoryStream _stream = new(); - private readonly SortedSet _reservedStrings = new(); + private readonly HashSet _reservedStrings = new(); private Dictionary _stringToOffset = new(); public void Write(Stream stream) @@ -43,9 +42,13 @@ public void ReserveString(Utf8String text) private void FlushReservedStrings() { - Utf8String[] reservedStrings = _reservedStrings.ToArray(); + Utf8String[] reservedStrings = new Utf8String[_reservedStrings.Count]; + _reservedStrings.CopyTo(reservedStrings); - // Pre-sort the string based on their matching suffix + // Establish a deterministic order before the in-place suffix sort. + Array.Sort(reservedStrings); + + // Sort strings so matching suffixes are adjacent. MultiKeySort(reservedStrings, 0); // Add the strings to string table diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/CoffStringTableTests.cs b/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/CoffStringTableTests.cs new file mode 100644 index 00000000000000..8b2b4442811034 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/CoffStringTableTests.cs @@ -0,0 +1,255 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using System.Text; +using ILCompiler.ObjectWriter; +using Internal.Text; +using Xunit; + +namespace ILCompiler.Compiler.Tests +{ + public class CoffStringTableTests + { + [Fact] + public void EncodesSectionAndSymbolNames() + { + string[] sectionNames = + [ + "12345678", + "éééé", + ".very.long.section", + "ééééx", + ]; + string[] symbolNames = + [ + "short", + "12345678", + "123456789", + "éééé", + "ééééx", + ".very.long.section", + "prefix.symbol.name", + "symbol.name", + "short", + ]; + + CoffStringTableResult result = CoffObjectWriterAccessor.BuildStringTable(sectionNames, symbolNames); + + Assert.Equal(CreateNameField("12345678"), result.SectionNameFields[0]); + Assert.Equal(CreateNameField("éééé"), result.SectionNameFields[1]); + Assert.Equal(CreateNameField("/4"), result.SectionNameFields[2]); + Assert.Equal(CreateNameField("/23"), result.SectionNameFields[3]); + + byte[] expectedStringTable = CreateExpectedStringTable( + ".very.long.section", + "ééééx", + "éééé", + "short", + "prefix.symbol.name", + "123456789", + "12345678"); + + Assert.Equal(expectedStringTable, result.StringTable); + Assert.Equal((uint)expectedStringTable.Length, result.Size); + Assert.Equal(4u, result.SymbolOffsets[".very.long.section"]); + Assert.Equal(23u, result.SymbolOffsets["ééééx"]); + Assert.Equal(33u, result.SymbolOffsets["éééé"]); + Assert.Equal(42u, result.SymbolOffsets["short"]); + Assert.Equal(48u, result.SymbolOffsets["prefix.symbol.name"]); + Assert.Equal(55u, result.SymbolOffsets["symbol.name"]); + Assert.Equal(67u, result.SymbolOffsets["123456789"]); + Assert.Equal(77u, result.SymbolOffsets["12345678"]); + } + + [Fact] + public void SharesSuffixesAndDeduplicatesNames() + { + string[] symbolNames = + [ + "symbol", + "shared.symbol", + "prefix.shared.symbol", + "shared.symbol", + "prefix.shared.symbol", + ]; + + CoffStringTableResult result = CoffObjectWriterAccessor.BuildStringTable(Array.Empty(), symbolNames); + byte[] expectedStringTable = CreateExpectedStringTable("prefix.shared.symbol"); + + Assert.Equal(expectedStringTable, result.StringTable); + Assert.Equal((uint)expectedStringTable.Length, result.Size); + Assert.Equal(4u, result.SymbolOffsets["prefix.shared.symbol"]); + Assert.Equal(11u, result.SymbolOffsets["shared.symbol"]); + Assert.Equal(18u, result.SymbolOffsets["symbol"]); + } + + [Fact] + public void SupportsMultipleReservationBatchesAndRepeatedWrites() + { + var stringTable = new StringTableBuilder(); + var sharedSymbol = new Utf8String("shared.symbol"); + var symbol = new Utf8String("symbol"); + + stringTable.ReserveString(sharedSymbol); + stringTable.ReserveString(symbol); + + Assert.Equal(14u, stringTable.Size); + Assert.Equal(0u, stringTable.GetStringOffset(sharedSymbol)); + Assert.Equal(7u, stringTable.GetStringOffset(symbol)); + + var prefixedSymbol = new Utf8String("prefix.shared.symbol"); + stringTable.ReserveString(prefixedSymbol); + stringTable.ReserveString(sharedSymbol); + + Assert.Equal(35u, stringTable.Size); + Assert.Equal(14u, stringTable.GetStringOffset(prefixedSymbol)); + + byte[] expected = Encoding.UTF8.GetBytes("shared.symbol\0prefix.shared.symbol\0"); + Assert.Equal(expected, WriteStringTable(stringTable)); + Assert.Equal(expected, WriteStringTable(stringTable)); + } + + [Fact] + public void OutputIsDeterministicAndOrdinalAcrossReservationOrders() + { + string[][] reservationOrders = + [ + ["", "alpha", "ALPHA", "prefix.shared", "shared", "méthode", "方法", "12345678", "123456789"], + ["123456789", "12345678", "方法", "méthode", "shared", "prefix.shared", "ALPHA", "alpha", ""], + ["shared", "", "12345678", "alpha", "方法", "123456789", "prefix.shared", "méthode", "ALPHA"], + ]; + + CoffStringTableResult expected = CoffObjectWriterAccessor.BuildStringTable( + Array.Empty(), + reservationOrders[0]); + Assert.Equal( + CreateExpectedStringTable("方法", "méthode", "prefix.shared", "alpha", "ALPHA", "123456789", "12345678"), + expected.StringTable); + Assert.Equal(64u, expected.SymbolOffsets[""]); + Assert.Equal(27u, expected.SymbolOffsets["shared"]); + + foreach (string[] reservationOrder in reservationOrders) + { + CoffStringTableResult actual = CoffObjectWriterAccessor.BuildStringTable( + Array.Empty(), + reservationOrder); + + Assert.Equal(expected.Size, actual.Size); + Assert.Equal(expected.StringTable, actual.StringTable); + foreach (KeyValuePair pair in expected.SymbolOffsets) + { + Assert.Equal(pair.Value, actual.SymbolOffsets[pair.Key]); + } + } + } + + private static byte[] WriteStringTable(StringTableBuilder stringTable) + { + using MemoryStream stream = new(); + stringTable.Write(stream); + return stream.ToArray(); + } + + private static byte[] CreateNameField(string text) + { + byte[] result = new byte[8]; + Encoding.UTF8.GetBytes(text).CopyTo(result, 0); + return result; + } + + private static byte[] CreateExpectedStringTable(params string[] entries) + { + using MemoryStream stream = new(); + stream.Write(new byte[sizeof(uint)]); + foreach (string entry in entries) + { + stream.Write(Encoding.UTF8.GetBytes(entry)); + stream.WriteByte(0); + } + + byte[] result = stream.ToArray(); + BinaryPrimitives.WriteUInt32LittleEndian(result, checked((uint)result.Length)); + return result; + } + + private sealed class CoffStringTableResult + { + public CoffStringTableResult( + byte[][] sectionNameFields, + byte[] stringTable, + uint size, + Dictionary symbolOffsets) + { + SectionNameFields = sectionNameFields; + StringTable = stringTable; + Size = size; + SymbolOffsets = symbolOffsets; + } + + public byte[][] SectionNameFields { get; } + public byte[] StringTable { get; } + public uint Size { get; } + public Dictionary SymbolOffsets { get; } + } + + private sealed class CoffObjectWriterAccessor : CoffObjectWriter + { + private CoffObjectWriterAccessor() + : base(null, default) + { + } + + public static CoffStringTableResult BuildStringTable( + IReadOnlyList sectionNames, + IReadOnlyList symbolNames) + { + CoffStringTable stringTable = new(); + byte[][] sectionNameFields = new byte[sectionNames.Count][]; + + using (MemoryStream sectionHeaders = new()) + { + for (int i = 0; i < sectionNames.Count; i++) + { + long headerOffset = sectionHeaders.Position; + var sectionHeader = new CoffSectionHeader + { + Name = sectionNames[i], + }; + sectionHeader.Write(sectionHeaders, stringTable); + sectionNameFields[i] = sectionHeaders.GetBuffer().AsSpan((int)headerOffset, 8).ToArray(); + } + } + + Utf8String[] utf8SymbolNames = new Utf8String[symbolNames.Count]; + for (int i = 0; i < symbolNames.Count; i++) + { + utf8SymbolNames[i] = new Utf8String(symbolNames[i]); + stringTable.ReserveString(utf8SymbolNames[i]); + } + + var symbolOffsets = new Dictionary(StringComparer.Ordinal); + for (int i = 0; i < symbolNames.Count; i++) + { + if (!symbolOffsets.ContainsKey(symbolNames[i])) + { + symbolOffsets.Add(symbolNames[i], stringTable.GetStringOffset(utf8SymbolNames[i])); + } + } + uint size = stringTable.Size; + + using MemoryStream tableStream = new(); + stringTable.Write(tableStream); + + return new CoffStringTableResult( + sectionNameFields, + tableStream.ToArray(), + size, + symbolOffsets); + } + } + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/ILCompiler.Compiler.Tests.csproj b/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/ILCompiler.Compiler.Tests.csproj index 69e9d87637f92d..1ecabefafcf990 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/ILCompiler.Compiler.Tests.csproj +++ b/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/ILCompiler.Compiler.Tests.csproj @@ -40,6 +40,7 @@ + diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj b/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj index 0964719be88f3d..a2f3a9ad93dd89 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj @@ -26,6 +26,8 @@ + + all contentfiles From 3e1647acde342abdbead5c40da508d12cb504f1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Sat, 29 Aug 2026 20:58:48 -0400 Subject: [PATCH 2/3] Address string table review feedback Skip empty string-table flushes and remove the dedicated white-box tests in favor of the existing functional object-writer coverage. --- .../ObjectWriter/StringTableBuilder.cs | 5 + .../CoffStringTableTests.cs | 255 ------------------ .../ILCompiler.Compiler.Tests.csproj | 1 - .../ILCompiler.Compiler.csproj | 2 - 4 files changed, 5 insertions(+), 258 deletions(-) delete mode 100644 src/coreclr/tools/aot/ILCompiler.Compiler.Tests/CoffStringTableTests.cs diff --git a/src/coreclr/tools/Common/Compiler/ObjectWriter/StringTableBuilder.cs b/src/coreclr/tools/Common/Compiler/ObjectWriter/StringTableBuilder.cs index 00a033df90f10a..a491cc5d60582a 100644 --- a/src/coreclr/tools/Common/Compiler/ObjectWriter/StringTableBuilder.cs +++ b/src/coreclr/tools/Common/Compiler/ObjectWriter/StringTableBuilder.cs @@ -42,6 +42,11 @@ public void ReserveString(Utf8String text) private void FlushReservedStrings() { + if (_reservedStrings.Count == 0) + { + return; + } + Utf8String[] reservedStrings = new Utf8String[_reservedStrings.Count]; _reservedStrings.CopyTo(reservedStrings); diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/CoffStringTableTests.cs b/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/CoffStringTableTests.cs deleted file mode 100644 index 8b2b4442811034..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/CoffStringTableTests.cs +++ /dev/null @@ -1,255 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Buffers.Binary; -using System.Collections.Generic; -using System.IO; -using System.Text; -using ILCompiler.ObjectWriter; -using Internal.Text; -using Xunit; - -namespace ILCompiler.Compiler.Tests -{ - public class CoffStringTableTests - { - [Fact] - public void EncodesSectionAndSymbolNames() - { - string[] sectionNames = - [ - "12345678", - "éééé", - ".very.long.section", - "ééééx", - ]; - string[] symbolNames = - [ - "short", - "12345678", - "123456789", - "éééé", - "ééééx", - ".very.long.section", - "prefix.symbol.name", - "symbol.name", - "short", - ]; - - CoffStringTableResult result = CoffObjectWriterAccessor.BuildStringTable(sectionNames, symbolNames); - - Assert.Equal(CreateNameField("12345678"), result.SectionNameFields[0]); - Assert.Equal(CreateNameField("éééé"), result.SectionNameFields[1]); - Assert.Equal(CreateNameField("/4"), result.SectionNameFields[2]); - Assert.Equal(CreateNameField("/23"), result.SectionNameFields[3]); - - byte[] expectedStringTable = CreateExpectedStringTable( - ".very.long.section", - "ééééx", - "éééé", - "short", - "prefix.symbol.name", - "123456789", - "12345678"); - - Assert.Equal(expectedStringTable, result.StringTable); - Assert.Equal((uint)expectedStringTable.Length, result.Size); - Assert.Equal(4u, result.SymbolOffsets[".very.long.section"]); - Assert.Equal(23u, result.SymbolOffsets["ééééx"]); - Assert.Equal(33u, result.SymbolOffsets["éééé"]); - Assert.Equal(42u, result.SymbolOffsets["short"]); - Assert.Equal(48u, result.SymbolOffsets["prefix.symbol.name"]); - Assert.Equal(55u, result.SymbolOffsets["symbol.name"]); - Assert.Equal(67u, result.SymbolOffsets["123456789"]); - Assert.Equal(77u, result.SymbolOffsets["12345678"]); - } - - [Fact] - public void SharesSuffixesAndDeduplicatesNames() - { - string[] symbolNames = - [ - "symbol", - "shared.symbol", - "prefix.shared.symbol", - "shared.symbol", - "prefix.shared.symbol", - ]; - - CoffStringTableResult result = CoffObjectWriterAccessor.BuildStringTable(Array.Empty(), symbolNames); - byte[] expectedStringTable = CreateExpectedStringTable("prefix.shared.symbol"); - - Assert.Equal(expectedStringTable, result.StringTable); - Assert.Equal((uint)expectedStringTable.Length, result.Size); - Assert.Equal(4u, result.SymbolOffsets["prefix.shared.symbol"]); - Assert.Equal(11u, result.SymbolOffsets["shared.symbol"]); - Assert.Equal(18u, result.SymbolOffsets["symbol"]); - } - - [Fact] - public void SupportsMultipleReservationBatchesAndRepeatedWrites() - { - var stringTable = new StringTableBuilder(); - var sharedSymbol = new Utf8String("shared.symbol"); - var symbol = new Utf8String("symbol"); - - stringTable.ReserveString(sharedSymbol); - stringTable.ReserveString(symbol); - - Assert.Equal(14u, stringTable.Size); - Assert.Equal(0u, stringTable.GetStringOffset(sharedSymbol)); - Assert.Equal(7u, stringTable.GetStringOffset(symbol)); - - var prefixedSymbol = new Utf8String("prefix.shared.symbol"); - stringTable.ReserveString(prefixedSymbol); - stringTable.ReserveString(sharedSymbol); - - Assert.Equal(35u, stringTable.Size); - Assert.Equal(14u, stringTable.GetStringOffset(prefixedSymbol)); - - byte[] expected = Encoding.UTF8.GetBytes("shared.symbol\0prefix.shared.symbol\0"); - Assert.Equal(expected, WriteStringTable(stringTable)); - Assert.Equal(expected, WriteStringTable(stringTable)); - } - - [Fact] - public void OutputIsDeterministicAndOrdinalAcrossReservationOrders() - { - string[][] reservationOrders = - [ - ["", "alpha", "ALPHA", "prefix.shared", "shared", "méthode", "方法", "12345678", "123456789"], - ["123456789", "12345678", "方法", "méthode", "shared", "prefix.shared", "ALPHA", "alpha", ""], - ["shared", "", "12345678", "alpha", "方法", "123456789", "prefix.shared", "méthode", "ALPHA"], - ]; - - CoffStringTableResult expected = CoffObjectWriterAccessor.BuildStringTable( - Array.Empty(), - reservationOrders[0]); - Assert.Equal( - CreateExpectedStringTable("方法", "méthode", "prefix.shared", "alpha", "ALPHA", "123456789", "12345678"), - expected.StringTable); - Assert.Equal(64u, expected.SymbolOffsets[""]); - Assert.Equal(27u, expected.SymbolOffsets["shared"]); - - foreach (string[] reservationOrder in reservationOrders) - { - CoffStringTableResult actual = CoffObjectWriterAccessor.BuildStringTable( - Array.Empty(), - reservationOrder); - - Assert.Equal(expected.Size, actual.Size); - Assert.Equal(expected.StringTable, actual.StringTable); - foreach (KeyValuePair pair in expected.SymbolOffsets) - { - Assert.Equal(pair.Value, actual.SymbolOffsets[pair.Key]); - } - } - } - - private static byte[] WriteStringTable(StringTableBuilder stringTable) - { - using MemoryStream stream = new(); - stringTable.Write(stream); - return stream.ToArray(); - } - - private static byte[] CreateNameField(string text) - { - byte[] result = new byte[8]; - Encoding.UTF8.GetBytes(text).CopyTo(result, 0); - return result; - } - - private static byte[] CreateExpectedStringTable(params string[] entries) - { - using MemoryStream stream = new(); - stream.Write(new byte[sizeof(uint)]); - foreach (string entry in entries) - { - stream.Write(Encoding.UTF8.GetBytes(entry)); - stream.WriteByte(0); - } - - byte[] result = stream.ToArray(); - BinaryPrimitives.WriteUInt32LittleEndian(result, checked((uint)result.Length)); - return result; - } - - private sealed class CoffStringTableResult - { - public CoffStringTableResult( - byte[][] sectionNameFields, - byte[] stringTable, - uint size, - Dictionary symbolOffsets) - { - SectionNameFields = sectionNameFields; - StringTable = stringTable; - Size = size; - SymbolOffsets = symbolOffsets; - } - - public byte[][] SectionNameFields { get; } - public byte[] StringTable { get; } - public uint Size { get; } - public Dictionary SymbolOffsets { get; } - } - - private sealed class CoffObjectWriterAccessor : CoffObjectWriter - { - private CoffObjectWriterAccessor() - : base(null, default) - { - } - - public static CoffStringTableResult BuildStringTable( - IReadOnlyList sectionNames, - IReadOnlyList symbolNames) - { - CoffStringTable stringTable = new(); - byte[][] sectionNameFields = new byte[sectionNames.Count][]; - - using (MemoryStream sectionHeaders = new()) - { - for (int i = 0; i < sectionNames.Count; i++) - { - long headerOffset = sectionHeaders.Position; - var sectionHeader = new CoffSectionHeader - { - Name = sectionNames[i], - }; - sectionHeader.Write(sectionHeaders, stringTable); - sectionNameFields[i] = sectionHeaders.GetBuffer().AsSpan((int)headerOffset, 8).ToArray(); - } - } - - Utf8String[] utf8SymbolNames = new Utf8String[symbolNames.Count]; - for (int i = 0; i < symbolNames.Count; i++) - { - utf8SymbolNames[i] = new Utf8String(symbolNames[i]); - stringTable.ReserveString(utf8SymbolNames[i]); - } - - var symbolOffsets = new Dictionary(StringComparer.Ordinal); - for (int i = 0; i < symbolNames.Count; i++) - { - if (!symbolOffsets.ContainsKey(symbolNames[i])) - { - symbolOffsets.Add(symbolNames[i], stringTable.GetStringOffset(utf8SymbolNames[i])); - } - } - uint size = stringTable.Size; - - using MemoryStream tableStream = new(); - stringTable.Write(tableStream); - - return new CoffStringTableResult( - sectionNameFields, - tableStream.ToArray(), - size, - symbolOffsets); - } - } - } -} diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/ILCompiler.Compiler.Tests.csproj b/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/ILCompiler.Compiler.Tests.csproj index 1ecabefafcf990..69e9d87637f92d 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/ILCompiler.Compiler.Tests.csproj +++ b/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/ILCompiler.Compiler.Tests.csproj @@ -40,7 +40,6 @@ - diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj b/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj index a2f3a9ad93dd89..0964719be88f3d 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj @@ -26,8 +26,6 @@ - - all contentfiles From d26a801c511b678ff7d33a44cd282e2e4a8e61f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Sat, 29 Aug 2026 21:31:59 -0400 Subject: [PATCH 3/3] Mark string offset map readonly --- .../tools/Common/Compiler/ObjectWriter/StringTableBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/tools/Common/Compiler/ObjectWriter/StringTableBuilder.cs b/src/coreclr/tools/Common/Compiler/ObjectWriter/StringTableBuilder.cs index a491cc5d60582a..d643c98ec06441 100644 --- a/src/coreclr/tools/Common/Compiler/ObjectWriter/StringTableBuilder.cs +++ b/src/coreclr/tools/Common/Compiler/ObjectWriter/StringTableBuilder.cs @@ -15,7 +15,7 @@ internal class StringTableBuilder { private readonly MemoryStream _stream = new(); private readonly HashSet _reservedStrings = new(); - private Dictionary _stringToOffset = new(); + private readonly Dictionary _stringToOffset = new(); public void Write(Stream stream) {