Skip to content

Instantly share code, notes, and snippets.

@cathei
Last active December 28, 2022 00:09
Show Gist options
  • Save cathei/955a8cc160a8ea97b39db8d7a07ff69b to your computer and use it in GitHub Desktop.
Save cathei/955a8cc160a8ea97b39db8d7a07ff69b to your computer and use it in GitHub Desktop.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using BenchmarkDotNet.Attributes;
[MemoryDiagnoser]
public unsafe class UnsafeArray
{
private const int Count = 100_000_000;
private int* arrayPtr;
private NB<int> nb;
[StructLayout(LayoutKind.Sequential, Size = 1)]
public struct TestThreadSafety : IDisposable
{
public void Dispose()
{
}
}
[StructLayout(LayoutKind.Sequential, Size = 1)]
public readonly struct Sentinel
{
public TestThreadSafety TestThreadSafety() => new TestThreadSafety();
}
public struct NB<T> where T : unmanaged
{
private readonly uint _capacity;
private readonly IntPtr _ptr;
private readonly Sentinel _threadSentinel;
private static readonly int SIZE = Unsafe.SizeOf<T>();
public NB(IntPtr array, uint capacity)
: this()
{
this._ptr = array;
this._capacity = capacity;
this._threadSentinel = new();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe ref T GetFixed(int index)
{
// using (this._threadSentinel.TestThreadSafety())
return ref Unsafe.AsRef<T>((void*)(this._ptr + (int)index * Unsafe.SizeOf<T>()));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe ref T GetWithSize(int index)
{
// using (this._threadSentinel.TestThreadSafety())
return ref Unsafe.AsRef<T>((void*)(this._ptr + (int)index * SIZE));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe ref T GetWithSentinel(int index)
{
using (this._threadSentinel.TestThreadSafety())
return ref Unsafe.AsRef<T>((void*)(this._ptr + (int)index * Unsafe.SizeOf<T>()));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe ref T GetWithSentinelAndSize(int index)
{
using (this._threadSentinel.TestThreadSafety())
return ref Unsafe.AsRef<T>((void*)(this._ptr + (int)index * SIZE));
}
}
[GlobalSetup]
public void GlobalSetup()
{
arrayPtr = (int*)Marshal.AllocHGlobal(Count * sizeof(int));
for (int i = 0; i < Count; ++i)
arrayPtr[i] = i;
nb = new((IntPtr)arrayPtr, Count);
}
[GlobalCleanup]
public void GlobalCleanup()
{
Marshal.FreeHGlobal((IntPtr)arrayPtr);
}
[Benchmark]
public int PointerAccess()
{
int result = 0;
for (int i = 0; i < Count; ++i)
{
result += arrayPtr[i];
}
return result;
}
[Benchmark]
public int NBFixed()
{
int result = 0;
for (int i = 0; i < Count; ++i)
{
result += nb.GetFixed(i);
}
return result;
}
[Benchmark]
public int NBWithSize()
{
int result = 0;
for (int i = 0; i < Count; ++i)
{
result += nb.GetWithSize(i);
}
return result;
}
[Benchmark]
public int NBWithSentinel()
{
int result = 0;
for (int i = 0; i < Count; ++i)
{
result += nb.GetWithSentinel(i);
}
return result;
}
[Benchmark(Baseline = true)]
public int NBWithSentinelAndSize()
{
int result = 0;
for (int i = 0; i < Count; ++i)
{
result += nb.GetWithSentinelAndSize(i);
}
return result;
}
}
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using NUnit.Framework;
using Unity.Collections.LowLevel.Unsafe;
using Unity.PerformanceTesting;
public unsafe class UnsafeBenchmark
{
private const int Count = 1_000_000;
private int* arrayPtr;
private NB<int> nb;
private static int sink;
[StructLayout(LayoutKind.Sequential, Size = 1)]
public struct TestThreadSafety : IDisposable
{
public void Dispose()
{
}
}
[StructLayout(LayoutKind.Sequential, Size = 1)]
public readonly struct Sentinel
{
public TestThreadSafety TestThreadSafety() => new TestThreadSafety();
}
public struct NB<T> where T : unmanaged
{
private readonly uint _capacity;
private readonly IntPtr _ptr;
private readonly Sentinel _threadSentinel;
private static readonly int SIZE = Unsafe.SizeOf<T>();
public NB(IntPtr array, uint capacity)
: this()
{
this._ptr = array;
this._capacity = capacity;
this._threadSentinel = new();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref T SystemUnsafe(int index)
{
// using (this._threadSentinel.TestThreadSafety())
return ref Unsafe.AsRef<T>((void*)(this._ptr + (int)index * Unsafe.SizeOf<T>()));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref T UnityUnsafe(int index)
{
return ref UnsafeUtility.AsRef<T>((void*)(this._ptr + (int)index * UnsafeUtility.SizeOf<T>()));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref T WithSentinelAndSize(int index)
{
using (this._threadSentinel.TestThreadSafety())
return ref Unsafe.AsRef<T>((void*)(this._ptr + (int)index * SIZE));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref T WithSize(int index)
{
return ref Unsafe.AsRef<T>((void*)(this._ptr + (int)index * SIZE));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref T WithSentinel(int index)
{
using (this._threadSentinel.TestThreadSafety())
return ref Unsafe.AsRef<T>((void*)(this._ptr + (int)index * Unsafe.SizeOf<T>()));
}
}
[OneTimeSetUp]
public void GlobalSetup()
{
arrayPtr = (int*)Marshal.AllocHGlobal(Count * sizeof(int));
for (int i = 0; i < Count; ++i)
arrayPtr[i] = i;
nb = new((IntPtr)arrayPtr, Count);
}
[OneTimeTearDown]
public void GlobalCleanup()
{
Marshal.FreeHGlobal((IntPtr)arrayPtr);
}
[Test, Performance]
public void AccessPointerDirectly()
{
Measure.Method(() =>
{
int result = 0;
for (int i = 0; i < Count; ++i)
{
result += arrayPtr[i];
}
sink = result;
}).MeasurementCount(1000).Run();
}
[Test, Performance]
public void AccessWithSystemUnsafe()
{
Measure.Method(() =>
{
int result = 0;
for (int i = 0; i < Count; ++i)
{
result += nb.SystemUnsafe(i);
}
sink = result;
}).MeasurementCount(1000).Run();
}
[Test, Performance]
public void AccessWithUnityUnsafe()
{
Measure.Method(() =>
{
int result = 0;
for (int i = 0; i < Count; ++i)
{
result += nb.UnityUnsafe(i);
}
sink = result;
}).MeasurementCount(1000).Run();
}
[Test, Performance]
public void AccessWithSentinelAndSize()
{
Measure.Method(() =>
{
int result = 0;
for (int i = 0; i < Count; ++i)
{
result += nb.WithSentinelAndSize(i);
}
sink = result;
}).MeasurementCount(1000).Run();
}
[Test, Performance]
public void AccessWithSentinel()
{
Measure.Method(() =>
{
int result = 0;
for (int i = 0; i < Count; ++i)
{
result += nb.WithSentinel(i);
}
sink = result;
}).MeasurementCount(1000).Run();
}
[Test, Performance]
public void AccessWithSize()
{
Measure.Method(() =>
{
int result = 0;
for (int i = 0; i < Count; ++i)
{
result += nb.WithSize(i);
}
sink = result;
}).MeasurementCount(1000).Run();
}
}

Results (.NET 6.0)

BenchmarkDotNet=v0.13.2, OS=macOS Monterey 12.3 (21E230) [Darwin 21.4.0]
Apple M1 Pro, 1 CPU, 10 logical and 10 physical cores
.NET SDK=7.0.101
  [Host]     : .NET 6.0.2 (6.0.222.6406), Arm64 RyuJIT AdvSIMD
  DefaultJob : .NET 6.0.2 (6.0.222.6406), Arm64 RyuJIT AdvSIMD

Method Mean Error StdDev Ratio Allocated Alloc Ratio
PointerAccess 57.97 ms 0.772 ms 0.722 ms 0.46 154 B 0.23
NBFixed 59.77 ms 0.258 ms 0.215 ms 0.47 75 B 0.11
NBWithSize 240.70 ms 0.737 ms 0.689 ms 1.90 893 B 1.33
NBWithSentinel 127.20 ms 0.262 ms 0.233 ms 1.00 168 B 0.25
NBWithSentinelAndSize 126.93 ms 0.148 ms 0.123 ms 1.00 670 B 1.00

Results (.NET 7.0)

BenchmarkDotNet=v0.13.2, OS=macOS Monterey 12.3 (21E230) [Darwin 21.4.0]
Apple M1 Pro, 1 CPU, 10 logical and 10 physical cores
.NET SDK=7.0.101
  [Host]     : .NET 7.0.1 (7.0.122.56804), Arm64 RyuJIT AdvSIMD
  DefaultJob : .NET 7.0.1 (7.0.122.56804), Arm64 RyuJIT AdvSIMD

Method Mean Error StdDev Ratio Allocated Alloc Ratio
PointerAccess 41.42 ms 0.174 ms 0.154 ms 0.39 63 B 0.39
NBFixed 64.07 ms 0.209 ms 0.186 ms 0.61 102 B 0.63
NBWithSize 63.77 ms 0.284 ms 0.252 ms 0.60 102 B 0.63
NBWithSentinel 104.91 ms 0.344 ms 0.288 ms 0.99 163 B 1.00
NBWithSentinelAndSize 105.58 ms 0.339 ms 0.265 ms 1.00 163 B 1.00

Results (Mono)

BenchmarkDotNet=v0.13.2, OS=macOS Monterey 12.3 (21E230) [Darwin 21.4.0]
Apple M1 Pro 2.40GHz, 1 CPU, 10 logical and 10 physical cores
  [Host]     : Mono 6.12.0.162 (2020-02/2ca650f1f62 Tue), X64 VectorSize=128
  Job-CKTNGU : Mono 6.12.0.162 (2020-02/2ca650f1f62 Tue), X64 VectorSize=128

Runtime=Mono  
Method Mean Error StdDev Ratio Allocated Alloc Ratio
PointerAccess 72.67 ms 0.657 ms 0.614 ms 0.16 - NA
NBFixed 239.03 ms 1.065 ms 0.996 ms 0.52 - NA
NBWithSize 468.89 ms 0.411 ms 0.344 ms 1.02 - NA
NBWithSentinel 720.25 ms 1.590 ms 1.488 ms 1.56 - NA
NBWithSentinelAndSize 460.97 ms 2.005 ms 1.876 ms 1.00 - NA
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment