From 156e0f497308f9c5a9fbe9441eea612f8137e22c Mon Sep 17 00:00:00 2001 From: ikpil Date: Sat, 14 Dec 2024 23:25:06 +0900 Subject: [PATCH] struct RcRentedArray : IDispose -> ref struct RcRentedArray BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.2605) AMD Ryzen 5 3600, 1 CPU, 12 logical and 6 physical cores .NET SDK 9.0.100 [Host] : .NET 9.0.0 (9.0.24.52809), X64 RyuJIT AVX2 DefaultJob : .NET 9.0.0 (9.0.24.52809), X64 RyuJIT AVX2 | Method | HashTableSize | Mean | Error | StdDev | Median | Gen0 | Allocated | |----------- |-------------- |-------------:|-----------:|-----------:|-------------:|-------:|----------:| | New | 16 | 5.842 ns | 0.0500 ns | 0.0443 ns | 5.849 ns | 0.0182 | 152 B | | Stackalloc | 16 | 4.142 ns | 0.0831 ns | 0.0777 ns | 4.112 ns | - | - | | Rent | 16 | 16.409 ns | 0.0244 ns | 0.0204 ns | 16.399 ns | - | - | | New | 256 | 50.550 ns | 1.0255 ns | 2.8245 ns | 49.036 ns | 0.2477 | 2072 B | | Stackalloc | 256 | 65.315 ns | 0.2746 ns | 0.2293 ns | 65.259 ns | - | - | | Rent | 256 | 39.722 ns | 0.0638 ns | 0.0597 ns | 39.734 ns | - | - | | New | 1024 | 285.897 ns | 22.9065 ns | 67.5402 ns | 303.147 ns | 0.9813 | 8216 B | | Stackalloc | 1024 | 261.509 ns | 0.3847 ns | 0.3410 ns | 261.528 ns | - | - | | Rent | 1024 | 87.780 ns | 0.1627 ns | 0.1359 ns | 87.752 ns | - | - | | New | 8192 | 1,156.367 ns | 9.6633 ns | 9.0390 ns | 1,156.284 ns | 7.8125 | 65560 B | | Stackalloc | 8192 | 2,134.754 ns | 5.4929 ns | 4.8693 ns | 2,134.541 ns | - | - | | Rent | 8192 | 582.443 ns | 1.0532 ns | 0.9336 ns | 582.631 ns | - | - | --- src/DotRecast.Core/Buffers/RcRentedArray.cs | 119 +++++------------- test/DotRecast.Benchmark/BenchmarkProgram.cs | 6 +- .../Benchmarks/ArrayBenchmarks.cs | 82 ++++++++++++ .../Benchmarks/StackallocBenchmarks.cs | 71 ----------- .../RcArrayBenchmarkTests.cs | 6 +- test/DotRecast.Core.Test/RcRentedArrayTest.cs | 51 ++------ 6 files changed, 132 insertions(+), 203 deletions(-) create mode 100644 test/DotRecast.Benchmark/Benchmarks/ArrayBenchmarks.cs delete mode 100644 test/DotRecast.Benchmark/Benchmarks/StackallocBenchmarks.cs diff --git a/src/DotRecast.Core/Buffers/RcRentedArray.cs b/src/DotRecast.Core/Buffers/RcRentedArray.cs index 9c1c8ec2..957fcf78 100644 --- a/src/DotRecast.Core/Buffers/RcRentedArray.cs +++ b/src/DotRecast.Core/Buffers/RcRentedArray.cs @@ -6,107 +6,50 @@ namespace DotRecast.Core.Buffers { - public readonly struct RcRentIdGen + public class RcRentedArray { - public readonly int Id; - public readonly int Gen; + public static readonly RcRentedArray Shared = new RcRentedArray(); - public RcRentIdGen(int id, int gen) + private RcRentedArray() { - Id = id; - Gen = gen; - } - } - - internal sealed class RcRentIdPool - { - private int[] _generations; - private readonly Queue _freeIds; - private int _maxId; - - public RcRentIdPool(int capacity) - { - _generations = new int[capacity]; - _freeIds = new Queue(capacity); } - internal RcRentIdGen AcquireId() + public RcRentedArray Rent(int minimumLength) { - if (!_freeIds.TryDequeue(out int id)) - { - id = _maxId++; - if (_generations.Length <= id) - { - Array.Resize(ref _generations, _generations.Length << 1); - } - } - - return new RcRentIdGen(id, _generations[id]); + return new RcRentedArray(minimumLength); } - internal void ReturnId(int id) + public void Return(RcRentedArray array) { - _generations[id]++; - _freeIds.Enqueue(id); - } + if (array.IsDisposed) + return; - internal int GetGeneration(int id) - { - return _generations.Length <= id ? 0 : _generations[id]; + array.Dispose(); } } - public static class RcRentedArray + public ref struct RcRentedArray { - public const int START_RENT_ID_POOL_CAPACITY = 16; - private static readonly ThreadLocal _rentPool = new ThreadLocal(() => new RcRentIdPool(START_RENT_ID_POOL_CAPACITY)); - - public static RcRentedArray Rent(int minimumLength) - { - var array = ArrayPool.Shared.Rent(minimumLength); - return new RcRentedArray(ArrayPool.Shared, _rentPool.Value.AcquireId(), array, minimumLength); - } + private readonly T[] _items; + public readonly int Length; + private bool _disposed; - internal static bool IsDisposed(RcRentIdGen rentIdGen) - { - return _rentPool.Value.GetGeneration(rentIdGen.Id) != rentIdGen.Gen; - } + public bool IsDisposed => _disposed; - internal static void ReturnId(RcRentIdGen rentIdGen) + internal RcRentedArray(int length) { - _rentPool.Value.ReturnId(rentIdGen.Id); - } - } - - - - public struct RcRentedArray : IDisposable - { - private ArrayPool _owner; - private T[] _array; - private readonly RcRentIdGen _rentIdGen; - - public int Length { get; } - public bool IsDisposed => null == _owner || null == _array || RcRentedArray.IsDisposed(_rentIdGen); - - internal RcRentedArray(ArrayPool owner, RcRentIdGen rentIdGen, T[] array, int length) - { - _owner = owner; - _array = array; Length = length; - _rentIdGen = rentIdGen; + _items = ArrayPool.Shared.Rent(length); + _disposed = false; } public void Dispose() { - if (null != _owner && null != _array && !RcRentedArray.IsDisposed(_rentIdGen)) - { - RcRentedArray.ReturnId(_rentIdGen); - _owner.Return(_array, true); - } + if (_disposed) + return; - _owner = null; - _array = null; + _disposed = true; + ArrayPool.Shared.Return(_items, true); } public ref T this[int index] @@ -114,22 +57,22 @@ public ref T this[int index] [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - RcThrowHelper.ThrowExceptionIfIndexOutOfRange(index, Length); - if (IsDisposed) - throw new NullReferenceException(); - return ref _array[index]; - } - } + if (0 > index || Length <= index) + RcThrowHelper.ThrowExceptionIfIndexOutOfRange(index, Length); + if (_disposed) + RcThrowHelper.ThrowNullReferenceException("already disposed"); - public T[] AsArray() - { - return _array; + return ref _items[index]; + } } public Span AsSpan() { - return new Span(_array, 0, Length); + if (_disposed) + RcThrowHelper.ThrowNullReferenceException("already disposed"); + + return new Span(_items, 0, Length); } } } \ No newline at end of file diff --git a/test/DotRecast.Benchmark/BenchmarkProgram.cs b/test/DotRecast.Benchmark/BenchmarkProgram.cs index 8f4c0578..684bb42e 100644 --- a/test/DotRecast.Benchmark/BenchmarkProgram.cs +++ b/test/DotRecast.Benchmark/BenchmarkProgram.cs @@ -10,9 +10,9 @@ public static class BenchmarkProgram public static int Main(string[] args) { var runs = ImmutableArray.Create( - BenchmarkConverter.TypeToBenchmarks(typeof(VectorBenchmarks)), - BenchmarkConverter.TypeToBenchmarks(typeof(PriorityQueueBenchmarks)), - BenchmarkConverter.TypeToBenchmarks(typeof(StackallocBenchmarks)) + // BenchmarkConverter.TypeToBenchmarks(typeof(VectorBenchmarks)), + // BenchmarkConverter.TypeToBenchmarks(typeof(PriorityQueueBenchmarks)), + BenchmarkConverter.TypeToBenchmarks(typeof(ArrayBenchmarks)) ); var summary = BenchmarkRunner.Run(runs.ToArray()); diff --git a/test/DotRecast.Benchmark/Benchmarks/ArrayBenchmarks.cs b/test/DotRecast.Benchmark/Benchmarks/ArrayBenchmarks.cs new file mode 100644 index 00000000..dbc640b1 --- /dev/null +++ b/test/DotRecast.Benchmark/Benchmarks/ArrayBenchmarks.cs @@ -0,0 +1,82 @@ +using System; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Engines; +using DotRecast.Core.Buffers; + +namespace DotRecast.Benchmark.Benchmarks; + +/* + +BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.2605) +AMD Ryzen 5 3600, 1 CPU, 12 logical and 6 physical cores +.NET SDK 9.0.100 + [Host] : .NET 9.0.0 (9.0.24.52809), X64 RyuJIT AVX2 + DefaultJob : .NET 9.0.0 (9.0.24.52809), X64 RyuJIT AVX2 + + +| Method | HashTableSize | Mean | Error | StdDev | Median | Gen0 | Allocated | +|----------- |-------------- |-------------:|-----------:|-----------:|-------------:|-------:|----------:| +| New | 16 | 5.842 ns | 0.0500 ns | 0.0443 ns | 5.849 ns | 0.0182 | 152 B | +| Stackalloc | 16 | 4.142 ns | 0.0831 ns | 0.0777 ns | 4.112 ns | - | - | +| Rent | 16 | 16.409 ns | 0.0244 ns | 0.0204 ns | 16.399 ns | - | - | +| New | 256 | 50.550 ns | 1.0255 ns | 2.8245 ns | 49.036 ns | 0.2477 | 2072 B | +| Stackalloc | 256 | 65.315 ns | 0.2746 ns | 0.2293 ns | 65.259 ns | - | - | +| Rent | 256 | 39.722 ns | 0.0638 ns | 0.0597 ns | 39.734 ns | - | - | +| New | 1024 | 285.897 ns | 22.9065 ns | 67.5402 ns | 303.147 ns | 0.9813 | 8216 B | +| Stackalloc | 1024 | 261.509 ns | 0.3847 ns | 0.3410 ns | 261.528 ns | - | - | +| Rent | 1024 | 87.780 ns | 0.1627 ns | 0.1359 ns | 87.752 ns | - | - | +| New | 8192 | 1,156.367 ns | 9.6633 ns | 9.0390 ns | 1,156.284 ns | 7.8125 | 65560 B | +| Stackalloc | 8192 | 2,134.754 ns | 5.4929 ns | 4.8693 ns | 2,134.541 ns | - | - | +| Rent | 8192 | 582.443 ns | 1.0532 ns | 0.9336 ns | 582.631 ns | - | - | + +*/ + +[MemoryDiagnoser] +public class ArrayBenchmarks +{ + private readonly Consumer _consumer = new(); + + [Params(1 << 4, 1 << 8, 1 << 10, 1 << 13)] + public int HashTableSize; + + [Benchmark] + public void New() + { + Span hashTable = new long[HashTableSize]; + _consumer.Consume(hashTable[0]); + } + + [Benchmark] + public void Stackalloc() + { + Span hashTable = stackalloc long[HashTableSize]; + _consumer.Consume(hashTable[0]); + } + + [Benchmark] + public void Rent() + { + using var hashTable = RcRentedArray.Shared.Rent(HashTableSize); + _consumer.Consume(hashTable[0]); + } + + + // [Benchmark] + // [SkipLocalsInit] + // public void Stackalloc_Long_SkipLocalsInit() + // { + // Span hashTable = stackalloc long[HashTableSize]; + // + // _consumer.Consume(hashTable[0]); + // } + + + // [Benchmark] + // [SkipLocalsInit] + // public void New_Long_SkipLocalsInit() + // { + // Span hashTable = new long[HashTableSize]; + // + // _consumer.Consume(hashTable[0]); + // } +} \ No newline at end of file diff --git a/test/DotRecast.Benchmark/Benchmarks/StackallocBenchmarks.cs b/test/DotRecast.Benchmark/Benchmarks/StackallocBenchmarks.cs deleted file mode 100644 index bc542e4e..00000000 --- a/test/DotRecast.Benchmark/Benchmarks/StackallocBenchmarks.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Engines; - -namespace DotRecast.Benchmark.Benchmarks; - -/* - -| Method | TestArraySize | Mean | Error | StdDev | Median | -|------------------------------- |-------------- |-------------:|-----------:|------------:|-------------:| -| Stackalloc_Long | 16 | 3.016 ns | 0.0179 ns | 0.0149 ns | 3.017 ns | -| Stackalloc_Long_SkipLocalsInit | 16 | 2.265 ns | 0.0197 ns | 0.0184 ns | 2.258 ns | -| New_Long | 16 | 5.917 ns | 0.1964 ns | 0.5634 ns | 5.761 ns | -| New_Long_SkipLocalsInit | 16 | 5.703 ns | 0.1371 ns | 0.3935 ns | 5.661 ns | -| Stackalloc_Long | 256 | 39.418 ns | 0.2737 ns | 0.2285 ns | 39.410 ns | -| Stackalloc_Long_SkipLocalsInit | 256 | 2.274 ns | 0.0147 ns | 0.0131 ns | 2.274 ns | -| New_Long | 256 | 53.901 ns | 2.9999 ns | 8.4614 ns | 51.449 ns | -| New_Long_SkipLocalsInit | 256 | 53.480 ns | 1.8716 ns | 5.4298 ns | 51.858 ns | -| Stackalloc_Long | 1024 | 137.037 ns | 0.3652 ns | 0.3416 ns | 137.031 ns | -| Stackalloc_Long_SkipLocalsInit | 1024 | 3.669 ns | 0.0254 ns | 0.0226 ns | 3.668 ns | -| New_Long | 1024 | 197.324 ns | 9.2795 ns | 27.0687 ns | 186.588 ns | -| New_Long_SkipLocalsInit | 1024 | 210.996 ns | 10.0255 ns | 27.9471 ns | 206.110 ns | -| Stackalloc_Long | 8192 | 1,897.989 ns | 7.1814 ns | 5.9968 ns | 1,897.814 ns | -| Stackalloc_Long_SkipLocalsInit | 8192 | 20.598 ns | 0.2645 ns | 0.2344 ns | 20.572 ns | -| New_Long | 8192 | 1,324.061 ns | 39.8447 ns | 116.2288 ns | 1,298.794 ns | -| New_Long_SkipLocalsInit | 8192 | 1,305.211 ns | 35.1855 ns | 102.0796 ns | 1,295.539 ns | -*/ - -public class StackallocBenchmarks -{ - private readonly Consumer _consumer = new(); - - [Params(1 << 4, 1 << 8, 1 << 10, 1 << 13)] - public int HashTableSize; - - [Benchmark] - public void Stackalloc_Long() - { - Span hashTable = stackalloc long[HashTableSize]; - - _consumer.Consume(hashTable[0]); - } - - [Benchmark] - [SkipLocalsInit] - public void Stackalloc_Long_SkipLocalsInit() - { - Span hashTable = stackalloc long[HashTableSize]; - - _consumer.Consume(hashTable[0]); - } - - [Benchmark] - public void New_Long() - { - Span hashTable = new long[HashTableSize]; - - _consumer.Consume(hashTable[0]); - } - - - [Benchmark] - [SkipLocalsInit] - public void New_Long_SkipLocalsInit() - { - Span hashTable = new long[HashTableSize]; - - _consumer.Consume(hashTable[0]); - } -} \ No newline at end of file diff --git a/test/DotRecast.Core.Test/RcArrayBenchmarkTests.cs b/test/DotRecast.Core.Test/RcArrayBenchmarkTests.cs index 38badaee..dfbb0f3f 100644 --- a/test/DotRecast.Core.Test/RcArrayBenchmarkTests.cs +++ b/test/DotRecast.Core.Test/RcArrayBenchmarkTests.cs @@ -54,8 +54,8 @@ private void RoundForPureRentArray(int len) private void RoundForRcRentedArray(int len) { - using var rentedArray = RcRentedArray.Rent(len); - var array = rentedArray.AsArray(); + using var rentedArray = RcRentedArray.Shared.Rent(len); + var array = rentedArray.AsSpan(); for (int i = 0; i < rentedArray.Length; ++i) { array[i] = _rand.NextInt32(); @@ -88,7 +88,7 @@ public void TestBenchmarkArrays() var results = new List<(string title, long ticks)>(); results.Add(Bench("new int[len]", RoundForArray)); results.Add(Bench("ArrayPool.Shared.Rent(len)", RoundForPureRentArray)); - results.Add(Bench("RcRentedArray.Rent(len)", RoundForRcRentedArray)); + results.Add(Bench("RcRentedArray.Shared.Rent(len)", RoundForRcRentedArray)); results.Add(Bench("new RcStackArray512()", RoundForRcStackArray)); results.Add(Bench("stackalloc int[len]", RoundForStackalloc)); diff --git a/test/DotRecast.Core.Test/RcRentedArrayTest.cs b/test/DotRecast.Core.Test/RcRentedArrayTest.cs index 9372d72a..93a1757c 100644 --- a/test/DotRecast.Core.Test/RcRentedArrayTest.cs +++ b/test/DotRecast.Core.Test/RcRentedArrayTest.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using DotRecast.Core.Buffers; using NUnit.Framework; @@ -32,7 +31,8 @@ public void TestRentedArray() { int length = Math.Max(2, (int)(rand.Next() * 2048)); var values = RandomValues(length); - using var array = RcRentedArray.Rent(length); + using var array = RcRentedArray.Shared.Rent(length); + using var array2 = RcRentedArray.Shared.Rent(length); for (int i = 0; i < array.Length; ++i) { @@ -44,17 +44,8 @@ public void TestRentedArray() Assert.That(array[i], Is.EqualTo(values[i])); } - Assert.That(array[^1], Is.EqualTo(values[^1])); - - Assert.Throws(() => array[-1] = 0); - Assert.Throws(() => array[array.Length + 1] = 0); - Assert.Throws(() => _ = array[-1]); - Assert.Throws(() => _ = array[array.Length + 1]); - - // danger - rentedArray = array; + Assert.That(array[array.Length - 1], Is.EqualTo(values[^1])); } - Assert.Throws(() => rentedArray[^1] = 0); } } @@ -63,36 +54,36 @@ public void TestSame() { // not same { - using var r1 = RcRentedArray.Rent(1024); - using var r2 = RcRentedArray.Rent(1024); + using var r1 = RcRentedArray.Shared.Rent(1024); + using var r2 = RcRentedArray.Shared.Rent(1024); - Assert.That(r2.AsArray() != r1.AsArray(), Is.EqualTo(true)); + Assert.That(r2.AsSpan() != r1.AsSpan(), Is.EqualTo(true)); } // same { // error case - float[] r1Array; - using (var r1 = RcRentedArray.Rent(1024)) + Span r1Array; + { - r1Array = r1.AsArray(); + using var r1 = RcRentedArray.Shared.Rent(1024); + r1Array = r1.AsSpan(); for (int i = 0; i < r1.Length; ++i) { r1[i] = 123; } } - using var r2 = RcRentedArray.Rent(1024); + using var r2 = RcRentedArray.Shared.Rent(1024); - Assert.That(r2.AsArray() == r1Array, Is.EqualTo(true)); - Assert.That(r2.AsArray().Sum(), Is.EqualTo(0)); + Assert.That(r2.AsSpan() == r1Array, Is.EqualTo(true)); } } [Test] public void TestDispose() { - var r1 = RcRentedArray.Rent(1024); + using var r1 = RcRentedArray.Shared.Rent(1024); for (int i = 0; i < r1.Length; ++i) { r1[i] = 123; @@ -101,21 +92,5 @@ public void TestDispose() Assert.That(r1.IsDisposed, Is.EqualTo(false)); r1.Dispose(); Assert.That(r1.IsDisposed, Is.EqualTo(true)); - Assert.That(r1.AsArray(), Is.Null); - } - - [Test] - public void TestIdPoolCapacity() - { - var buffer = new RcRentedArray[RcRentedArray.START_RENT_ID_POOL_CAPACITY + 2]; - for (int i = 0; i < buffer.Length; i++) - { - Assert.DoesNotThrow(() => buffer[i] = RcRentedArray.Rent(4)); - } - - for (int i = 0; i < buffer.Length; i++) - { - Assert.DoesNotThrow(() => buffer[i].Dispose()); - } } } \ No newline at end of file