diff --git a/src/libraries/System.Reflection.Metadata/src/System.Reflection.Metadata.csproj b/src/libraries/System.Reflection.Metadata/src/System.Reflection.Metadata.csproj index 02c844b70e43e9..fbc8909bdc6e19 100644 --- a/src/libraries/System.Reflection.Metadata/src/System.Reflection.Metadata.csproj +++ b/src/libraries/System.Reflection.Metadata/src/System.Reflection.Metadata.csproj @@ -1,4 +1,4 @@ - + $(NetCoreAppCurrent);$(NetCoreAppPrevious);$(NetCoreAppMinimum);netstandard2.0;$(NetFrameworkMinimum) @@ -49,6 +49,7 @@ The System.Reflection.Metadata library is built-in as part of the shared framewo + diff --git a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Internal/Utilities/Hash.cs b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Internal/Utilities/Hash.cs index d083ff1307c3c3..d490710d1749ce 100644 --- a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Internal/Utilities/Hash.cs +++ b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Internal/Utilities/Hash.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Immutable; - namespace System.Reflection.Internal { internal static class Hash @@ -40,13 +38,20 @@ internal static int Combine(bool newKeyPart, int currentKey) /// /// The sequence of bytes /// The FNV-1a hash of - internal static int GetFNVHashCode(ReadOnlySpan data) - { - int hashCode = Hash.FnvOffsetBias; + internal static int GetFNVHashCode(ReadOnlySpan data) => AccumulateFNVHashCode(FnvOffsetBias, data); + /// + /// Compute the FNV-1a hash of a sequence of bytes + /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + /// + /// The current hash code + /// The sequence of bytes + /// The updated hash code + internal static int AccumulateFNVHashCode(int hashCode, ReadOnlySpan data) + { for (int i = 0; i < data.Length; i++) { - hashCode = unchecked((hashCode ^ data[i]) * Hash.FnvPrime); + hashCode = unchecked((hashCode ^ data[i]) * FnvPrime); } return hashCode; diff --git a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/BlobBuilder.Segment.cs b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/BlobBuilder.Segment.cs new file mode 100644 index 00000000000000..bf21acbc09da5b --- /dev/null +++ b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/BlobBuilder.Segment.cs @@ -0,0 +1,410 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Reflection.Internal; + +namespace System.Reflection.Metadata +{ + public partial class BlobBuilder + { + /// + /// Returns whether the is in a state where -addressable + /// data can be written to it. + /// + /// + /// This is true if the blob builder contains more than one chunk, the head chunk is empty, and the + /// previous chunk has some minimum free space. + /// + /// + private bool CanWriteSegment => _nextOrPrevious != this && Length == 0 && _nextOrPrevious.FreeBytes > 0; + + // TODO: Move the chunking logic to the main BlobBuilder.cs file, and use it everywhere in BlobBuilder. + // https://github.com/dotnet/runtime/issues/100418 + internal const int DefaultMaxChunkSize = 8192; + + private int NextChunkLength => Math.Max(MinChunkSize, Math.Min(Count, DefaultMaxChunkSize)); + + /// + /// Brings the into a state where -addressable + /// data can be written to it. + /// + /// + private void EnsureCanWriteSegment(int minimumSize = 1) + { + Debug.Assert(minimumSize > 0); + if (!CanWriteSegment || _nextOrPrevious.FreeBytes < minimumSize) + { + int chunkSize = NextChunkLength; + // We need to have some free space in the last chunk. If the head chunk is full, + // calling Expand() will move it to the last chunk, so we need to call it twice. + if (FreeBytes == 0) + { + Expand(chunkSize, writingSegments: true); + } + Expand(chunkSize, writingSegments: true); + Debug.Assert(CanWriteSegment && _nextOrPrevious.FreeBytes >= minimumSize); + } + } + + private void AddLengthForSegment(int length) + { + Debug.Assert(length > 0); + Debug.Assert(CanWriteSegment && _nextOrPrevious.FreeBytes >= length); + _nextOrPrevious.AddLength(length); + PreviousLength += length; + } + + /// + /// Starts writing -addressable data to the . + /// + /// The first chunk of the segment. + /// The offset to the first byte within the first chunk of the segment. + private void StartSegment(out BlobBuilder startChunk, out int startOffset) + { + Debug.Assert(IsHead); + EnsureCanWriteSegment(); + startChunk = _nextOrPrevious; + startOffset = startChunk.Length; + } + + /// + /// Writes a compressed integer to the . This method should be preferred + /// over when the is used to create + /// s. + /// + /// The integer to write. + private void WriteCompressedIntegerForSegment(int value) + { + EnsureCanWriteSegment(minimumSize: sizeof(int)); + Span writeBuffer = _nextOrPrevious.RemainingSpan; + int written = BlobWriterImpl.WriteCompressedInteger(writeBuffer, unchecked((uint)value)); + AddLengthForSegment(written); + } + + /// + /// Writes a buffer to the , ensuring that its content can be accessed from + /// a . + /// + /// + /// must be called before calling this method. + /// + private void WriteForSegment(ReadOnlySpan buffer) + { + Debug.Assert(CanWriteSegment); + int expectedCount = Count + buffer.Length; + while (!buffer.IsEmpty) + { + Span writeBuffer = _nextOrPrevious.RemainingSpan; + int bytesToWrite = Math.Min(writeBuffer.Length, buffer.Length); + buffer.Slice(0, bytesToWrite).CopyTo(writeBuffer); + buffer = buffer.Slice(bytesToWrite); + AddLengthForSegment(bytesToWrite); + EnsureCanWriteSegment(); + } + Debug.Assert(Count == expectedCount); + } + + /// + /// Finishes creation of a . + /// + private Segment FinishSegment(BlobBuilder startChunk, int startOffset, int size) + { + Debug.Assert(CanWriteSegment); + CheckInvariants(writingSegments: true); + return new Segment(startChunk, startOffset, size); + } + + /// + /// Writes a buffer to the and returns a over + /// the written data. + /// + /// The buffer to write. + /// Whether to prepend 's size as + /// a compressed integer. The bytes of the size will not be included in the . + internal Segment WriteSegment(ReadOnlySpan buffer, bool prependCompressedSize = false) + { + if (prependCompressedSize) + { + WriteCompressedIntegerForSegment(buffer.Length); + } + StartSegment(out BlobBuilder startChunk, out int startOffset); + WriteForSegment(buffer); + return FinishSegment(startChunk, startOffset, buffer.Length); + } + + /// + /// Writes the contents of a to the and + /// returns a over the written data. + /// + /// The to write. + /// Whether to prepend 's size as + /// a compressed integer. The bytes of the size will not be included in the . + internal Segment WriteSegment(BlobBuilder builder, bool prependCompressedSize = false) + { + if (prependCompressedSize) + { + WriteCompressedIntegerForSegment(builder.Count); + } + StartSegment(out BlobBuilder startChunk, out int startOffset); + foreach (BlobBuilder chunk in builder.GetChunks()) + { + WriteForSegment(chunk.Span); + } + return FinishSegment(startChunk, startOffset, builder.Count); + } + + /// + /// Performs necessary adjustments to the after writing - + /// addressable data to it. + /// + /// + /// This method must be called before linking a that has had - + /// addressable data written to it, and invalidates previously returned s. + /// + internal void FinishWritingSegments() + { + Debug.Assert(IsHead); + if (_nextOrPrevious == this || Length > 0) + { + return; + } + + BlobBuilder lastChunk = _nextOrPrevious; + BlobBuilder firstChunk = lastChunk._nextOrPrevious; + BlobBuilder penultimateChunk = firstChunk; + while (penultimateChunk._nextOrPrevious != lastChunk) + { + penultimateChunk = penultimateChunk._nextOrPrevious; + } + bool twoChunks = firstChunk == lastChunk; + + // We have: + // [First]->[]->[Penultimate]->[Last] <- [this (empty)] + // ^__________________________| + // And want to turn it into this: + // [First]->[]->[Penultimate] <- [Last] + // ^_______________| + // Degenerate cases: + // [First == Penultimate == Last] <- [this (empty)] + // ^______________________| + // [First == Penultimate] <- [Last] <- [this (empty)] + // ^_________________________| + + // Swap buffers of last and head chunks, and free the last chunk: + byte[] lastBuffer = lastChunk._buffer; + int lastLength = lastChunk.Length; // don't copy the frozen flag + lastChunk._buffer = _buffer; + lastChunk._length = _length; + _buffer = lastBuffer; + _length = (uint)lastLength; + lastChunk.ClearAndFreeChunk(); + // Relink chunks: + if (twoChunks) + { + // If there are only two chunks, turn this into a single-chunk BlobBuilder. + _nextOrPrevious = this; + PreviousLength = 0; + } + else + { + // If there are more than two chunks, set the penultimate chunk as the new last chunk. + // This handles the case of First == Penultimate as well. + _nextOrPrevious = penultimateChunk; + penultimateChunk._nextOrPrevious = firstChunk; + PreviousLength = penultimateChunk.Count; + } + + // The standard set of invariants should now hold. + CheckInvariants(); + } + + /// + /// Represents a contiguous region of data in a . + /// + /// + /// + [DebuggerDisplay("Count = {Count}")] + internal readonly struct Segment(BlobBuilder builder, int offset, int size) + { + private readonly BlobBuilder _builder = builder; + private readonly int _offset = offset; + + public int Count { get; } = size; + + public Chunks GetChunks() => new Chunks(this); + + public bool ContentEquals(ReadOnlySpan other) + { + if (Count != other.Length) + { + return false; + } + + foreach (ReadOnlySpan chunk in GetChunks()) + { + if (!chunk.SequenceEqual(other.Slice(0, chunk.Length))) + { + return false; + } + other = other.Slice(chunk.Length); + } + + return true; + } + + public bool ContentEquals(BlobBuilder other) + { + if (Count != other.Count) + { + return false; + } + + Chunks leftEnumerator = GetChunks(); + BlobBuilder.Chunks rightEnumerator = other.GetChunks(); + int leftStart = 0; + int rightStart = 0; + + bool leftContinues = leftEnumerator.MoveNext(); + bool rightContinues = rightEnumerator.MoveNext(); + + while (leftContinues && rightContinues) + { + Debug.Assert(leftStart == 0 || rightStart == 0); + + ReadOnlySpan left = leftEnumerator.Current; + ReadOnlySpan right = rightEnumerator.Current.Span; + + int minLength = Math.Min(left.Length - leftStart, right.Length - rightStart); + if (!left.Slice(leftStart, minLength).SequenceEqual(right.Slice(rightStart, minLength))) + { + return false; + } + + leftStart += minLength; + rightStart += minLength; + + // nothing remains in left chunk to compare: + if (leftStart == left.Length) + { + leftContinues = leftEnumerator.MoveNext(); + leftStart = 0; + } + + // nothing remains in right chunk to compare: + if (rightStart == right.Length) + { + rightContinues = rightEnumerator.MoveNext(); + rightStart = 0; + } + } + + return leftContinues == rightContinues; + } + +#if NET + public bool ContentEquals(Segment other) + { + if (Count != other.Count) + { + return false; + } + + Chunks leftEnumerator = GetChunks(); + Chunks rightEnumerator = other.GetChunks(); + int leftStart = 0; + int rightStart = 0; + + bool leftContinues = leftEnumerator.MoveNext(); + bool rightContinues = rightEnumerator.MoveNext(); + + while (leftContinues && rightContinues) + { + Debug.Assert(leftStart == 0 || rightStart == 0); + + ReadOnlySpan left = leftEnumerator.Current; + ReadOnlySpan right = rightEnumerator.Current; + + int minLength = Math.Min(left.Length - leftStart, right.Length - rightStart); + if (!left.Slice(leftStart, minLength).SequenceEqual(right.Slice(rightStart, minLength))) + { + return false; + } + + leftStart += minLength; + rightStart += minLength; + + // nothing remains in left chunk to compare: + if (leftStart == left.Length) + { + leftContinues = leftEnumerator.MoveNext(); + leftStart = 0; + } + + // nothing remains in right chunk to compare: + if (rightStart == right.Length) + { + rightContinues = rightEnumerator.MoveNext(); + rightStart = 0; + } + } + + return leftContinues == rightContinues; + } +#endif + + public int GetContentFNVHashCode() + { + int hash = Hash.FnvOffsetBias; + foreach (ReadOnlySpan chunk in GetChunks()) + { + hash = Hash.AccumulateFNVHashCode(hash, chunk); + } + return hash; + } + + public struct Chunks + { + private BlobBuilder _current; + private int _chunkOffset, _chunkLength; + private int _remaining; + private int _steps; + + internal Chunks(Segment segment) + { + _current = segment._builder; + _chunkOffset = segment._offset; + _chunkLength = Math.Min(segment.Count, _current.Length - _chunkOffset); + _remaining = segment.Count - _chunkLength; + } + + public readonly ReadOnlySpan Current => _current.Span.Slice(_chunkOffset, _chunkLength); + + public bool MoveNext() + { + if (_remaining == 0 && _steps != 0) + { + // The segment has been fully enumerated, including the case where it was empty. + return false; + } + switch (_steps++) + { + case 0: + // The enumerator is already initialized to the first chunk, so do nothing the first time MoveNext is called. + return true; + case 1: + // Chunks after the first one start at their beginning. + _chunkOffset = 0; + break; + } + _current = _current._nextOrPrevious; + _chunkLength = Math.Min(_remaining, _current.Length); + _remaining -= _chunkLength; + return true; + } + + public readonly Chunks GetEnumerator() => this; + } + } + } +} diff --git a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/BlobBuilder.cs b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/BlobBuilder.cs index 21628b52aa48f1..23b91b4c3f7bdf 100644 --- a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/BlobBuilder.cs +++ b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/BlobBuilder.cs @@ -30,7 +30,16 @@ public partial class BlobBuilder // // In this case the content represented is a sequence (1,2,3,4). // This structure optimizes for append write operations and sequential enumeration from the start of the chain. - // Data can only be written to the head node. Other nodes are "frozen". + // Usually, data can only be written to the head node and other nodes are "frozen". + // + // However, when using the Segment APIs, we write to the last node, creating additional nodes if necessary. + // Data in a segment cannot reside in the head node, because you cannot link to it from a previous node. + // Writing to the last node satisfies this requirement, and also minimizes changes to the existing writing + // paths, which write to the head node as always. + // This means that alternating between regular and segment writes will lead to gaps in the buffers, but the + // Segment APIs are not public, and this is not an expected use case. + // Blob builders used for writing segments will always have an empty head node, but that memory does not go + // to waste as more data get written, and if the builder is linked, it gets freed normally. private BlobBuilder _nextOrPrevious; private BlobBuilder FirstChunk => _nextOrPrevious._nextOrPrevious; @@ -51,6 +60,7 @@ public partial class BlobBuilder private int Length => (int)(_length & ~IsFrozenMask); private uint FrozenLength => _length | IsFrozenMask; private Span Span => _buffer.AsSpan(0, Length); + private Span RemainingSpan => _buffer.AsSpan(Length); public BlobBuilder(int capacity = DefaultChunkSize) { @@ -119,7 +129,7 @@ internal void ClearChunk() } [Conditional("DEBUG")] - private void CheckInvariants() + private void CheckInvariants(bool writingSegments = false) { Debug.Assert(_buffer != null); Debug.Assert(Length >= 0 && Length <= _buffer.Length); @@ -133,7 +143,15 @@ private void CheckInvariants() int totalLength = 0; foreach (var chunk in GetChunks()) { - Debug.Assert(chunk.IsHead || chunk.Length > 0); + if (!writingSegments) + { + Debug.Assert(chunk.IsHead || chunk.Length > 0); + } + else + { + // The last chunk can be empty if we are writing segments. + Debug.Assert(chunk.IsHead || chunk.Length > 0 || chunk == _nextOrPrevious); + } totalLength += chunk.Length; } @@ -249,7 +267,7 @@ public bool ContentEquals(BlobBuilder other) leftStart = 0; } - // nothing remains in left chunk to compare: + // nothing remains in right chunk to compare: if (rightStart == right.Length) { rightContinues = rightEnumerator.MoveNext(); @@ -260,6 +278,20 @@ public bool ContentEquals(BlobBuilder other) return leftContinues == rightContinues; } + internal int GetContentFNVHashCode() + { + if (!IsHead) + { + Throw.InvalidOperationBuilderAlreadyLinked(); + } + int hashCode = Hash.FnvOffsetBias; + foreach (BlobBuilder chunk in GetChunks()) + { + hashCode = Hash.AccumulateFNVHashCode(hashCode, chunk.Span); + } + return hashCode; + } + /// Content is not available, the builder has been linked with another one. public byte[] ToArray() { @@ -318,19 +350,6 @@ public ImmutableArray ToImmutableArray(int start, int byteCount) return ImmutableCollectionsMarshal.AsImmutableArray(array); } - internal bool TryGetSpan(out ReadOnlySpan buffer) - { - if (_nextOrPrevious == this) - { - // If the blob builder has one chunk, we can just return it and avoid copies. - buffer = Span; - return true; - } - - buffer = default; - return false; - } - /// is null. /// Content is not available, the builder has been linked with another one. public void WriteContentTo(Stream destination) @@ -520,7 +539,7 @@ private void AddLength(int value) } [MethodImpl(MethodImplOptions.NoInlining)] - private void Expand(int newLength) + private void Expand(int newLength, bool writingSegments = false) { // TODO: consider converting the last chunk to a smaller one if there is too much empty space left @@ -540,9 +559,10 @@ private void Expand(int newLength) var newBuffer = newChunk._buffer; - if (_length == 0) + if (_length == 0 && !writingSegments) { // If the first write into an empty buffer needs more space than the buffer provides, swap the buffers. + // We don't want this when writing segments. newChunk._buffer = _buffer; _buffer = newBuffer; } @@ -573,7 +593,7 @@ private void Expand(int newLength) _length = 0; } - CheckInvariants(); + CheckInvariants(writingSegments); } /// diff --git a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/BlobWriterImpl.cs b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/BlobWriterImpl.cs index fc27827c788b6b..a152479f99a896 100644 --- a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/BlobWriterImpl.cs +++ b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/BlobWriterImpl.cs @@ -32,6 +32,33 @@ internal static int GetCompressedIntegerSize(int value) return 4; } + internal static int WriteCompressedInteger(Span buffer, uint value) + { + unchecked + { + if (value <= SingleByteCompressedIntegerMaxValue) + { + buffer[0] = (byte)value; + return sizeof(byte); + } + else if (value <= TwoByteCompressedIntegerMaxValue) + { + BinaryPrimitives.WriteUInt16BigEndian(buffer, (ushort)(0x8000 | value)); + return sizeof(ushort); + } + else if (value <= MaxCompressedIntegerValue) + { + BinaryPrimitives.WriteUInt32BigEndian(buffer, 0xc0000000 | value); + return sizeof(uint); + } + else + { + Throw.ValueArgumentOutOfRange(); + return 0; + } + } + } + internal static void WriteCompressedInteger(ref BlobWriter writer, uint value) { unchecked diff --git a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/Ecma335/BlobDictionary.cs b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/Ecma335/BlobDictionary.cs index 0fa272a27062b1..f89e3eb6eb56e5 100644 --- a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/Ecma335/BlobDictionary.cs +++ b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/Ecma335/BlobDictionary.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using System.Collections.Immutable; using System.Diagnostics; using System.Reflection.Internal; #if NET @@ -12,68 +11,84 @@ namespace System.Reflection.Metadata.Ecma335 { [DebuggerDisplay("Count = {Count}")] - internal readonly struct BlobDictionary + internal readonly struct BlobDictionary(BlobBuilder builder, int capacity = 0) { - private readonly Dictionary, BlobHandle>> _dictionary; +#if NET + private readonly Dictionary _dictionary = new(capacity, new Comparer(builder)); + + public BlobHandle GetOrAdd(T key, BlobHandle value) where T : notnull, allows ref struct + { + // Always use the alternate lookup; we do not support directly adding segments. + ref BlobHandle entry = + ref CollectionsMarshal.GetValueRefOrAddDefault(_dictionary.GetAlternateLookup(), key, out bool exists); + + if (!exists) + { + entry = value; + } + + return entry; + } + + private sealed class Comparer(BlobBuilder builder) : IEqualityComparer, IAlternateEqualityComparer, BlobBuilder.Segment>, IAlternateEqualityComparer + { + public BlobBuilder.Segment Create(ReadOnlySpan alternate) => builder.WriteSegment(alternate, prependCompressedSize: true); + public BlobBuilder.Segment Create(BlobBuilder alternate) => builder.WriteSegment(alternate, prependCompressedSize: true); + + public bool Equals(BlobBuilder.Segment x, BlobBuilder.Segment y) => x.ContentEquals(y); + public bool Equals(ReadOnlySpan alternate, BlobBuilder.Segment other) => other.ContentEquals(alternate); + public bool Equals(BlobBuilder alternate, BlobBuilder.Segment other) => other.ContentEquals(alternate); + public int GetHashCode(BlobBuilder.Segment obj) => obj.GetContentFNVHashCode(); + public int GetHashCode(ReadOnlySpan alternate) => Hash.GetFNVHashCode(alternate); + public int GetHashCode(BlobBuilder alternate) => alternate.GetContentFNVHashCode(); + } +#else + private readonly BlobBuilder _builder = builder; + + private readonly Dictionary> _dictionary = new(capacity); // A simple LCG. Constants taken from // https://github.com/imneme/pcg-c/blob/83252d9c23df9c82ecb42210afed61a7b42402d7/include/pcg_variants.h#L276-L284 private static int GetNextDictionaryKey(int dictionaryKey) => (int)((uint)dictionaryKey * 747796405 + 2891336453); -#if NET - private unsafe ref KeyValuePair, BlobHandle> GetValueRefOrAddDefault(ReadOnlySpan key, out bool exists) + public BlobHandle GetOrAdd(BlobBuilder key, BlobHandle value) { - int dictionaryKey = Hash.GetFNVHashCode(key); + int dictionaryKey = key.GetContentFNVHashCode(); + KeyValuePair entry; + bool exists; while (true) { - ref var entry = ref CollectionsMarshal.GetValueRefOrAddDefault(_dictionary, dictionaryKey, out exists); - if (!exists || entry.Key.AsSpan().SequenceEqual(key)) + if (!(exists = _dictionary.TryGetValue(dictionaryKey, out entry)) + || entry.Key.ContentEquals(key)) { -#pragma warning disable CS9082 // Local is returned by reference but was initialized to a value that cannot be returned by reference - // In .NET 6 the assembly of GetValueRefOrAddDefault was compiled with earlier ref safety rules - // and caused an error, which was turned into a warning because of unsafe and was suppressed. - return ref entry; -#pragma warning restore CS9082 + break; } dictionaryKey = GetNextDictionaryKey(dictionaryKey); } - } - public BlobHandle GetOrAdd(ReadOnlySpan key, ImmutableArray immutableKey, BlobHandle value, out bool exists) - { - ref var entry = ref GetValueRefOrAddDefault(key, out exists); if (exists) { return entry.Value; } - // If we are given an immutable array, do not allocate a new one. - if (immutableKey.IsDefault) - { - immutableKey = key.ToImmutableArray(); - } - else - { - Debug.Assert(immutableKey.AsSpan().SequenceEqual(key)); - } - - entry = new(immutableKey, value); + _dictionary.Add(dictionaryKey, new(_builder.WriteSegment(key, prependCompressedSize: true), value)); return value; } -#else - public BlobHandle GetOrAdd(ReadOnlySpan key, ImmutableArray immutableKey, BlobHandle value, out bool exists) + + public BlobHandle GetOrAdd(ReadOnlySpan key, BlobHandle value) { - int dictionarykey = Hash.GetFNVHashCode(key); - KeyValuePair, BlobHandle> entry; + int dictionaryKey = Hash.GetFNVHashCode(key); + KeyValuePair entry; + bool exists; while (true) { - if (!(exists = _dictionary.TryGetValue(dictionarykey, out entry)) - || entry.Key.AsSpan().SequenceEqual(key)) + if (!(exists = _dictionary.TryGetValue(dictionaryKey, out entry)) + || entry.Key.ContentEquals(key)) { break; } - dictionarykey = GetNextDictionaryKey(dictionarykey); + dictionaryKey = GetNextDictionaryKey(dictionaryKey); } if (exists) @@ -81,29 +96,13 @@ public BlobHandle GetOrAdd(ReadOnlySpan key, ImmutableArray immutabl return entry.Value; } - // If we are given an immutable array, do not allocate a new one. - if (immutableKey.IsDefault) - { - immutableKey = key.ToImmutableArray(); - } - else - { - Debug.Assert(immutableKey.AsSpan().SequenceEqual(key)); - } - - _dictionary.Add(dictionarykey, new(immutableKey, value)); + _dictionary.Add(dictionaryKey, new(_builder.WriteSegment(key, prependCompressedSize: true), value)); return value; } #endif - public BlobDictionary(int capacity = 0) - { - _dictionary = new(capacity); - } - public int Count => _dictionary.Count; - public Dictionary, BlobHandle>>.Enumerator GetEnumerator() => - _dictionary.GetEnumerator(); + public void Clear() => _dictionary.Clear(); } } diff --git a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/Ecma335/MetadataBuilder.Heaps.cs b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/Ecma335/MetadataBuilder.Heaps.cs index ec99c415fe8d3c..8d04747483b331 100644 --- a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/Ecma335/MetadataBuilder.Heaps.cs +++ b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/Ecma335/MetadataBuilder.Heaps.cs @@ -42,9 +42,9 @@ internal void SetCapacity(int capacity) private int _stringHeapCapacity = 4 * 1024; // #Blob heap - private readonly BlobDictionary _blobs = new BlobDictionary(1024); + private readonly BlobDictionary _blobs; + private readonly HeapBlobBuilder _blobBuilder = new HeapBlobBuilder(1024); private readonly int _blobHeapStartOffset; - private int _blobHeapSize; // #GUID heap private readonly Dictionary _guids = new Dictionary(); @@ -116,8 +116,8 @@ public MetadataBuilder( // beginning of the delta blob. _userStringBuilder.WriteByte(0); - _blobs.GetOrAdd(ReadOnlySpan.Empty, ImmutableArray.Empty, default, out _); - _blobHeapSize = 1; + _blobs = new BlobDictionary(_blobBuilder, 32); + _ = _blobs.GetOrAdd((ReadOnlySpan)[], default); // When EnC delta is applied #US, #String and #Blob heaps are appended. // Thus indices of strings and blobs added to this generation are offset @@ -150,8 +150,7 @@ public void SetCapacity(HeapIndex heap, int byteCount) switch (heap) { case HeapIndex.Blob: - // Not useful to set capacity. - // By the time the blob heap is serialized we know the exact size we need. + _blobBuilder.SetCapacity(byteCount); break; case HeapIndex.Guid: @@ -191,12 +190,7 @@ public BlobHandle GetOrAddBlob(BlobBuilder value) Throw.ArgumentNull(nameof(value)); } - if (value.TryGetSpan(out ReadOnlySpan buffer)) - { - return GetOrAddBlob(buffer); - } - - return GetOrAddBlob(value.ToImmutableArray()); + return GetOrAddBlobImpl(value); } /// @@ -215,17 +209,34 @@ public BlobHandle GetOrAddBlob(byte[] value) return GetOrAddBlob(new ReadOnlySpan(value)); } - private BlobHandle GetOrAddBlob(ReadOnlySpan value, ImmutableArray immutableValue = default) + private BlobHandle GetOrAddBlob(ReadOnlySpan value) => GetOrAddBlobImpl(value); + +#if NET + private BlobHandle GetOrAddBlobImpl(T value) + where T : notnull, allows ref struct { - BlobHandle nextHandle = BlobHandle.FromOffset(_blobHeapStartOffset + _blobHeapSize); - BlobHandle handle = _blobs.GetOrAdd(value, immutableValue, nextHandle, out bool exists); - if (!exists) - { - _blobHeapSize += BlobWriterImpl.GetCompressedIntegerSize(value.Length) + value.Length; - } + BlobHandle nextHandle = BlobHandle.FromOffset(_blobHeapStartOffset + _blobBuilder.Count); + BlobHandle handle = _blobs.GetOrAdd(value, nextHandle); return handle; } +#else + private BlobHandle GetOrAddBlobImpl(ReadOnlySpan value) + { + BlobHandle nextHandle = BlobHandle.FromOffset(_blobHeapStartOffset + _blobBuilder.Count); + BlobHandle handle = _blobs.GetOrAdd(value, nextHandle); + + return handle; + } + + private BlobHandle GetOrAddBlobImpl(BlobBuilder value) + { + BlobHandle nextHandle = BlobHandle.FromOffset(_blobHeapStartOffset + _blobBuilder.Count); + BlobHandle handle = _blobs.GetOrAdd(value, nextHandle); + + return handle; + } +#endif /// /// Adds specified blob to Blob heap, if it's not there already. @@ -240,7 +251,7 @@ public BlobHandle GetOrAddBlob(ImmutableArray value) Throw.ArgumentNull(nameof(value)); } - return GetOrAddBlob(value.AsSpan(), value); + return GetOrAddBlob(value.AsSpan()); } /// @@ -606,33 +617,11 @@ internal void WriteHeapsTo(BlobBuilder builder, BlobBuilder stringHeap) WriteAligned(stringHeap, builder); WriteAligned(_userStringBuilder, builder); WriteAligned(_guidBuilder, builder); - WriteAlignedBlobHeap(builder); - } - - private void WriteAlignedBlobHeap(BlobBuilder builder) - { - int alignment = BitArithmetic.Align(_blobHeapSize, 4) - _blobHeapSize; - - var writer = new BlobWriter(builder.ReserveBytes(_blobHeapSize + alignment)); - - // Perf consideration: With large heap the following loop may cause a lot of cache misses - // since the order of entries in _blobs dictionary depends on the hash of the array values, - // which is not correlated to the heap index. If we observe such issue we should order - // the entries by heap position before running this loop. - - int startOffset = _blobHeapStartOffset; - foreach (var entry in _blobs) - { - int heapOffset = entry.Value.Value.GetHeapOffset(); - var blob = entry.Value.Key; - - writer.Offset = (heapOffset == 0) ? 0 : heapOffset - startOffset; - writer.WriteCompressedInteger(blob.Length); - writer.WriteBytes(blob); - } - - writer.Offset = _blobHeapSize; - writer.WriteBytes(0, alignment); + // FinishWritingSegments will invalidate the segments in the dictionary, + // so we need to clear it first. + _blobs.Clear(); + _blobBuilder.FinishWritingSegments(); + WriteAligned(_blobBuilder, builder); } private static void WriteAligned(BlobBuilder source, BlobBuilder target) diff --git a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/Ecma335/MetadataBuilder.cs b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/Ecma335/MetadataBuilder.cs index c0c576cf806021..328905df294457 100644 --- a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/Ecma335/MetadataBuilder.cs +++ b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/Ecma335/MetadataBuilder.cs @@ -22,7 +22,7 @@ internal SerializedMetadata GetSerializedMetadata(ImmutableArray externalRo var heapSizes = ImmutableArray.Create( _userStringBuilder.Count, stringHeapBuilder.Count, - _blobHeapSize, + _blobBuilder.Count, _guidBuilder.Count); var sizes = new MetadataSizes(GetRowCounts(), externalRowCounts, heapSizes, metadataVersionByteCount, isStandaloneDebugMetadata);