Conversation
Iliya-usov
commented
Feb 16, 2026
- Implement Memory.IsReadWriteAtomic
- 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);
- 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);
## Memory.SizeOf<T>() Add a public unconstrained `SizeOf<T>()` method that returns the managed size of any type. Results are cached in a static generic class `SizeOfCache<T>`. Implementation per TFM: - net5+: delegates to `Unsafe.SizeOf<T>()` - 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<T> cleanup Replace the old approximate field-sum size calculation (which used `Marshal.SizeOf` for primitives and recursive field enumeration) with exact `SizeOf<T>()`. 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<T>, Pair<T1,T2>, Triple<T1,T2,T3> with value types, reference types, nested generics, constrained generics - BCL generics: Nullable<T>, KeyValuePair<K,V>, ValueTuple - SizeOf<T>() 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) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR improves ViewableProperty<T> correctness under concurrency by adding a type-based atomicity check in Memory, using it to avoid torn reads for non-atomic value types, and strengthening memory ordering between value updates and change notifications.
Changes:
- Add
Memory.SizeOf<T>()andMemory.IsReadWriteAtomic<T>()(with caching) and wire required dependency fornetstandard2.0. - Update
ViewableProperty<T>to store value/hasValue separately, guard non-atomic reads with a lock, and add an explicit memory barrier before firing change notifications. - Add extensive unit tests covering
SizeOf<T>()andIsReadWriteAtomic<T>()across many type/layout scenarios.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 9 comments.
| File | Description |
|---|---|
| rd-net/Test.Lifetimes/Utils/MemoryTest.cs | New test suite for Memory.SizeOf<T>() and Memory.IsReadWriteAtomic<T>() across primitives, structs, layout/pack edge cases |
| rd-net/Lifetimes/Util/Memory.cs | Adds SizeOf<T>() and IsReadWriteAtomic<T>() implementations with caching and runtime-specific sizing strategy |
| rd-net/Lifetimes/Lifetimes.csproj | Adds System.Reflection.Emit.Lightweight for netstandard2.0 to support DynamicMethod sizing path |
| rd-net/Lifetimes/Collections/Viewable/ViewableProperty.cs | Uses atomicity check + locking to avoid torn reads and adds ordering barrier between state update and Fire() |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| Assert.IsTrue(Memory.IsReadWriteAtomic<long>()); | ||
| } | ||
|
|
||
| [Test] | ||
| public void IsReadWriteAtomic_ULong_ReturnsTrue() | ||
| { | ||
| Assert.IsTrue(Memory.IsReadWriteAtomic<ulong>()); |
| Assert.IsTrue(Memory.IsReadWriteAtomic<long>()); | ||
| } | ||
|
|
||
| [Test] | ||
| public void IsReadWriteAtomic_ULong_ReturnsTrue() | ||
| { | ||
| Assert.IsTrue(Memory.IsReadWriteAtomic<ulong>()); |
| [Test] | ||
| public void IsReadWriteAtomic_Double_ReturnsTrue() | ||
| { | ||
| Assert.IsTrue(Memory.IsReadWriteAtomic<double>()); |
| // DateTime is 8 bytes (single ulong internally) | ||
| Assert.IsTrue(Memory.IsReadWriteAtomic<DateTime>()); | ||
| } | ||
|
|
||
| [Test] | ||
| public void IsReadWriteAtomic_TimeSpan_ReturnsTrue() | ||
| { | ||
| // TimeSpan is 8 bytes (single long internally) | ||
| Assert.IsTrue(Memory.IsReadWriteAtomic<TimeSpan>()); |
| // 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<OptimalSequentialStruct>()); |
| // Size=8 <= MaxAtomicSize on 64-bit, so this is atomic. | ||
| Assert.IsTrue(Memory.IsReadWriteAtomic<Pack16IntShort>()); |
|
|
||
| // 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)] |
| public void IsReadWriteAtomic_TimeSpan_ReturnsTrue() | ||
| { | ||
| // TimeSpan is 8 bytes (single long internally) | ||
| Assert.IsTrue(Memory.IsReadWriteAtomic<TimeSpan>()); |
| // int + byte, worst-case padded to 8 → always atomic | ||
| Assert.IsTrue(Memory.IsReadWriteAtomic<AutoLayoutMixedSmall>()); |
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) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR improves ViewableProperty<T> correctness under concurrency by avoiding torn reads for non-atomic value types and enforcing ordering between storing the new value and firing change notifications. It also adds Memory.SizeOf<T>() and Memory.IsReadWriteAtomic<T>() utilities plus extensive tests covering many layout/size edge cases.
Changes:
- Added
Memory.SizeOf<T>()andMemory.IsReadWriteAtomic<T>()with caching and aReflection.Emitfallback. - Updated
ViewableProperty<T>to storemyValue/myHasValueseparately, use atomicity detection to avoid tearing, and add an explicit memory barrier before firing change events. - Added a large new NUnit test suite validating
SizeOf<T>()andIsReadWriteAtomic<T>()across many types and layouts.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| rd-net/Test.Lifetimes/Utils/MemoryTest.cs | Adds comprehensive tests for Memory.SizeOf<T>() and Memory.IsReadWriteAtomic<T>(). |
| rd-net/Lifetimes/Util/Memory.cs | Implements SizeOf<T>() and IsReadWriteAtomic<T>() with per-type caching and NET5+/fallback paths. |
| rd-net/Lifetimes/Lifetimes.csproj | Adds System.Reflection.Emit.Lightweight for netstandard2.0 to support the fallback DynamicMethod path. |
| rd-net/Lifetimes/Collections/Viewable/ViewableProperty.cs | Uses atomicity detection to avoid torn reads, makes Maybe consistent with HasValue, and enforces ordering before firing change. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| { | ||
| if (Maybe.HasValue && EqualityComparer<T>.Default.Equals(Maybe.Value, value)) return; | ||
| Maybe = new Maybe<T>(value); | ||
| myValue = value; |
|
|
||
| // 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)] |