Skip to content

Commit e2318d4

Browse files
committed
fix(storage): DTypeSize reports in-memory stride, not Marshal.SizeOf
`UnmanagedStorage.DTypeSize` (exposed via `NDArray.dtypesize`) was delegating to `Marshal.SizeOf(_dtype)`. For every numeric dtype that matches, but for bool, `Marshal.SizeOf(typeof(bool)) == 4` because bool is marshaled to win32 BOOL (32-bit). The in-memory layout of `bool[]` is 1 byte per element, so every caller computing a byte offset as `ptr + index * arr.dtypesize` was reading/writing 4× too far into the buffer for bool arrays. Switches to `_typecode.SizeOf()` which correctly returns 1 for bool and matches `Marshal.SizeOf` for every other type. 21 existing call sites (matmul, binary/unary/comparison/reduction ops, nan reductions, std/var, argmax, random shuffle, boolean mask gather, etc.) now get the right value without any downstream change. The bug had been latent until the Phase 2 iterator migration started routing more code paths through NpyIter.Copy and the new NDIterator wrapper; it surfaced most visibly as `sliced_bool[mask]` returning the wrong elements when the source was non-contiguous. With the root fix: var full = np.array(new[] { T,F,T,F,T,F,T,F,T }); var sliced = full["::2"]; // [T,T,T,T,T] non-contig var result = sliced[new_bool_mask]; // now correct per-element np.save.cs already special-cases bool before falling through to `Marshal.SizeOf`, so serialization was unaffected. Remaining Marshal.SizeOf references in the codebase are either in comments that explain this exact issue, or in the `InfoOf<T>.Size` fallback that only runs for types outside the 12 supported dtypes (e.g. Complex). Tests: 6,748 / 6,748 passing on net8.0 and net10.0 with the CI filter (TestCategory!=OpenBugs&TestCategory!=HighMemory).
1 parent b86b348 commit e2318d4

1 file changed

Lines changed: 9 additions & 2 deletions

File tree

src/NumSharp.Core/Backends/Unmanaged/UnmanagedStorage.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,15 @@ public partial class UnmanagedStorage : ICloneable
141141

142142
/// <summary>
143143
/// The size in bytes of a single value of <see cref="DType"/>
144+
/// as stored in the unmanaged buffer.
144145
/// </summary>
145-
/// <remarks>Computed by <see cref="Marshal.SizeOf(object)"/></remarks>
146+
/// <remarks>
147+
/// Returns the in-memory element stride, not the marshaling size.
148+
/// For bool that is 1, not <see cref="Marshal.SizeOf(object)"/>'s 4
149+
/// (bool is marshaled to win32 BOOL = int). All pointer arithmetic
150+
/// over <c>Address</c> uses this value, so the in-memory layout is
151+
/// the only correct reference.
152+
/// </remarks>
146153
public int DTypeSize
147154
{
148155
get
@@ -152,7 +159,7 @@ public int DTypeSize
152159
return IntPtr.Size;
153160
}
154161

155-
return Marshal.SizeOf(_dtype);
162+
return _typecode.SizeOf();
156163
}
157164
}
158165

0 commit comments

Comments
 (0)