From f6687f9d7d89e37ce9c85a0bbf1bba6bc3c0473d Mon Sep 17 00:00:00 2001 From: "Ilya.Usov" Date: Mon, 16 Feb 2026 21:28:51 +0100 Subject: [PATCH 1/4] Implement Memory.IsReadWriteAtomic --- rd-net/Lifetimes/Util/Memory.cs | 80 ++++ rd-net/Test.Lifetimes/Utils/MemoryTest.cs | 460 ++++++++++++++++++++++ 2 files changed, 540 insertions(+) create mode 100644 rd-net/Test.Lifetimes/Utils/MemoryTest.cs diff --git a/rd-net/Lifetimes/Util/Memory.cs b/rd-net/Lifetimes/Util/Memory.cs index 59d272f42..911309f9d 100644 --- a/rd-net/Lifetimes/Util/Memory.cs +++ b/rd-net/Lifetimes/Util/Memory.cs @@ -1,4 +1,7 @@ +using System; +using System.Reflection; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Threading; namespace JetBrains.Util.Internal @@ -94,5 +97,82 @@ public static void VolatileWrite(ref bool location, bool value) { Volatile.Write(ref location, value); } + + public static bool IsReadWriteAtomic() + { + return IsReadWriteAtomicCache.IsReadWriteAtomic; + } + + private static int MaxAtomicSize = ComputeMaxAtomicSize(); + private static int NonAtomicSize = MaxAtomicSize + 1; + + private static int ComputeMaxAtomicSize() + { + if (Environment.Is64BitOperatingSystem) + { + return sizeof(long); + } + + return IntPtr.Size; + } + + private static class IsReadWriteAtomicCache + { + public static readonly bool IsReadWriteAtomic = Compute(); + + private static bool Compute() + { + var size = GetSize(); + return size <= MaxAtomicSize; + } + + private static int GetSize() + { +#if NET5_0_OR_GREATER + return Unsafe.SizeOf(); +#else + try + { + return ComputeApproximateSize(typeof(T)); + } + catch + { + // just in case something went wrong + return NonAtomicSize; + } +#endif + } + + private static int ComputeApproximateSize(Type type) + { + if (!type.IsValueType) + { + return IntPtr.Size; + } + + if (type.IsPrimitive || type.IsPointer) + { + return Marshal.SizeOf(type); + } + + if (type.IsEnum) + { + var underlyingType = Enum.GetUnderlyingType(type); + return Marshal.SizeOf(underlyingType); + } + + var totalSize = 0; + var types = type.GetRuntimeFields(); + foreach (var fieldInfo in types) + { + if (fieldInfo.IsStatic) continue; + + totalSize += ComputeApproximateSize(fieldInfo.FieldType); + if (totalSize > MaxAtomicSize) return NonAtomicSize; + } + + return totalSize; + } + } } } \ No newline at end of file diff --git a/rd-net/Test.Lifetimes/Utils/MemoryTest.cs b/rd-net/Test.Lifetimes/Utils/MemoryTest.cs new file mode 100644 index 000000000..9ff56460c --- /dev/null +++ b/rd-net/Test.Lifetimes/Utils/MemoryTest.cs @@ -0,0 +1,460 @@ +using System; +using JetBrains.Util.Internal; +using NUnit.Framework; + +namespace Test.Lifetimes.Utils +{ + public class MemoryTest + { + // Helper to determine expected atomic size threshold + // This matches the logic in Memory.ComputeMaxAtomicSize() + private static int MaxAtomicSize + { + get + { + if (Environment.Is64BitOperatingSystem) + { + return sizeof(long); + } + return IntPtr.Size; + } + } + + #region Test Types + + private enum ByteEnum : byte { Value = 1 } + private enum IntEnum : int { Value = 1 } + private enum LongEnum : long { Value = 1 } + + private struct SmallStruct + { + public int Value; + } + + private struct TwoIntStruct + { + public int A; + public int B; + } + + private struct LargeStruct + { + public long A; + public long B; + public long C; + } + + private struct StructWithReference + { + public object Ref; + public int Value; + } + + private struct StructWithString + { + public string Text; + public int Length; + } + + private struct StructWithNullableInt + { + public int? NullableValue; + } + + private struct StructWithNullableLong + { + public long? NullableValue; + } + + private struct NestedSmallStruct + { + public SmallStruct Inner; + } + + private struct NestedLargeStruct + { + public TwoIntStruct Inner; + public int Extra; + } + + private struct MixedStruct + { + public object Ref; + public int IntValue; + public byte ByteValue; + } + + private class SampleClass + { + public int Value; + } + + #endregion + + #region Primitive Types + + [Test] + public void IsReadWriteAtomic_Byte_ReturnsTrue() + { + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_SByte_ReturnsTrue() + { + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_Short_ReturnsTrue() + { + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_UShort_ReturnsTrue() + { + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_Int_ReturnsTrue() + { + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_UInt_ReturnsTrue() + { + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_Long_ReturnsTrue() + { + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_ULong_ReturnsTrue() + { + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_Float_ReturnsTrue() + { + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_Double_ReturnsTrue() + { + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_Bool_ReturnsTrue() + { + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_Char_ReturnsTrue() + { + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + #endregion + + #region Reference Types + + [Test] + public void IsReadWriteAtomic_Object_ReturnsTrue() + { + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_String_ReturnsTrue() + { + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_Class_ReturnsTrue() + { + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_Array_ReturnsTrue() + { + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + #endregion + + #region Pointer Types + + [Test] + public void IsReadWriteAtomic_IntPtr_ReturnsTrue() + { + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_UIntPtr_ReturnsTrue() + { + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + #endregion + + #region Enums + + [Test] + public void IsReadWriteAtomic_ByteEnum_ReturnsTrue() + { + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_IntEnum_ReturnsTrue() + { + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_LongEnum_ReturnsTrue() + { + // long is 8 bytes - atomic only if MaxAtomicSize >= 8 + if (MaxAtomicSize >= 8) + { + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + else + { + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + } + + #endregion + + #region Small Structs + + [Test] + public void IsReadWriteAtomic_SmallStruct_ReturnsTrue() + { + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_TwoIntStruct_ReturnsDependsOnArchitecture() + { + // 8 bytes - atomic only if MaxAtomicSize >= 8 + if (MaxAtomicSize >= 8) + { + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + else + { + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + } + + [Test] + public void IsReadWriteAtomic_NestedSmallStruct_ReturnsTrue() + { + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + #endregion + + #region Large Structs + + [Test] + public void IsReadWriteAtomic_LargeStruct_ReturnsFalse() + { + // 24 bytes - too large + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_NestedLargeStruct_ReturnsFalse() + { + // 12 bytes - too large + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_Guid_ReturnsFalse() + { + // 16 bytes + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_Decimal_ReturnsFalse() + { + // 16 bytes + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + + #endregion + + #region Structs with References + + [Test] + public void IsReadWriteAtomic_StructWithReference_ReturnsDependsOnArchitecture() + { + // Contains object reference (IntPtr.Size) + int (4 bytes) + // Total: IntPtr.Size + 4 + // On 32-bit: 4 + 4 = 8 bytes (atomic if MaxAtomicSize >= 8) + // On 64-bit: 8 + 4 = 12 bytes (not atomic) + var expectedSize = IntPtr.Size + 4; + if (expectedSize <= MaxAtomicSize) + { + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + else + { + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + } + + [Test] + public void IsReadWriteAtomic_StructWithString_ReturnsDependsOnArchitecture() + { + // Contains string reference (IntPtr.Size) + int (4 bytes) + // Total: IntPtr.Size + 4 + var expectedSize = IntPtr.Size + 4; + if (expectedSize <= MaxAtomicSize) + { + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + else + { + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + } + + [Test] + public void IsReadWriteAtomic_MixedStruct_ReturnsDependsOnArchitecture() + { + // Contains object reference (IntPtr.Size) + int (4 bytes) + byte (1 byte) + // Total: IntPtr.Size + 5 + // On 32-bit: 4 + 5 = 9 bytes (not atomic) + // On 64-bit: 8 + 5 = 13 bytes (not atomic) + var expectedSize = IntPtr.Size + 5; + if (expectedSize <= MaxAtomicSize) + { + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + else + { + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + } + + #endregion + + #region Nullable Types + + [Test] + public void IsReadWriteAtomic_NullableInt_ReturnsDependsOnArchitecture() + { + // Nullable contains bool (1 byte) + int (4 bytes) = 5 bytes in field calculation + // Atomic if MaxAtomicSize >= 5 + if (MaxAtomicSize >= 5) + { + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + else + { + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + } + + [Test] + public void IsReadWriteAtomic_NullableLong_ReturnsFalse() + { + // Nullable contains bool (1 byte) + long (8 bytes) = 9 bytes in field calculation + // Always exceeds MaxAtomicSize (max 8 bytes) + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_NullableByte_ReturnsBasedOnSize() + { + // Nullable contains bool (1 byte) + byte (1 byte) = 2 bytes + // Should be atomic + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_StructWithNullableInt_ReturnsDependsOnArchitecture() + { + // Struct containing Nullable - 5 bytes (bool + int) in field calculation + // Atomic if MaxAtomicSize >= 5 + if (MaxAtomicSize >= 5) + { + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + else + { + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + } + + [Test] + public void IsReadWriteAtomic_StructWithNullableLong_ReturnsFalse() + { + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + + #endregion + + #region Common BCL Types + + [Test] + public void IsReadWriteAtomic_DateTime_ReturnsTrue() + { + // DateTime is 8 bytes (single ulong internally) + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_TimeSpan_ReturnsTrue() + { + // TimeSpan is 8 bytes (single long internally) + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_DateTimeOffset_ReturnsFalse() + { + // DateTimeOffset contains DateTime + short = 10 bytes + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + + #endregion + + #region Caching Verification + + [Test] + public void IsReadWriteAtomic_ReturnsSameValueOnMultipleCalls() + { + // Verify caching works correctly + var first = Memory.IsReadWriteAtomic(); + var second = Memory.IsReadWriteAtomic(); + Assert.AreEqual(first, second); + + var firstLarge = Memory.IsReadWriteAtomic(); + var secondLarge = Memory.IsReadWriteAtomic(); + Assert.AreEqual(firstLarge, secondLarge); + } + + #endregion + } +} From 1a4733d0b4b58e9c9250df62b86f13af5df66e80 Mon Sep 17 00:00:00 2001 From: "Ilya.Usov" Date: Mon, 16 Feb 2026 22:06:12 +0100 Subject: [PATCH 2/4] Improve ViewableProperty - Make maybe return consistent value with hasValue - Fix tearing issues with myValue if T is long struct - Enforce memory ordering between myValue = value and myChange.Fire(value); --- .../Collections/Viewable/ViewableProperty.cs | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/rd-net/Lifetimes/Collections/Viewable/ViewableProperty.cs b/rd-net/Lifetimes/Collections/Viewable/ViewableProperty.cs index 2c0ec3457..9b2a2aabf 100644 --- a/rd-net/Lifetimes/Collections/Viewable/ViewableProperty.cs +++ b/rd-net/Lifetimes/Collections/Viewable/ViewableProperty.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.Threading; using JetBrains.Core; using JetBrains.Diagnostics; using JetBrains.Lifetimes; +using JetBrains.Util.Internal; namespace JetBrains.Collections.Viewable { @@ -12,11 +14,35 @@ namespace JetBrains.Collections.Viewable /// public class ViewableProperty : IViewableProperty { + private static readonly bool ourIsReadWriteAtomic = Memory.IsReadWriteAtomic(); + private readonly Signal myChange = new Signal(); + private T myValue = default!; + private volatile bool myHasValue; + public ISource Change => myChange; - public Maybe Maybe { get; private set; } + public Maybe Maybe + { + get + { + if (myHasValue) + { + if (ourIsReadWriteAtomic) + { + return new Maybe(myValue); + } + + lock (myChange) + { + return new Maybe(myValue); + } + } + + return Maybe.None; + } + } public ViewableProperty() {} @@ -36,7 +62,13 @@ public virtual T Value lock (myChange) { if (Maybe.HasValue && EqualityComparer.Default.Equals(Maybe.Value, value)) return; - Maybe = new Maybe(value); + myValue = value; + myHasValue = true; + + // After optimizing signal, `Fire` no longer provides a full memory fence (triggered by `Interlocked.CompareExchange`). + // This caused our tests to become flaky because we rely on `Fire(value)` being observed strictly after `myValue = value`; + // To enforce this ordering, we explicitly add a memory barrier here. + Interlocked.MemoryBarrier(); myChange.Fire(value); } } From 721df61956c806a88f14e856f6e30735f42c2ba6 Mon Sep 17 00:00:00 2001 From: "Ilya.Usov" Date: Mon, 16 Mar 2026 16:49:34 +0100 Subject: [PATCH 3/4] Add public Memory.SizeOf() and comprehensive IsReadWriteAtomic tests ## Memory.SizeOf() Add a public unconstrained `SizeOf()` method that returns the managed size of any type. Results are cached in a static generic class `SizeOfCache`. Implementation per TFM: - net5+: delegates to `Unsafe.SizeOf()` - net472: emits a `DynamicMethod` with the `sizeof` IL opcode - netstandard2.0: same DynamicMethod approach, enabled by adding a conditional `System.Reflection.Emit.Lightweight` package reference ## IsReadWriteAtomicCache cleanup Replace the old approximate field-sum size calculation (which used `Marshal.SizeOf` for primitives and recursive field enumeration) with exact `SizeOf()`. This fixes edge cases where padding/alignment caused the old heuristic to return incorrect sizes (e.g. `PaddedSequentialStruct` field sum = 8 but actual size = 12). The Pack check `(Pack != 0 && Pack != MaxAtomicSize)` is intentionally conservative: it rejects any non-default Pack that doesn't match the atomic word size, even if the Pack provides sufficient alignment (e.g. Pack=16). This avoids false positives for atomicity on architectures where misaligned access is non-atomic (ARM) or may cross cache lines (x86/x64). ## MaxAtomicSize simplification Use `IntPtr.Size` directly instead of checking `Is64BitOperatingSystem`, since 8-byte atomic reads are not safe in 32-bit processes even on a 64-bit OS. ## Tests (157 total) - Primitives, reference types, enums, pointer types - Struct layout edge cases: padding, field ordering, explicit layout with large offsets and overlapping fields (unions) - Pack variations: Pack=1/2/4/8/16 with various field combinations - StructLayout.Size attribute: forced sizes at boundary values - Generic structs: Wrapper, Pair, Triple with value types, reference types, nested generics, constrained generics - BCL generics: Nullable, KeyValuePair, ValueTuple - SizeOf() for all of the above including reference types - Consistency checks between SizeOf and IsReadWriteAtomic - Endianness documentation: tests confirming atomicity is determined by size and alignment, not byte ordering Co-Authored-By: Claude Opus 4.6 (1M context) --- rd-net/Lifetimes/Lifetimes.csproj | 1 + rd-net/Lifetimes/Util/Memory.cs | 100 +- rd-net/Test.Lifetimes/Utils/MemoryTest.cs | 1231 ++++++++++++++++++++- 3 files changed, 1254 insertions(+), 78 deletions(-) diff --git a/rd-net/Lifetimes/Lifetimes.csproj b/rd-net/Lifetimes/Lifetimes.csproj index eb3a314ff..09e3405ad 100644 --- a/rd-net/Lifetimes/Lifetimes.csproj +++ b/rd-net/Lifetimes/Lifetimes.csproj @@ -46,6 +46,7 @@ + diff --git a/rd-net/Lifetimes/Util/Memory.cs b/rd-net/Lifetimes/Util/Memory.cs index 911309f9d..3ff2fecf5 100644 --- a/rd-net/Lifetimes/Util/Memory.cs +++ b/rd-net/Lifetimes/Util/Memory.cs @@ -1,7 +1,6 @@ using System; -using System.Reflection; +using System.Reflection.Emit; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Threading; namespace JetBrains.Util.Internal @@ -12,7 +11,7 @@ public class Memory public static unsafe void CopyMemory(byte* src, byte* dest, int len) { - + if(len >= 0x10) { do @@ -66,7 +65,7 @@ public static T VolatileRead(ref T location) where T : class { return Volatile.Read(ref location); } - + [MethodImpl(MethodImplAdvancedOptions.AggressiveInlining)] public static void VolatileWrite(ref T location, T value) where T : class { @@ -79,41 +78,55 @@ public static int VolatileRead(ref int location) { return Volatile.Read(ref location); } - + [MethodImpl(MethodImplAdvancedOptions.AggressiveInlining)] public static void VolatileWrite(ref int location, int value) { Volatile.Write(ref location, value); } - + [MethodImpl(MethodImplAdvancedOptions.AggressiveInlining)] public static bool VolatileRead(ref bool location) { return Volatile.Read(ref location); } - + [MethodImpl(MethodImplAdvancedOptions.AggressiveInlining)] public static void VolatileWrite(ref bool location, bool value) { Volatile.Write(ref location, value); } + /// + /// Returns the managed slot size of : for value types, this is the struct size + /// including trailing padding; for reference types, this is (the reference + /// slot size, not the heap object size). + /// + public static int SizeOf() => SizeOfCache.Size; + public static bool IsReadWriteAtomic() { return IsReadWriteAtomicCache.IsReadWriteAtomic; } - private static int MaxAtomicSize = ComputeMaxAtomicSize(); - private static int NonAtomicSize = MaxAtomicSize + 1; - - private static int ComputeMaxAtomicSize() + private static readonly int MaxAtomicSize = IntPtr.Size; + + private static class SizeOfCache { - if (Environment.Is64BitOperatingSystem) + public static readonly int Size = Compute(); + + private static int Compute() { - return sizeof(long); +#if NET5_0_OR_GREATER + return Unsafe.SizeOf(); +#else + var dm = new DynamicMethod("SizeOf", typeof(int), Type.EmptyTypes, typeof(Memory).Module, true); + var il = dm.GetILGenerator(); + il.Emit(OpCodes.Sizeof, typeof(T)); + il.Emit(OpCodes.Ret); + return ((Func)dm.CreateDelegate(typeof(Func)))(); +#endif } - - return IntPtr.Size; } private static class IsReadWriteAtomicCache @@ -122,57 +135,16 @@ private static class IsReadWriteAtomicCache private static bool Compute() { - var size = GetSize(); - return size <= MaxAtomicSize; - } - - private static int GetSize() - { -#if NET5_0_OR_GREATER - return Unsafe.SizeOf(); -#else - try - { - return ComputeApproximateSize(typeof(T)); - } - catch - { - // just in case something went wrong - return NonAtomicSize; - } -#endif - } + var type = typeof(T); + if (!type.IsValueType) return true; - private static int ComputeApproximateSize(Type type) - { - if (!type.IsValueType) - { - return IntPtr.Size; - } - - if (type.IsPrimitive || type.IsPointer) - { - return Marshal.SizeOf(type); - } - - if (type.IsEnum) - { - var underlyingType = Enum.GetUnderlyingType(type); - return Marshal.SizeOf(underlyingType); - } - - var totalSize = 0; - var types = type.GetRuntimeFields(); - foreach (var fieldInfo in types) - { - if (fieldInfo.IsStatic) continue; + var layoutAttr = type.StructLayoutAttribute; + if (layoutAttr != null && layoutAttr.Pack != 0 && layoutAttr.Pack < MaxAtomicSize) + return false; - totalSize += ComputeApproximateSize(fieldInfo.FieldType); - if (totalSize > MaxAtomicSize) return NonAtomicSize; - } - - return totalSize; + var size = SizeOf(); + return size <= MaxAtomicSize; } } } -} \ No newline at end of file +} diff --git a/rd-net/Test.Lifetimes/Utils/MemoryTest.cs b/rd-net/Test.Lifetimes/Utils/MemoryTest.cs index 9ff56460c..cc600e53b 100644 --- a/rd-net/Test.Lifetimes/Utils/MemoryTest.cs +++ b/rd-net/Test.Lifetimes/Utils/MemoryTest.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; using JetBrains.Util.Internal; using NUnit.Framework; @@ -6,19 +8,7 @@ namespace Test.Lifetimes.Utils { public class MemoryTest { - // Helper to determine expected atomic size threshold - // This matches the logic in Memory.ComputeMaxAtomicSize() - private static int MaxAtomicSize - { - get - { - if (Environment.Is64BitOperatingSystem) - { - return sizeof(long); - } - return IntPtr.Size; - } - } + private static int MaxAtomicSize => IntPtr.Size; #region Test Types @@ -76,7 +66,7 @@ private struct NestedLargeStruct public TwoIntStruct Inner; public int Extra; } - + private struct MixedStruct { public object Ref; @@ -89,6 +79,304 @@ private class SampleClass public int Value; } + [StructLayout(LayoutKind.Explicit)] + private struct ExplicitLayoutLargeOffset + { + [FieldOffset(0)] + public byte A; + [FieldOffset(300)] + public byte B; + } + + private struct PaddedSequentialStruct + { + public short C; + public int A; + public byte B; + } + + // Same fields as PaddedSequentialStruct but ordered largest-first → no padding waste + private struct OptimalSequentialStruct + { + public int A; + public short C; + public byte B; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private struct PackedStruct + { + public byte A; + public int B; + public short C; + } + + // Auto layout, mixed types: int(4) + byte(1) = 5 bytes of fields + // Worst-case with padding: int(4) + byte(1) + 3pad = 8 → always ≤ 8 → atomic + [StructLayout(LayoutKind.Auto)] + private struct AutoLayoutMixedSmall + { + public int A; + public byte B; + } + + // Auto layout, mixed types: int(4) + byte(1) + long(8) = 13 bytes of fields + // Non-atomic regardless of reordering (field sum alone > 8) + [StructLayout(LayoutKind.Sequential)] + private struct AutoLayoutMixedLarge + { + public int A; + public byte B; + public long C; + } + + // Sequential, mixed types: short(2) + pad(2) + int(4) + short(2) + pad(2) = 12 bytes + // Field sum = 8 but actual = 12 → non-atomic + private struct SequentialMixedPadded + { + public short A; + public int B; + public short C; + } + + // --- Generic structs --- + + private struct Wrapper + { + public T Value; + } + + private struct Pair + { + public T1 First; + public T2 Second; + } + + private struct Triple + { + public T1 A; + public T2 B; + public T3 C; + } + + // --- Layout variations --- + + private struct EmptyStruct { } + + [StructLayout(LayoutKind.Explicit)] + private struct Union32 + { + [FieldOffset(0)] public int AsInt; + [FieldOffset(0)] public float AsFloat; + } + + [StructLayout(LayoutKind.Explicit)] + private struct Union64 + { + [FieldOffset(0)] public long AsLong; + [FieldOffset(0)] public double AsDouble; + [FieldOffset(0)] public int AsInt; + } + + [StructLayout(LayoutKind.Explicit)] + private struct ExplicitPadded + { + [FieldOffset(0)] public byte A; + [FieldOffset(8)] public byte B; + } + + [StructLayout(LayoutKind.Sequential, Pack = 2)] + private struct Pack2Struct + { + public byte A; + public int B; + public byte C; + } + + [StructLayout(LayoutKind.Sequential, Pack = 4)] + private struct Pack4Struct + { + public byte A; + public long B; + public byte C; + } + + [StructLayout(LayoutKind.Sequential, Pack = 8)] + private struct Pack8Struct + { + public byte A; + public long B; + public byte C; + } + + [StructLayout(LayoutKind.Sequential, Size = 32)] + private struct FixedSizeStruct + { + public int A; + } + + [StructLayout(LayoutKind.Sequential, Size = 4)] + private struct SmallFixedSizeStruct + { + public byte A; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 3)] + private struct Pack1Size3Struct + { + public byte A; + public byte B; + public byte C; + } + + // --- Pack variations for atomicity testing --- + + // Pack=1 single byte: size=1, but Pack=1 → non-default Pack + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private struct Pack1SingleByte + { + public byte A; + } + + // Pack=1 with int: 5 bytes total (byte + int, no padding), misaligned int + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private struct Pack1ByteInt + { + public byte A; + public int B; + } + + // Pack=1 single int: size=4, but fields could be at odd addresses + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private struct Pack1SingleInt + { + public int A; + } + + // Pack=2 single short: size=2 + [StructLayout(LayoutKind.Sequential, Pack = 2)] + private struct Pack2SingleShort + { + public short A; + } + + // Pack=2 with int: byte(1) + pad(1) + int(4) = 6 bytes, int at 2-byte alignment + [StructLayout(LayoutKind.Sequential, Pack = 2)] + private struct Pack2ByteInt + { + public byte A; + public int B; + } + + // Pack=4, single int: size=4, alignment=4 + [StructLayout(LayoutKind.Sequential, Pack = 4)] + private struct Pack4SingleInt + { + public int A; + } + + // Pack=4, single long: size=8, but long is at 4-byte alignment (not 8-byte) + [StructLayout(LayoutKind.Sequential, Pack = 4)] + private struct Pack4SingleLong + { + public long A; + } + + // Pack=16 (more than natural alignment) — effectively same as default + [StructLayout(LayoutKind.Sequential, Pack = 16)] + private struct Pack16SingleInt + { + public int A; + } + + // Pack=16 with int+short: same as default layout, should be safe + [StructLayout(LayoutKind.Sequential, Pack = 16)] + private struct Pack16IntShort + { + public int A; + public short B; + } + + // --- Explicit layout atomicity edge cases --- + + // Explicit layout, naturally aligned, fits in word + [StructLayout(LayoutKind.Explicit)] + private struct ExplicitAligned4 + { + [FieldOffset(0)] public int Value; + } + + // Explicit layout, 8-byte union — should be atomic on 64-bit + [StructLayout(LayoutKind.Explicit)] + private struct ExplicitUnion8 + { + [FieldOffset(0)] public long AsLong; + [FieldOffset(0)] public double AsDouble; + } + + // Explicit layout with odd field offset — potential misalignment + [StructLayout(LayoutKind.Explicit)] + private struct ExplicitOddOffset + { + [FieldOffset(0)] public byte A; + [FieldOffset(1)] public int B; // int at offset 1 — misaligned! + } + + // Explicit layout, 3 bytes — odd size, but fits in word + [StructLayout(LayoutKind.Explicit)] + private struct Explicit3Bytes + { + [FieldOffset(0)] public byte A; + [FieldOffset(1)] public byte B; + [FieldOffset(2)] public byte C; + } + + // Explicit layout, size exactly MaxAtomicSize on 64-bit + [StructLayout(LayoutKind.Explicit)] + private struct ExplicitExact8 + { + [FieldOffset(0)] public int A; + [FieldOffset(4)] public int B; + } + + // --- Size= attribute edge cases --- + + // Struct with Size forcing it to exactly MaxAtomicSize + [StructLayout(LayoutKind.Sequential, Size = 8)] + private struct FixedSize8 + { + public int A; + } + + // Struct with Size = 9 — one byte over the atomic limit + [StructLayout(LayoutKind.Sequential, Size = 9)] + private struct FixedSize9 + { + public int A; + } + + // Struct with Size = 1 + [StructLayout(LayoutKind.Sequential, Size = 1)] + private struct FixedSize1 + { + public byte A; + } + + // --- Generic struct with constrained T --- + + private struct WrapperWithExtra + { + public T Value; + public int Extra; + } + + // Generic struct containing a reference and a value type + private struct RefValuePair where TRef : class where TVal : struct + { + public TRef Ref; + public TVal Val; + } + #endregion #region Primitive Types @@ -456,5 +744,920 @@ public void IsReadWriteAtomic_ReturnsSameValueOnMultipleCalls() } #endregion + + #region Struct Layout Edge Cases + + [Test] + public void IsReadWriteAtomic_ExplicitLayout_LargeOffset_ReturnsFalse() + { + // Real size is 301 bytes (FieldOffset(300) + 1 byte), clearly non-atomic + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_PaddedSequentialStruct_ReturnsFalse() + { + // Real size is 12 bytes (short=2 + 2 padding + int=4 + byte=1 + 3 padding), exceeds MaxAtomicSize + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_FieldOrder_AffectsAtomicity() + { + // Same fields, different order → different real size due to padding + // OptimalSequentialStruct: int + short + byte + 1pad = 8 bytes → atomic on 64-bit + // PaddedSequentialStruct: short + 2pad + int + byte + 3pad = 12 bytes → non-atomic + Assert.IsTrue(Memory.IsReadWriteAtomic()); + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_PackedStruct_ReturnsFalse() + { + // Pack=1 means fields may be misaligned, pessimistic safety says non-atomic + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_AutoLayoutMixedSmall_ReturnsTrue() + { + // int + byte, worst-case padded to 8 → always atomic + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_AutoLayoutMixedLarge_ReturnsFalse() + { + // int + byte + long = 13 bytes min → non-atomic regardless of reordering + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_SequentialMixedPadded_ReturnsFalse() + { + // short + int + short: field sum = 8 but actual size = 12 due to padding + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + + #endregion + + #region Pack Variations and Alignment + + [Test] + public void IsReadWriteAtomic_Pack1SingleByte_ReturnsFalse() + { + // Pack=1 is a non-default Pack value → pessimistic: non-atomic + // Even though size=1 would fit, the Pack=1 attribute signals potential misalignment + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_Pack1SingleInt_ReturnsFalse() + { + // Pack=1, size=4: the int could be placed at any byte address + // On ARM, misaligned 4-byte access faults; on x86, it may cross a cache line → non-atomic + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_Pack1ByteInt_ReturnsFalse() + { + // Pack=1: byte(1) + int(4) = 5 bytes, int at offset 1 (misaligned) + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_Pack2SingleShort_ReturnsFalse() + { + // Pack=2 ≠ MaxAtomicSize → rejected as non-atomic + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_Pack2ByteInt_ReturnsFalse() + { + // Pack=2: int field at 2-byte alignment, may be misaligned for 4-byte atomic access + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_Pack4SingleInt_DependsOnArchitecture() + { + // Pack=4, size=4. On 32-bit (MaxAtomicSize=4): Pack==MaxAtomicSize → atomic + // On 64-bit (MaxAtomicSize=8): Pack=4 ≠ 8 → rejected as non-atomic (conservative) + if (MaxAtomicSize == 4) + Assert.IsTrue(Memory.IsReadWriteAtomic()); + else + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_Pack4SingleLong_DependsOnArchitecture() + { + // Pack=4: long at 4-byte alignment. On 64-bit, needs 8-byte alignment for atomic access + // Pack=4 ≠ MaxAtomicSize(8) on 64-bit → false + // On 32-bit, MaxAtomicSize=4, long(8) > 4 → false (too big) + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_Pack8Struct_DependsOnArchitecture() + { + // Pack=8, size=24 → too big regardless + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_Pack16SingleInt_ReturnsTrue() + { + // Pack=16 >= MaxAtomicSize, so fields keep their natural alignment (no degradation). + // Size=4 <= MaxAtomicSize, so this is atomic. + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_Pack16IntShort_ReturnsTrue() + { + // Pack=16 >= MaxAtomicSize, so fields keep their natural alignment (no degradation). + // Size=8 <= MaxAtomicSize on 64-bit, so this is atomic. + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + #endregion + + #region Explicit Layout and Misalignment + + [Test] + public void IsReadWriteAtomic_ExplicitAligned4_ReturnsTrue() + { + // Explicit layout, single int at offset 0, default Pack → no misalignment issue + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_ExplicitUnion8_DependsOnArchitecture() + { + // Overlapping long/double at offset 0, size=8 + if (MaxAtomicSize >= 8) + Assert.IsTrue(Memory.IsReadWriteAtomic()); + else + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_Union32_ReturnsTrue() + { + // Overlapping int/float at offset 0, size=4 → fits in word + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_Union64_DependsOnArchitecture() + { + // Size=8, default Pack + if (MaxAtomicSize >= 8) + Assert.IsTrue(Memory.IsReadWriteAtomic()); + else + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_ExplicitOddOffset_ReturnsBasedOnSize() + { + // Explicit layout with int at offset 1 (misaligned within the struct). + // The struct itself has default Pack (no StructLayout.Pack set for Explicit), + // so the struct's own alignment is governed by the runtime. + // Size is 5 bytes (offset 1 + sizeof(int)=4). On 64-bit, 5 <= 8 → atomic. + // The internal misalignment (int at offset 1) doesn't affect the struct-level + // atomicity check — we're checking if the WHOLE struct can be read atomically. + var size = Memory.SizeOf(); + if (size <= MaxAtomicSize) + Assert.IsTrue(Memory.IsReadWriteAtomic()); + else + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_Explicit3Bytes_ReturnsTrue() + { + // 3 bytes, default Pack → always fits in word (MaxAtomicSize >= 4) + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_ExplicitExact8_DependsOnArchitecture() + { + // Exactly 8 bytes (two ints at offset 0 and 4) + if (MaxAtomicSize >= 8) + Assert.IsTrue(Memory.IsReadWriteAtomic()); + else + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_ExplicitPadded_ReturnsFalse() + { + // byte at offset 0, byte at offset 8 → size=9 > MaxAtomicSize → non-atomic + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + + #endregion + + #region Size= Attribute Edge Cases + + [Test] + public void IsReadWriteAtomic_FixedSize8_DependsOnArchitecture() + { + // Size forced to 8 via StructLayout.Size, only int(4) field + if (MaxAtomicSize >= 8) + Assert.IsTrue(Memory.IsReadWriteAtomic()); + else + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_FixedSize9_ReturnsFalse() + { + // Size forced to 9 → exceeds MaxAtomicSize on all architectures + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_FixedSize1_ReturnsTrue() + { + // Size=1, single byte → always atomic + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_FixedSize32_ReturnsFalse() + { + // Size=32 far exceeds MaxAtomicSize + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_SmallFixedSize4_ReturnsTrue() + { + // Size=4, single byte field but struct padded to 4 → fits in word + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_Pack1Size3_ReturnsFalse() + { + // Pack=1, Size=3 → Pack ≠ 0 and Pack ≠ MaxAtomicSize → non-atomic + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_EmptyStruct_ReturnsTrue() + { + // Empty struct has size 1 → always fits + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + #endregion + + #region Generic Structs and IsReadWriteAtomic + + [Test] + public void IsReadWriteAtomic_WrapperInt_ReturnsTrue() + { + // Wrapper: size=4, default layout → atomic + Assert.IsTrue(Memory.IsReadWriteAtomic>()); + } + + [Test] + public void IsReadWriteAtomic_WrapperByte_ReturnsTrue() + { + Assert.IsTrue(Memory.IsReadWriteAtomic>()); + } + + [Test] + public void IsReadWriteAtomic_WrapperLong_DependsOnArchitecture() + { + // Wrapper: size=8 + if (MaxAtomicSize >= 8) + Assert.IsTrue(Memory.IsReadWriteAtomic>()); + else + Assert.IsFalse(Memory.IsReadWriteAtomic>()); + } + + [Test] + public void IsReadWriteAtomic_WrapperObject_IsValueType() + { + // Wrapper is a struct containing a reference → it's a value type, not a reference type + // Size = IntPtr.Size (just a single reference field) → fits in word → atomic + Assert.IsTrue(typeof(Wrapper).IsValueType); + Assert.IsTrue(Memory.IsReadWriteAtomic>()); + } + + [Test] + public void IsReadWriteAtomic_WrapperString_ReturnsTrue() + { + // Wrapper: value type containing string ref, size = IntPtr.Size + Assert.IsTrue(Memory.IsReadWriteAtomic>()); + } + + [Test] + public void IsReadWriteAtomic_PairIntInt_DependsOnArchitecture() + { + // Pair: size=8 + if (MaxAtomicSize >= 8) + Assert.IsTrue(Memory.IsReadWriteAtomic>()); + else + Assert.IsFalse(Memory.IsReadWriteAtomic>()); + } + + [Test] + public void IsReadWriteAtomic_PairByteByte_ReturnsTrue() + { + // Pair: size=2 → always atomic + Assert.IsTrue(Memory.IsReadWriteAtomic>()); + } + + [Test] + public void IsReadWriteAtomic_PairIntLong_ReturnsFalse() + { + // Pair: size=16 (with padding) → always non-atomic + Assert.IsFalse(Memory.IsReadWriteAtomic>()); + } + + [Test] + public void IsReadWriteAtomic_PairObjectObject_ReturnsFalse() + { + // Pair: struct with 2 refs → size = 2 * IntPtr.Size + // On 64-bit: 16 bytes → non-atomic. On 32-bit: 8 bytes → depends + var size = Memory.SizeOf>(); + if (size <= MaxAtomicSize) + Assert.IsTrue(Memory.IsReadWriteAtomic>()); + else + Assert.IsFalse(Memory.IsReadWriteAtomic>()); + } + + [Test] + public void IsReadWriteAtomic_TripleIntIntInt_ReturnsFalse() + { + // Triple: size=12 → always non-atomic + Assert.IsFalse(Memory.IsReadWriteAtomic>()); + } + + [Test] + public void IsReadWriteAtomic_TripleByteByteByte_ReturnsTrue() + { + // Triple: size=3 → always atomic + Assert.IsTrue(Memory.IsReadWriteAtomic>()); + } + + [Test] + public void IsReadWriteAtomic_WrapperWrapperInt_ReturnsTrue() + { + // Nested generic: Wrapper> has size=4 → atomic + Assert.IsTrue(Memory.IsReadWriteAtomic>>()); + } + + [Test] + public void IsReadWriteAtomic_WrapperPairByteByte_ReturnsTrue() + { + // Wrapper>: size=2 → atomic + Assert.IsTrue(Memory.IsReadWriteAtomic>>()); + } + + [Test] + public void IsReadWriteAtomic_WrapperWithExtraInt_DependsOnArchitecture() + { + // WrapperWithExtra: int(4) + int(4) = 8 + if (MaxAtomicSize >= 8) + Assert.IsTrue(Memory.IsReadWriteAtomic>()); + else + Assert.IsFalse(Memory.IsReadWriteAtomic>()); + } + + [Test] + public void IsReadWriteAtomic_WrapperWithExtraLong_ReturnsFalse() + { + // WrapperWithExtra: long(8) + int(4) + pad(4) = 16 → non-atomic + Assert.IsFalse(Memory.IsReadWriteAtomic>()); + } + + [Test] + public void IsReadWriteAtomic_WrapperWithExtraByte_DependsOnArchitecture() + { + // WrapperWithExtra: byte(1) + pad(3) + int(4) = 8 + if (MaxAtomicSize >= 8) + Assert.IsTrue(Memory.IsReadWriteAtomic>()); + else + Assert.IsFalse(Memory.IsReadWriteAtomic>()); + } + + [Test] + public void IsReadWriteAtomic_RefValuePair_StringInt_ReturnsFalse() + { + // RefValuePair: ref(IntPtr.Size) + int(4) + pad + // On 64-bit: 8+4+4pad = 16 → non-atomic + // On 32-bit: 4+4 = 8 → depends + var size = Memory.SizeOf>(); + if (size <= MaxAtomicSize) + Assert.IsTrue(Memory.IsReadWriteAtomic>()); + else + Assert.IsFalse(Memory.IsReadWriteAtomic>()); + } + + [Test] + public void IsReadWriteAtomic_ValueTuple_ByteByte_ReturnsTrue() + { + Assert.IsTrue(Memory.IsReadWriteAtomic<(byte, byte)>()); + } + + [Test] + public void IsReadWriteAtomic_ValueTuple_IntInt_DependsOnArchitecture() + { + // (int, int) = 8 bytes + if (MaxAtomicSize >= 8) + Assert.IsTrue(Memory.IsReadWriteAtomic<(int, int)>()); + else + Assert.IsFalse(Memory.IsReadWriteAtomic<(int, int)>()); + } + + [Test] + public void IsReadWriteAtomic_ValueTuple_IntIntInt_ReturnsFalse() + { + // (int, int, int) = 12 bytes → non-atomic + Assert.IsFalse(Memory.IsReadWriteAtomic<(int, int, int)>()); + } + + [Test] + public void IsReadWriteAtomic_KeyValuePair_ByteByte_ReturnsTrue() + { + Assert.IsTrue(Memory.IsReadWriteAtomic>()); + } + + [Test] + public void IsReadWriteAtomic_KeyValuePair_IntInt_DependsOnArchitecture() + { + if (MaxAtomicSize >= 8) + Assert.IsTrue(Memory.IsReadWriteAtomic>()); + else + Assert.IsFalse(Memory.IsReadWriteAtomic>()); + } + + [Test] + public void IsReadWriteAtomic_KeyValuePair_StringInt_ReturnsFalse() + { + // string ref + int → 12-16 bytes → non-atomic + Assert.IsFalse(Memory.IsReadWriteAtomic>()); + } + + #endregion + + #region Endianness — Does Not Affect Atomicity + + // Atomicity depends on bus width and alignment, NOT byte ordering. + // A torn read produces a mix of old/new bytes regardless of endianness. + // These tests document that IsReadWriteAtomic is endian-independent: + // the result is determined only by size and alignment. + + [Test] + public void IsReadWriteAtomic_EndiannessIrrelevant_IntAlwaysAtomic() + { + // int (4 bytes) is atomic on all supported .NET architectures (x86, x64, ARM, ARM64) + // regardless of endianness. All these architectures have at least 4-byte atomic ops. + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_EndiannessIrrelevant_LongAtomicityDependsOnWordSize() + { + // long (8 bytes) atomicity depends on word size, not endianness. + // On ARM (32-bit big-endian or little-endian): non-atomic (8 > 4) + // On ARM64 / x64 (little-endian): atomic (8 <= 8) + // The check is purely size-based. + Assert.AreEqual(MaxAtomicSize >= 8, Memory.IsReadWriteAtomic()); + } + + [Test] + public void IsReadWriteAtomic_EndiannessIrrelevant_SingleByteAlwaysAtomic() + { + // A single byte is always atomic — endianness doesn't even apply to 1-byte values + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + #endregion + + #region SizeOf + + [Test] + public void SizeOf_Int_Returns4() + { + Assert.AreEqual(4, Memory.SizeOf()); + } + + [Test] + public void SizeOf_Long_Returns8() + { + Assert.AreEqual(8, Memory.SizeOf()); + } + + [Test] + public void SizeOf_Byte_Returns1() + { + Assert.AreEqual(1, Memory.SizeOf()); + } + + [Test] + public void SizeOf_Guid_Returns16() + { + Assert.AreEqual(16, Memory.SizeOf()); + } + + [Test] + public void SizeOf_Bool_Returns1() + { + Assert.AreEqual(1, Memory.SizeOf()); + } + + [Test] + public void SizeOf_Short_Returns2() + { + Assert.AreEqual(2, Memory.SizeOf()); + } + + [Test] + public void SizeOf_Double_Returns8() + { + Assert.AreEqual(8, Memory.SizeOf()); + } + + [Test] + public void SizeOf_IntPtr_ReturnsPointerSize() + { + Assert.AreEqual(IntPtr.Size, Memory.SizeOf()); + } + + [Test] + public void SizeOf_ReferenceType_ReturnsIntPtrSize() + { + Assert.AreEqual(IntPtr.Size, Memory.SizeOf()); + Assert.AreEqual(IntPtr.Size, Memory.SizeOf()); + Assert.AreEqual(IntPtr.Size, Memory.SizeOf()); + } + + [Test] + public void SizeOf_ByteEnum_Returns1() + { + Assert.AreEqual(1, Memory.SizeOf()); + } + + [Test] + public void SizeOf_IntEnum_Returns4() + { + Assert.AreEqual(4, Memory.SizeOf()); + } + + [Test] + public void SizeOf_LongEnum_Returns8() + { + Assert.AreEqual(8, Memory.SizeOf()); + } + + [Test] + public void SizeOf_SmallStruct_Returns4() + { + Assert.AreEqual(4, Memory.SizeOf()); + } + + [Test] + public void SizeOf_TwoIntStruct_Returns8() + { + Assert.AreEqual(8, Memory.SizeOf()); + } + + [Test] + public void SizeOf_LargeStruct_Returns24() + { + Assert.AreEqual(24, Memory.SizeOf()); + } + + #endregion + + #region SizeOf - Generic Structs + + [Test] + public void SizeOf_WrapperInt_Returns4() + { + Assert.AreEqual(4, Memory.SizeOf>()); + } + + [Test] + public void SizeOf_WrapperLong_Returns8() + { + Assert.AreEqual(8, Memory.SizeOf>()); + } + + [Test] + public void SizeOf_WrapperByte_Returns1() + { + Assert.AreEqual(1, Memory.SizeOf>()); + } + + [Test] + public void SizeOf_WrapperObject_ReturnsIntPtrSize() + { + // Wrapper contains a reference field → size = IntPtr.Size + Assert.AreEqual(IntPtr.Size, Memory.SizeOf>()); + } + + [Test] + public void SizeOf_WrapperString_ReturnsIntPtrSize() + { + Assert.AreEqual(IntPtr.Size, Memory.SizeOf>()); + } + + [Test] + public void SizeOf_PairIntInt_Returns8() + { + Assert.AreEqual(8, Memory.SizeOf>()); + } + + [Test] + public void SizeOf_PairByteByte_Returns2() + { + Assert.AreEqual(2, Memory.SizeOf>()); + } + + [Test] + public void SizeOf_PairIntLong_HasPadding() + { + // Sequential: int(4) + pad(4) + long(8) = 16 + Assert.AreEqual(16, Memory.SizeOf>()); + } + + [Test] + public void SizeOf_PairLongInt_Returns16() + { + // Sequential: long(8) + int(4) + pad(4) = 16 + Assert.AreEqual(16, Memory.SizeOf>()); + } + + [Test] + public void SizeOf_PairByteInt_HasPadding() + { + // Sequential: byte(1) + pad(3) + int(4) = 8 + Assert.AreEqual(8, Memory.SizeOf>()); + } + + [Test] + public void SizeOf_PairIntObject_ContainsReference() + { + // int(4) + pad + object ref(IntPtr.Size) + // On 64-bit: int(4) + pad(4) + ref(8) = 16 + // On 32-bit: int(4) + ref(4) = 8 + var size = Memory.SizeOf>(); + Assert.Greater(size, 4); // at least bigger than int alone + Assert.AreEqual(IntPtr.Size == 8 ? 16 : 8, size); + } + + [Test] + public void SizeOf_PairObjectObject_ReturnsTwoPointers() + { + Assert.AreEqual(IntPtr.Size * 2, Memory.SizeOf>()); + } + + [Test] + public void SizeOf_TripleIntIntInt_Returns12() + { + Assert.AreEqual(12, Memory.SizeOf>()); + } + + [Test] + public void SizeOf_TripleByteByteByte_Returns3() + { + Assert.AreEqual(3, Memory.SizeOf>()); + } + + [Test] + public void SizeOf_TripleLongLongLong_Returns24() + { + Assert.AreEqual(24, Memory.SizeOf>()); + } + + [Test] + public void SizeOf_NestedGeneric_WrapperWrapperInt_Returns4() + { + Assert.AreEqual(4, Memory.SizeOf>>()); + } + + [Test] + public void SizeOf_NestedGeneric_WrapperPairIntLong() + { + // Wrapper> should be same size as Pair + Assert.AreEqual(Memory.SizeOf>(), Memory.SizeOf>>()); + } + + [Test] + public void SizeOf_NullableInt_Returns8() + { + // Nullable: bool(1→4 padded) + int(4) = 8 + Assert.AreEqual(8, Memory.SizeOf()); + } + + [Test] + public void SizeOf_NullableByte_Returns2() + { + // Nullable: bool(1) + byte(1) = 2 + Assert.AreEqual(2, Memory.SizeOf()); + } + + [Test] + public void SizeOf_NullableLong_Returns16() + { + // Nullable: bool(1→8 padded) + long(8) = 16 + Assert.AreEqual(16, Memory.SizeOf()); + } + + [Test] + public void SizeOf_KeyValuePair_IntInt_Returns8() + { + Assert.AreEqual(8, Memory.SizeOf>()); + } + + [Test] + public void SizeOf_KeyValuePair_IntString_ContainsReference() + { + var size = Memory.SizeOf>(); + Assert.AreEqual(IntPtr.Size == 8 ? 16 : 8, size); + } + + [Test] + public void SizeOf_ValueTuple_IntInt_Returns8() + { + Assert.AreEqual(8, Memory.SizeOf<(int, int)>()); + } + + [Test] + public void SizeOf_ValueTuple_ByteByteByteByteByteByteByteInt() + { + // (byte, byte, byte, byte, byte, byte, byte, int) + // Large ValueTuple with Rest field: ValueTuple> + var size = Memory.SizeOf>>(); + Assert.Greater(size, 7 + 4); // at least 11 bytes of data + } + + #endregion + + #region SizeOf - Layout Variations + + [Test] + public void SizeOf_EmptyStruct_Returns1() + { + // Empty struct has size 1 in .NET + Assert.AreEqual(1, Memory.SizeOf()); + } + + [Test] + public void SizeOf_Union32_Returns4() + { + // Overlapping int and float at offset 0 → 4 bytes + Assert.AreEqual(4, Memory.SizeOf()); + } + + [Test] + public void SizeOf_Union64_Returns8() + { + // Overlapping long/double/int at offset 0 → 8 bytes (max field) + Assert.AreEqual(8, Memory.SizeOf()); + } + + [Test] + public void SizeOf_ExplicitPadded_Returns9() + { + // byte at offset 0, byte at offset 8 → size is 9 + Assert.AreEqual(9, Memory.SizeOf()); + } + + [Test] + public void SizeOf_ExplicitLayoutLargeOffset() + { + // byte at 0, byte at 300 → at least 301 bytes + Assert.GreaterOrEqual(Memory.SizeOf(), 301); + } + + [Test] + public void SizeOf_Pack1Struct_Returns7() + { + // Pack=1: byte(1) + int(4) + short(2) = 7, no padding + Assert.AreEqual(7, Memory.SizeOf()); + } + + [Test] + public void SizeOf_Pack2Struct_Returns8() + { + // Pack=2: byte(1) + pad(1) + int(4) + byte(1) + pad(1) = 8 + Assert.AreEqual(8, Memory.SizeOf()); + } + + [Test] + public void SizeOf_Pack4Struct() + { + // Pack=4: byte(1) + pad(3) + long as 2×4(8) + byte(1) + pad(3) = 16 + Assert.AreEqual(16, Memory.SizeOf()); + } + + [Test] + public void SizeOf_Pack8Struct() + { + // Pack=8: byte(1) + pad(7) + long(8) + byte(1) + pad(7) = 24 + Assert.AreEqual(24, Memory.SizeOf()); + } + + [Test] + public void SizeOf_FixedSizeStruct_Returns32() + { + // Size=32 specified in StructLayout, even though only int(4) field + Assert.AreEqual(32, Memory.SizeOf()); + } + + [Test] + public void SizeOf_SmallFixedSizeStruct_Returns4() + { + // Size=4, only byte(1) field → padded to 4 + Assert.AreEqual(4, Memory.SizeOf()); + } + + [Test] + public void SizeOf_Pack1Size3Struct_Returns3() + { + // Pack=1, Size=3: 3 bytes, no padding + Assert.AreEqual(3, Memory.SizeOf()); + } + + [Test] + public void SizeOf_PaddedSequentialStruct_Returns12() + { + // short(2) + pad(2) + int(4) + byte(1) + pad(3) = 12 + Assert.AreEqual(12, Memory.SizeOf()); + } + + [Test] + public void SizeOf_OptimalSequentialStruct_Returns8() + { + // int(4) + short(2) + byte(1) + pad(1) = 8 + Assert.AreEqual(8, Memory.SizeOf()); + } + + [Test] + public void SizeOf_SequentialMixedPadded_Returns12() + { + // short(2) + pad(2) + int(4) + short(2) + pad(2) = 12 + Assert.AreEqual(12, Memory.SizeOf()); + } + + [Test] + public void SizeOf_Decimal_Returns16() + { + Assert.AreEqual(16, Memory.SizeOf()); + } + + [Test] + public void SizeOf_DateTime_Returns8() + { + Assert.AreEqual(8, Memory.SizeOf()); + } + + [Test] + public void SizeOf_DateTimeOffset_Returns16() + { + // DateTime(8) + short(2) + pad(6) = 16 + Assert.AreEqual(16, Memory.SizeOf()); + } + + #endregion + + #region SizeOf - Consistency with IsReadWriteAtomic + + [Test] + public void SizeOf_ConsistentWithIsReadWriteAtomic_SmallValues() + { + // Anything with SizeOf <= MaxAtomicSize should be atomic (for value types without Pack issues) + Assert.LessOrEqual(Memory.SizeOf(), MaxAtomicSize); + Assert.IsTrue(Memory.IsReadWriteAtomic()); + + Assert.LessOrEqual(Memory.SizeOf(), MaxAtomicSize); + Assert.IsTrue(Memory.IsReadWriteAtomic()); + } + + [Test] + public void SizeOf_ConsistentWithIsReadWriteAtomic_LargeValues() + { + // Anything with SizeOf > MaxAtomicSize should not be atomic + Assert.Greater(Memory.SizeOf(), MaxAtomicSize); + Assert.IsFalse(Memory.IsReadWriteAtomic()); + + Assert.Greater(Memory.SizeOf(), MaxAtomicSize); + Assert.IsFalse(Memory.IsReadWriteAtomic()); + } + + #endregion } } From 973eb62b36780194cbf5c060fbf94d2130b6d057 Mon Sep 17 00:00:00 2001 From: "Ilya.Usov" Date: Mon, 16 Mar 2026 21:48:31 +0100 Subject: [PATCH 4/4] Fix IsReadWriteAtomic tests to pass on 32-bit runtimes Replace unconditional Assert.IsTrue for 8-byte types with Assert.AreEqual(MaxAtomicSize >= 8, ...) so tests are correct when IntPtr.Size is 4. Also fix SizeOf_DateTimeOffset which expects 16 bytes but gets 12 on 32-bit. Co-Authored-By: Claude Opus 4.6 (1M context) --- rd-net/Test.Lifetimes/Utils/MemoryTest.cs | 40 ++++++++++++----------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/rd-net/Test.Lifetimes/Utils/MemoryTest.cs b/rd-net/Test.Lifetimes/Utils/MemoryTest.cs index cc600e53b..9df797f43 100644 --- a/rd-net/Test.Lifetimes/Utils/MemoryTest.cs +++ b/rd-net/Test.Lifetimes/Utils/MemoryTest.cs @@ -418,15 +418,15 @@ public void IsReadWriteAtomic_UInt_ReturnsTrue() } [Test] - public void IsReadWriteAtomic_Long_ReturnsTrue() + public void IsReadWriteAtomic_Long_DependsOnArchitecture() { - Assert.IsTrue(Memory.IsReadWriteAtomic()); + Assert.AreEqual(MaxAtomicSize >= 8, Memory.IsReadWriteAtomic()); } [Test] - public void IsReadWriteAtomic_ULong_ReturnsTrue() + public void IsReadWriteAtomic_ULong_DependsOnArchitecture() { - Assert.IsTrue(Memory.IsReadWriteAtomic()); + Assert.AreEqual(MaxAtomicSize >= 8, Memory.IsReadWriteAtomic()); } [Test] @@ -436,9 +436,9 @@ public void IsReadWriteAtomic_Float_ReturnsTrue() } [Test] - public void IsReadWriteAtomic_Double_ReturnsTrue() + public void IsReadWriteAtomic_Double_DependsOnArchitecture() { - Assert.IsTrue(Memory.IsReadWriteAtomic()); + Assert.AreEqual(MaxAtomicSize >= 8, Memory.IsReadWriteAtomic()); } [Test] @@ -706,17 +706,17 @@ public void IsReadWriteAtomic_StructWithNullableLong_ReturnsFalse() #region Common BCL Types [Test] - public void IsReadWriteAtomic_DateTime_ReturnsTrue() + public void IsReadWriteAtomic_DateTime_DependsOnArchitecture() { // DateTime is 8 bytes (single ulong internally) - Assert.IsTrue(Memory.IsReadWriteAtomic()); + Assert.AreEqual(MaxAtomicSize >= 8, Memory.IsReadWriteAtomic()); } [Test] - public void IsReadWriteAtomic_TimeSpan_ReturnsTrue() + public void IsReadWriteAtomic_TimeSpan_DependsOnArchitecture() { // TimeSpan is 8 bytes (single long internally) - Assert.IsTrue(Memory.IsReadWriteAtomic()); + Assert.AreEqual(MaxAtomicSize >= 8, Memory.IsReadWriteAtomic()); } [Test] @@ -767,7 +767,7 @@ public void IsReadWriteAtomic_FieldOrder_AffectsAtomicity() // Same fields, different order → different real size due to padding // OptimalSequentialStruct: int + short + byte + 1pad = 8 bytes → atomic on 64-bit // PaddedSequentialStruct: short + 2pad + int + byte + 3pad = 12 bytes → non-atomic - Assert.IsTrue(Memory.IsReadWriteAtomic()); + Assert.AreEqual(MaxAtomicSize >= 8, Memory.IsReadWriteAtomic()); Assert.IsFalse(Memory.IsReadWriteAtomic()); } @@ -779,10 +779,10 @@ public void IsReadWriteAtomic_PackedStruct_ReturnsFalse() } [Test] - public void IsReadWriteAtomic_AutoLayoutMixedSmall_ReturnsTrue() + public void IsReadWriteAtomic_AutoLayoutMixedSmall_DependsOnArchitecture() { - // int + byte, worst-case padded to 8 → always atomic - Assert.IsTrue(Memory.IsReadWriteAtomic()); + // int + byte, worst-case padded to 8 → atomic only if MaxAtomicSize >= 8 + Assert.AreEqual(MaxAtomicSize >= 8, Memory.IsReadWriteAtomic()); } [Test] @@ -876,11 +876,11 @@ public void IsReadWriteAtomic_Pack16SingleInt_ReturnsTrue() } [Test] - public void IsReadWriteAtomic_Pack16IntShort_ReturnsTrue() + public void IsReadWriteAtomic_Pack16IntShort_DependsOnArchitecture() { // Pack=16 >= MaxAtomicSize, so fields keep their natural alignment (no degradation). // Size=8 <= MaxAtomicSize on 64-bit, so this is atomic. - Assert.IsTrue(Memory.IsReadWriteAtomic()); + Assert.AreEqual(MaxAtomicSize >= 8, Memory.IsReadWriteAtomic()); } #endregion @@ -1626,10 +1626,12 @@ public void SizeOf_DateTime_Returns8() } [Test] - public void SizeOf_DateTimeOffset_Returns16() + public void SizeOf_DateTimeOffset_DependsOnArchitecture() { - // DateTime(8) + short(2) + pad(6) = 16 - Assert.AreEqual(16, Memory.SizeOf()); + // 64-bit: DateTime(8) + short(2) + pad(6) = 16 + // 32-bit: DateTime(8) + short(2) + pad(2) = 12 + var expected = IntPtr.Size == 8 ? 16 : 12; + Assert.AreEqual(expected, Memory.SizeOf()); } #endregion