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 6d42a53f7c5a7c..ad67c99d457252 100644 --- a/src/libraries/System.Reflection.Metadata/src/System.Reflection.Metadata.csproj +++ b/src/libraries/System.Reflection.Metadata/src/System.Reflection.Metadata.csproj @@ -45,7 +45,6 @@ The System.Reflection.Metadata library is built-in as part of the shared framewo - @@ -60,6 +59,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/ByteSequenceComparer.cs b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Internal/Utilities/ByteSequenceComparer.cs deleted file mode 100644 index 2ddd1806da2f3f..00000000000000 --- a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Internal/Utilities/ByteSequenceComparer.cs +++ /dev/null @@ -1,68 +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.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; - -namespace System.Reflection.Internal -{ - internal sealed class ByteSequenceComparer : IEqualityComparer, IEqualityComparer> - { - internal static readonly ByteSequenceComparer Instance = new ByteSequenceComparer(); - - private ByteSequenceComparer() - { - } - - internal static bool Equals(ImmutableArray x, ImmutableArray y) - { - return x.AsSpan().SequenceEqual(y.AsSpan()); - } - - internal static bool Equals(byte[] left, int leftStart, byte[] right, int rightStart, int length) - { - return left.AsSpan(leftStart, length).SequenceEqual(right.AsSpan(rightStart, length)); - } - - internal static bool Equals(byte[]? left, byte[]? right) - { - return left.AsSpan().SequenceEqual(right.AsSpan()); - } - - // Both hash computations below use the FNV-1a algorithm (http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function). - - internal static int GetHashCode(byte[] x) - { - Debug.Assert(x != null); - return Hash.GetFNVHashCode(x); - } - - internal static int GetHashCode(ImmutableArray x) - { - Debug.Assert(!x.IsDefault); - return Hash.GetFNVHashCode(x.AsSpan()); - } - - bool IEqualityComparer.Equals(byte[]? x, byte[]? y) - { - return Equals(x, y); - } - - int IEqualityComparer.GetHashCode(byte[] x) - { - return GetHashCode(x); - } - - bool IEqualityComparer>.Equals(ImmutableArray x, ImmutableArray y) - { - return Equals(x, y); - } - - int IEqualityComparer>.GetHashCode(ImmutableArray x) - { - return GetHashCode(x); - } - } -} 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 8debffc0e94761..98bda9ebddd8e7 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 @@ -50,6 +50,7 @@ public partial class BlobBuilder private bool IsHead => (_length & IsFrozenMask) == 0; private int Length => (int)(_length & ~IsFrozenMask); private uint FrozenLength => _length | IsFrozenMask; + private Span Span => _buffer.AsSpan(0, Length); public BlobBuilder(int capacity = DefaultChunkSize) { @@ -234,7 +235,7 @@ public bool ContentEquals(BlobBuilder other) var right = rightEnumerator.Current; int minLength = Math.Min(left.Length - leftStart, right.Length - rightStart); - if (!ByteSequenceComparer.Equals(left._buffer, leftStart, right._buffer, rightStart, minLength)) + if (!left._buffer.AsSpan(leftStart, minLength).SequenceEqual(right._buffer.AsSpan(rightStart, minLength))) { return false; } @@ -318,6 +319,19 @@ public ImmutableArray ToImmutableArray(int start, int byteCount) return ImmutableByteArrayInterop.DangerousCreateFromUnderlyingArray(ref 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) @@ -344,7 +358,7 @@ public void WriteContentTo(ref BlobWriter destination) foreach (var chunk in GetChunks()) { - destination.WriteBytes(chunk._buffer.AsSpan(0, chunk.Length)); + destination.WriteBytes(chunk.Span); } } @@ -359,7 +373,7 @@ public void WriteContentTo(BlobBuilder destination) foreach (var chunk in GetChunks()) { - destination.WriteBytes(chunk._buffer.AsSpan(0, chunk.Length)); + destination.WriteBytes(chunk.Span); } } diff --git a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/BlobWriter.cs b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/BlobWriter.cs index fdd73e33ba289a..a9bd1288111556 100644 --- a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/BlobWriter.cs +++ b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/BlobWriter.cs @@ -54,7 +54,7 @@ public BlobWriter(byte[] buffer, int start, int count) /// public bool ContentEquals(BlobWriter other) { - return Length == other.Length && ByteSequenceComparer.Equals(_buffer, _start, other._buffer, other._start, Length); + return Length == other.Length && _buffer.AsSpan(_start, Length).SequenceEqual(other._buffer.AsSpan(other._start, other.Length)); } public int Offset 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 new file mode 100644 index 00000000000000..0fa272a27062b1 --- /dev/null +++ b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/Ecma335/BlobDictionary.cs @@ -0,0 +1,109 @@ +// 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.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Reflection.Internal; +#if NET +using System.Runtime.InteropServices; +#endif + +namespace System.Reflection.Metadata.Ecma335 +{ + [DebuggerDisplay("Count = {Count}")] + internal readonly struct BlobDictionary + { + private readonly Dictionary, BlobHandle>> _dictionary; + + // 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) + { + int dictionaryKey = Hash.GetFNVHashCode(key); + while (true) + { + ref var entry = ref CollectionsMarshal.GetValueRefOrAddDefault(_dictionary, dictionaryKey, out exists); + if (!exists || entry.Key.AsSpan().SequenceEqual(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 + } + 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); + return value; + } +#else + public BlobHandle GetOrAdd(ReadOnlySpan key, ImmutableArray immutableKey, BlobHandle value, out bool exists) + { + int dictionarykey = Hash.GetFNVHashCode(key); + KeyValuePair, BlobHandle> entry; + while (true) + { + if (!(exists = _dictionary.TryGetValue(dictionarykey, out entry)) + || entry.Key.AsSpan().SequenceEqual(key)) + { + break; + } + dictionarykey = GetNextDictionaryKey(dictionarykey); + } + + 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)); + } + + _dictionary.Add(dictionarykey, new(immutableKey, value)); + return value; + } +#endif + + public BlobDictionary(int capacity = 0) + { + _dictionary = new(capacity); + } + + public int Count => _dictionary.Count; + + public Dictionary, BlobHandle>>.Enumerator GetEnumerator() => + _dictionary.GetEnumerator(); + } +} 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 a58bf9f1f7883d..142ea5f67c6f99 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 @@ -3,10 +3,8 @@ using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics; using System.Reflection.Internal; -using System.Runtime.CompilerServices; -using System.Text; +using System.Runtime.InteropServices; namespace System.Reflection.Metadata.Ecma335 { @@ -44,7 +42,7 @@ internal void SetCapacity(int capacity) private int _stringHeapCapacity = 4 * 1024; // #Blob heap - private readonly Dictionary, BlobHandle> _blobs = new Dictionary, BlobHandle>(1024, ByteSequenceComparer.Instance); + private readonly BlobDictionary _blobs = new BlobDictionary(1024); private readonly int _blobHeapStartOffset; private int _blobHeapSize; @@ -118,7 +116,7 @@ public MetadataBuilder( // beginning of the delta blob. _userStringBuilder.WriteByte(0); - _blobs.Add(ImmutableArray.Empty, default(BlobHandle)); + _blobs.GetOrAdd(ReadOnlySpan.Empty, ImmutableArray.Empty, default, out _); _blobHeapSize = 1; // When EnC delta is applied #US, #String and #Blob heaps are appended. @@ -193,7 +191,11 @@ public BlobHandle GetOrAddBlob(BlobBuilder value) Throw.ArgumentNull(nameof(value)); } - // TODO: avoid making a copy if the blob exists in the index + if (value.TryGetSpan(out ReadOnlySpan buffer)) + { + return GetOrAddBlob(buffer); + } + return GetOrAddBlob(value.ToImmutableArray()); } @@ -210,8 +212,19 @@ public BlobHandle GetOrAddBlob(byte[] value) Throw.ArgumentNull(nameof(value)); } - // TODO: avoid making a copy if the blob exists in the index - return GetOrAddBlob(ImmutableArray.Create(value)); + return GetOrAddBlob(new ReadOnlySpan(value)); + } + + private BlobHandle GetOrAddBlob(ReadOnlySpan value, ImmutableArray immutableValue = default) + { + 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; + } + + return handle; } /// @@ -227,16 +240,7 @@ public BlobHandle GetOrAddBlob(ImmutableArray value) Throw.ArgumentNull(nameof(value)); } - BlobHandle handle; - if (!_blobs.TryGetValue(value, out handle)) - { - handle = BlobHandle.FromOffset(_blobHeapStartOffset + _blobHeapSize); - _blobs.Add(value, handle); - - _blobHeapSize += BlobWriterImpl.GetCompressedIntegerSize(value.Length) + value.Length; - } - - return handle; + return GetOrAddBlob(value.AsSpan(), value); } /// @@ -267,6 +271,16 @@ public unsafe BlobHandle GetOrAddConstantBlob(object? value) /// is null. public BlobHandle GetOrAddBlobUTF16(string value) { + if (value is null) + { + Throw.ArgumentNull(nameof(value)); + } + + if (BitConverter.IsLittleEndian) + { + return GetOrAddBlob(MemoryMarshal.AsBytes(value.AsSpan())); + } + var builder = PooledBlobBuilder.GetInstance(); builder.WriteUTF16(value); var handle = GetOrAddBlob(builder); @@ -611,8 +625,8 @@ private void WriteAlignedBlobHeap(BlobBuilder builder) int startOffset = _blobHeapStartOffset; foreach (var entry in _blobs) { - int heapOffset = entry.Value.GetHeapOffset(); - var blob = entry.Key; + int heapOffset = entry.Value.Value.GetHeapOffset(); + var blob = entry.Value.Key; writer.Offset = (heapOffset == 0) ? 0 : heapOffset - startOffset; writer.WriteCompressedInteger(blob.Length); diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/BlobTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/BlobTests.cs index cefd6e109d4e3b..6c05f36d046c9f 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/BlobTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/BlobTests.cs @@ -61,7 +61,7 @@ private void TestContentEquals(byte[] left, byte[] right) var builder2 = new BlobBuilder(0); builder2.WriteBytes(right); - bool expected = ByteSequenceComparer.Equals(left, right); + bool expected = left.AsSpan().SequenceEqual(right.AsSpan()); Assert.Equal(expected, builder1.ContentEquals(builder2)); }