Skip to content

refactor: replace reflection with UnsafeAccessor in hot paths#142

Open
mivertowski wants to merge 1 commit intomainfrom
refactor/unsafe-accessor
Open

refactor: replace reflection with UnsafeAccessor in hot paths#142
mivertowski wants to merge 1 commit intomainfrom
refactor/unsafe-accessor

Conversation

@mivertowski
Copy link
Copy Markdown
Owner

Summary

  • Swap three same-assembly FieldInfo.GetValue/SetValue sites for [UnsafeAccessor]-based extern shims (4 call sites, 3 distinct accessors). Each shim is resolved by the JIT at source-gen time and inlined into a direct field load/store — same hot-path cost as a normal field access, with no trim/AOT warnings and no reflection stack frame.
  • Investigated the full surface of src/ reflection; most remaining sites target runtime-loaded plugins, generic-typed consumer attributes, or open generics where T isn't known at the call site, and are correctly out of scope (see "Rejected candidates" below).

Adoption sites

1. CpuMemoryBufferView._parentsrc/Backends/DotCompute.Backends.CPU/CpuMemoryManager.cs

CpuMemoryBufferTypedWrapper<T>.GetParentBuffer() previously reflected into the view's internal _parent reference on every call. The comment in the old code explicitly noted "uses reflection to access the private field, which is not ideal." Now delegates to a new CpuMemoryBufferViewAccessor helper that uses [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_parent")].

2. UnifiedBuffer<T>.Length / SizeInBytes backing fields — src/Core/DotCompute.Memory/UnifiedBufferMemory.cs

Resize(int) previously used two FieldInfo.SetValue calls against the compiler-generated <Length>k__BackingField and <SizeInBytes>k__BackingField names to mutate the otherwise-readonly auto-properties. Now delegates to a new UnifiedBufferBackingFields helper with generic UnsafeAccessor methods — leverages the .NET 9+ generic-parameter support.

3. MetalCompiledKernel._pipelineState — 2 call sites in src/Backends/DotCompute.Backends.Metal/Execution/

MetalExecutionEngine.GetPipelineStateFromKernel and MetalCommandExecutor.SetComputePipelineState previously reflected to extract the native MTLComputePipelineState handle on every kernel dispatch. Both now share a single new MetalCompiledKernelAccessor.PipelineState() helper. The command executor also pattern-matches on MetalCompiledKernel directly instead of walking kernel.GetType(), so mock kernels in tests still no-op cleanly via the fallback branch.

Rejected candidates (investigated, not viable)

Site Why skipped
TypedMemoryBufferWrapper<T>._underlyingBuffer (Metal ExtractMetalBuffer/UnwrapBuffer) Concrete T unknown at the call site — needed a non-generic marker interface or InternalsVisibleTo + public accessor, out of scope
GeneratedKernelDiscoveryService (Name, FullName, Backends, VectorSize, IsParallel on KernelAttribute) KernelAttribute comes from arbitrary consumer assemblies; runtime (DotCompute.Runtime) doesn't reference DotCompute.Generators.Attributes
AotPluginRegistry / PluginSystem.CreatePluginInstance Reflects on runtime-loaded plugin Types — not statically resolvable
CudaKernelLauncher _devicePtr/DevicePointer lookups Runtime-generated generic types (CudaMemoryBuffer<T> with unknown T)
CPU/Metal/CUDA RingKernelRuntime queue reflection (TryDequeue/TryEnqueue/Count/Capacity) Reflects on InputQueue/OutputQueue typed as object?; concrete queue types only known at dispatch time
IAcceleratorExtensions / ICompiledKernelExtensions Reflect on arbitrary IAccelerator / ICompiledKernel implementations by name; targets are interface-style members
SystemInfoManager WMI paths (per task note) WMI types loaded dynamically, as expected
ComputeQueryableExtensions.ExecuteTyped<T> via MakeGenericMethod Runtime-only element type
MetalAccelerator.PerformCleanup reflecting BaseAccelerator._logger Not an UnsafeAccessor case — BaseAccelerator already exposes protected ILogger Logger; this is a gratuitous-reflection bug for a separate cleanup PR

Build / test status

  • dotnet build DotCompute.sln --configuration Release0 errors, 0 warnings
  • DotCompute.Backends.CPU.Tests → 140/140 pass
  • DotCompute.Core.Tests → 1692/1700 pass (6 flaky failures — main baseline shows 9 failures for the same suite; same 2 skips)
  • DotCompute.Memory.Tests Resize filter → 13/13 pass (directly exercises the UnsafeAccessor rewrite of Length/SizeInBytes backing fields)
  • DotCompute.Memory.Tests full suite → 48 failures, identical set to main baseline (pre-existing FluentAssertions / buffer-tracking test flakes; not introduced by this PR)

Test plan

  • Full solution build 0/0
  • Targeted Resize tests pass (validates the one site that writes readonly auto-property backing fields)
  • CPU backend tests 140/140 pass (validates CpuMemoryBufferViewAccessor)
  • Core tests no new regressions vs main
  • Metal on-device tests (not runnable on Linux CI; Metal changes compile cleanly)

🤖 Generated with Claude Code

Swap three same-assembly FieldInfo.GetValue/SetValue sites for
[UnsafeAccessor]-based extern shims. Each shim is resolved by the JIT
at source-gen time and inlined into a direct field load/store — same
hot-path cost as a normal field access, with no trim/AOT warnings and
no reflection stack frame.

Adoption sites (4 call sites, 3 distinct accessors):

- CpuMemoryBufferView._parent (Backends.CPU/CpuMemoryManager.cs):
  CpuMemoryBufferTypedWrapper<T> previously reflected into the view's
  internal parent reference on every GetParentBuffer() call. Now uses
  a strongly-typed CpuMemoryBufferViewAccessor.

- UnifiedBuffer<T>.Length / SizeInBytes backing fields
  (Core/DotCompute.Memory/UnifiedBufferMemory.cs): Resize() previously
  used two FieldInfo.SetValue calls against the compiler-generated
  "<Length>k__BackingField" and "<SizeInBytes>k__BackingField" to mutate
  the otherwise-readonly auto-properties. Now uses
  UnifiedBufferBackingFields.LengthRef/SizeInBytesRef, which leverage
  UnsafeAccessor's .NET 9+ generic-parameter support.

- MetalCompiledKernel._pipelineState (Backends.Metal): Two call sites
  (MetalExecutionEngine.GetPipelineStateFromKernel and
  MetalCommandExecutor.SetComputePipelineState) previously reflected to
  extract the native MTLComputePipelineState handle on every kernel
  dispatch. Now share a single MetalCompiledKernelAccessor shim. The
  command executor also pattern-matches on MetalCompiledKernel directly
  instead of walking the runtime type, so mocks in tests still no-op
  cleanly.

Rejected candidates (kept as-is):

- Core.Memory/TypedMemoryBufferWrapper<T>._underlyingBuffer (accessed
  by Metal ExtractMetalBuffer and UnwrapBuffer): concrete T not known
  at the call site, so UnsafeAccessor's generic-parameter resolution
  cannot be applied. Would require a non-generic marker interface or
  InternalsVisibleTo + a new public accessor — out of scope here.

- Runtime/GeneratedKernelDiscoveryService: reflects across
  KernelAttribute instances from arbitrary consumer assemblies.
  Attribute types are not referenced at compile time.

- Runtime/AotPluginRegistry / Plugins/PluginSystem: reflect on
  runtime-loaded plugin Types. Not statically resolvable.

- CudaKernelLauncher / MetalKernelParameterBinder /
  CPU/Metal/CUDA RingKernelRuntime: reflect on runtime-generated or
  runtime-supplied generic types whose element type is only known at
  dispatch time.

- IAcceleratorExtensions / ICompiledKernelExtensions: reflect on
  arbitrary IAccelerator / ICompiledKernel implementations. Targets
  are interface methods, not private members.

Build: dotnet build DotCompute.sln -c Release → 0 errors, 0 warnings.
Tests: Backends.CPU.Tests 140/140 pass; Core.Tests 1692/1700 pass
(same 6 flakes as main baseline, which shows 9 failures);
UnifiedBuffer Resize tests 13/13 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant