In your implementation Unsafe.AreSame() simple checks the reference to the same memory location. It isn't comparing the the struct for equality as seems to be your intent (otherwise just use the built in generic overload for reference types).
Regarding Interlocked.MemoryBarrier(), even if it does guarantee a full fence your implementation does not achieve atomicity for reasons pointed out by @PeterCordes and I in the comments section below. In your example, two concurrent calls to your implementation of Exchange could return the same initial value, which would be incorrect.
GLOBAL STRUCT SUPPORT ALTERNATIVE to Interlocked W/O "LOCK"
The following is a quick and dirty example of a CompareExchange implementation with global struct support (i.e. no 8-byte length restriction) that's marginally (microseconds per op) slower than Interlocked.Exchange() in a head to head test of 1M long RMW operations.
Benchmark
The following are BenchmarkDotNet comparisons between Interlocked and the Atomic implementation below. All benchmarks are 1M iterations with 2 competing threads. InterLocked doesn't support types > 8-bytes, which is why there is no head-to-head comp for Guid.
- "InterLocked_..." -
InterLocked.CompareExchange
- "Atomic..." -
Atomic<T>.CompareExchange - implementation below
- "Lock..." -
Atomic<T>.CompareExchange - modified to use lock{...}
| Method |
Mean |
Error |
StdDev |
Ratio |
RatioSD |
| Interlocked_Long |
6.989 ms |
0.0541 ms |
0.0506 ms |
1.00 |
0.00 |
| Atomic_Long |
9.566 ms |
0.0858 ms |
0.0761 ms |
1.37 |
0.01 |
| Lock_Long |
19.020 ms |
0.0721 ms |
0.0563 ms |
2.72 |
0.02 |
| Atomic_Guid |
76.644 ms |
1.0858 ms |
1.1151 ms |
10.98 |
0.15 |
| Lock__Guid |
84.223 ms |
0.1813 ms |
0.1514 ms |
12.05 |
0.09 |
Implementation
[StructLayout(LayoutKind.Auto)]
public struct Atomic<T> where T : struct
{
private AtomicSpinWait _lock;
public Atomic() => _lock = new();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T CompareExchange(ref T current, T value, T compareand)
{
_lock.Acquire();
var sizeOf = Unsafe.SizeOf<T>();
if (!MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As<T, byte>(ref current), sizeOf).SequenceEqual(
MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As<T, byte>(ref compareand), sizeOf)))
current = value;
_lock.Release();
return current;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T Exchange(ref T location, T value)
{
_lock.Acquire();
location = value;
_lock.Release();
return location;
}
[StructLayout(LayoutKind.Auto)]
private struct AtomicSpinWait
{
private int _value;
public AtomicSpinWait() => _value = 0;
internal void Acquire()
{
for (var sw = new SpinWait(); CompareExchange(1, 0) == 1; sw.SpinOnce()) ;
}
internal void Release() => _value = 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int CompareExchange(int value, int comparand)
=> Interlocked.CompareExchange(ref _value, value, comparand);
}
}
Example Usage
public class AtomicExample
{
static long current = 0;
//Instantiate Atomic<T> w/ desired struct type param
Atomic<long> _lock = new();
public bool Example(long value, long comparand)
{
if (_lock.CompareExchange(ref current, value, comparand) == value)
return true; //current == comparand, current = value
return false; //current != comarand, current = current
}
}