Class NDScope
- Namespace
- NumSharp
- Assembly
- NumSharp.dll
Ambient reclamation scope for transient NDArray intermediates — the
library's standard way to make a composition method eagerly return its temporaries'
pooled buffers instead of waiting on the finalizer (see DISPOSAL-GUIDELINES.md).
Every NDArray constructed on the current thread while a scope is open is
tracked by it; disposing the scope disposes every tracked array that was not yielded
via Returns<T>(T). Tracked disposal is ordinary ARC release (the buffer
frees only at refcount 0), so releasing a base whose view was yielded never corrupts —
the same safety as a hand-written Dispose, with the bookkeeping automated.
public sealed class NDScope : IDisposable
- Inheritance
-
NDScope
- Implements
- Inherited Members
Remarks
Usage. Open at the top of a boundary method, keep the original body, and route every egress through Returns<T>(T):
using var scope = NDScope.Open();
var b1 = x1.typecode == NPTypeCode.Boolean ? x1 : (x1 != 0);
var b2 = x2.typecode == NPTypeCode.Boolean ? x2 : (x2 != 0);
return scope.Returns((b1 & b2).MakeGeneric<bool>());
Ownership rules become structural. Inputs were constructed BEFORE the
scope opened, so they are never tracked — a passthrough (ravel()/
atleast_2d() returning its operand, a caller-supplied @out) needs no
guard, and Returns<T>(T) on such an array is a provable no-op (rule R2
for free). The return value is the one egress and is yielded (rule R1); an
out-parameter egress is written as result = scope.Returns(temp);.
Everything else — however deep the helper-call tree below the scope — is reclaimed,
on exception paths included.
Nesting. Scopes nest per thread; Returns<T>(T) re-tracks the yielded array into the parent scope, so an enclosing scope still reclaims an inner call's result if the caller drops it.
Threading. The current scope is [ThreadStatic]: a scope is opened,
used and disposed on ONE thread (asserted in debug builds; a HAND-WRITTEN scope must not
span await — the weaver's state-machine seam (driven by [NDScopedAsync] for
async methods/iterators and by [NDScoped] for synchronous iterators) is the one
thing that may carry a scope across suspensions, because it uninstalls the scope before
each continuation is scheduled and re-installs it on the resuming thread via
OpenOrResume(ref NDScope)/Suspend(NDScope)). Arrays constructed on other
threads (parallel kernel workers) see no scope and fall back to the finalizer
backstop — safe, just not eagerly reclaimed; a parallel region that wants eager
reclamation opens its own scope inside each worker body.
Granularity. Scope a CALL, not a caller loop: temps a scope holds are all
alive simultaneously, and batch-disposing thousands of same-size buffers overflows
their pool bucket (the excess is freed, not pooled). Hot loops should still
using the results they receive — the two-audience contract is unchanged.
Methods
Attach(NDArray)
Adopts nd into the CURRENT (innermost) scope, so it is reclaimed
at that scope's exit unless yielded — the inverse of Detach(NDArray). For an
array the scope did not construct: one a caller received before opening its scope
(the hot-loop pattern — attach instead of a per-result using), or one built
on another thread and handed over. Moves the array if some other scope tracked it
(the old registration is cleared first — an array is owned by at most ONE scope).
No-op when no scope is open or the array is already tracked here. Must run on the
thread that owns both the current scope and any previous tracking scope.
public static void Attach(NDArray nd)
Parameters
ndNDArray
CloseUnlessDeferred(NDScope)
Task-returning-method epilogue (weaver seam): the finally of a woven method whose
return is Task-like — disposes unless ReturnsTask<TResult>(Task<TResult>)/
ReturnsValueTask<TResult>(ValueTask<TResult>) deferred disposal to the
task's completion (the exception path never defers, so a throw still reclaims eagerly).
public static void CloseUnlessDeferred(NDScope scope)
Parameters
scopeNDScope
Detach(NDArray)
Permanently removes nd from whatever scope tracks it WITHOUT
re-tracking into a parent — for arrays that must outlive every scope (an array
being cached into a static / long-lived field from inside a scoped call). Must run
on the scope's owning thread (it always does: detachment happens at the caching
site, on the constructing thread). No-op for untracked arrays. Also the runtime egress
the NDScopedExitAttribute weaves at the entry of a method that RETAINS an
NDArray parameter (the callee runs inside the caller's ambient scope, so
this reaches into that scope with no call-site re-plumbing).
public static void Detach(NDArray nd)
Parameters
ndNDArray
Detach(NDArray[])
Detaches every element of an NDArray array — the NDArray[] egress of
an NDScopedExitAttribute parameter (a callee that retains a whole tuple/list
of arrays). A null array and null elements are skipped; each element is detached exactly as
Detach(NDArray).
public static void Detach(NDArray[] nds)
Parameters
ndsNDArray[]
Detach(ITuple)
Detaches every NDArray a tuple carries — the ValueTuple/Tuple
egress of an NDScopedExitAttribute parameter, any arity (Rest-packed 8+
included) and any component mix: bare NDArrays, whole NDArray[]
components, and NESTED tuples (recursively) all detach; components carrying no
NDArray are skipped, and a null tuple is a no-op. Indexing through
ITuple boxes value-type components, negligible on the once-per-call
escape path; it mirrors Returns(ITuple).
public static void Detach(ITuple tuple)
Parameters
tupleITuple
Dispose()
Disposes every tracked non-yielded array and reinstates the parent scope.
public void Dispose()
DisposeSlot(ref NDScope)
State-machine completion (weaver seam): disposes the slot's scope and clears the slot
— the async SetResult/SetException exit, the iterator's final false, and the
iterator enumerator's Dispose() (mid-iteration abandonment, which may land on
a different thread than the last MoveNext; that call is sequenced after it, so the
thread stamp is re-taken rather than asserted). No-op for an empty slot.
public static void DisposeSlot(ref NDScope slot)
Parameters
slotNDScope
ExitIterator(ref NDScope, bool)
Iterator-yield exit (weaver seam): a MoveNext that produced a value
(hasMore) suspends the invocation scope for the next resumption; a
finished one disposes it. For async iterators the weaver emits this BEFORE the
promise's SetResult(hasMore) — the consumer can re-enter MoveNext the instant
that signal lands, so the scope must already be off this thread.
public static void ExitIterator(ref NDScope slot, bool hasMore)
Parameters
Open()
Opens a scope on the current thread; nests (the previous scope resumes on dispose).
public static NDScope Open()
Returns
OpenOrResume(ref NDScope)
State-machine prologue (weaver seam): installs the invocation's scope on the current
thread — opening a fresh one into slot on the first MoveNext, and
RE-INSTALLING the suspended one (re-stamping its owning thread; segments of one state
machine are sequenced, so the handoff is race-free) on every resumption.
public static NDScope OpenOrResume(ref NDScope slot)
Parameters
slotNDScope
Returns
Returns(IArraySlice)
Protects a returned bare IArraySlice from this scope's reclamation of an NDArray that shares its buffer.
public IArraySlice Returns(IArraySlice slice)
Parameters
sliceIArraySlice
Returns
Returns(UnmanagedStorage)
Protects a returned bare UnmanagedStorage from this scope's reclamation of an NDArray that shares its buffer.
public UnmanagedStorage Returns(UnmanagedStorage storage)
Parameters
storageUnmanagedStorage
Returns
Returns(ITuple)
Yields every NDArray a tuple result CARRIES, at any arity (Rest-packed
8+ included) and ANY mix — the general ITuple egress that also covers a
reference-type Tuple. Each component dispatches by its RUNTIME
type through the same family the weaver emits for direct returns: a bare
NDArray, an NDArray[] (every element), a NESTED tuple
(recursively — ((a, b), c) yields all three), an INDArrayCarrier
struct (its YieldTo), and a bare IArraySlice/UnmanagedStorage
(counted-ref protection). Components carrying no NDArray (a scalar, a count, …) are
skipped; a null tuple is a no-op. This is the weaver's egress for a tuple the
strongly-typed overloads above don't cover (arity 5+, a non-NDArray component, or a
boxed/reference tuple). It indexes through ITuple, so value-type
components box — negligible on the once-per-call return path; hand-scope a hot
all-NDArray tuple through the typed overloads to avoid it.
public ITuple Returns(ITuple tuple)
Parameters
tupleITuple
Returns
ReturnsTask(Task)
Task egress without a DECLARED result (weaver seam for [NDScopedAsync] NON-async
methods returning bare Task): defers this scope's disposal to the
task's completion when it is still running — the in-flight work may hold tracked
temps. The runtime task may still be a Task<TResult> UP-CAST to
Task (Task<T> : Task is implicit, so
[NDScopedAsync] Task M() => ComputeAsync(); compiles) — a caller can recover
that result by casting back, so a carried result is sniffed and yielded rather than
handed back disposed.
public Task ReturnsTask(Task task)
Parameters
taskTask
Returns
ReturnsTask<TResult>(Task<TResult>)
Yields a task-shaped result (weaver seam for [NDScopedAsync] NON-async methods
returning Task<TResult>): a completed task's result is yielded
immediately; an incomplete one defers BOTH the yield and this scope's disposal to the
task's completion. Returns the task unchanged.
public Task<TResult> ReturnsTask<TResult>(Task<TResult> task)
Parameters
taskTask<TResult>
Returns
- Task<TResult>
Type Parameters
TResult
ReturnsValueTask(ValueTask)
Bare ValueTask egress (weaver seam) — the resultless twin of ReturnsValueTask<TResult>(ValueTask<TResult>).
public ValueTask ReturnsValueTask(ValueTask task)
Parameters
taskValueTask
Returns
ReturnsValueTask<TResult>(ValueTask<TResult>)
ValueTask<TResult> egress (weaver seam). A ValueTask is single-consumption — observing its result or registering a continuation would corrupt the caller's own await — so the scope first Preserve()s it and RETURNS THE PRESERVED task (multi-observable by contract; a plain-value or Task-backed ValueTask preserves to itself at no cost). The preserved form is then handled exactly like ReturnsTask<TResult>(Task<TResult>).
public ValueTask<TResult> ReturnsValueTask<TResult>(ValueTask<TResult> task)
Parameters
taskValueTask<TResult>
Returns
- ValueTask<TResult>
Type Parameters
TResult
Returns<T>(T)
Marks nd as this scope's yielded result (rule R1: never dispose
what you return): unregisters it here and re-tracks it into the parent scope, so an
enclosing scope still reclaims it if the caller drops it. An array this scope never
tracked — an input passthrough, a caller-owned @out — passes through as a
no-op, which is what makes "wrap every egress" a safe blanket rule. Also the egress
call for out-parameter assignments: result = scope.Returns(temp);.
public T Returns<T>(T nd) where T : NDArray
Parameters
ndT
Returns
- T
nd, with its static type preserved.
Type Parameters
T
Returns<T>(T[])
Yields every element of a tuple-style result (nonzero, meshgrid, split, …).
public T[] Returns<T>(T[] nds) where T : NDArray
Parameters
ndsT[]
Returns
- T[]
Type Parameters
T
Returns<T1, T2>((T1, T2))
Yields both components of a two-array tuple result (e.g. modf, polydiv, qr, eig, slogdet, average(returned)).
public (T1, T2) Returns<T1, T2>((T1, T2) tuple) where T1 : NDArray where T2 : NDArray
Parameters
tuple(T1, T2)
Returns
- (T1, T2)
Type Parameters
T1T2
Returns<T1, T2, T3>((T1, T2, T3))
Yields all three components of a three-array tuple result (e.g. svd).
public (T1, T2, T3) Returns<T1, T2, T3>((T1, T2, T3) tuple) where T1 : NDArray where T2 : NDArray where T3 : NDArray
Parameters
tuple(T1, T2, T3)
Returns
- (T1, T2, T3)
Type Parameters
T1T2T3
Returns<T1, T2, T3, T4>((T1, T2, T3, T4))
Yields all four components of a four-array tuple result (e.g. lstsq).
public (T1, T2, T3, T4) Returns<T1, T2, T3, T4>((T1, T2, T3, T4) tuple) where T1 : NDArray where T2 : NDArray where T3 : NDArray where T4 : NDArray
Parameters
tuple(T1, T2, T3, T4)
Returns
- (T1, T2, T3, T4)
Type Parameters
T1T2T3T4
Suspend(NDScope)
Uninstalls scope from the current thread WITHOUT disposing it
(weaver seam): emitted immediately BEFORE the builder's
Await[Unsafe]OnCompleted — once that call is made the continuation may already
be resuming on another thread, so the scope must be off this thread's chain first.
Everything tracked stays alive until the state machine completes. Also the deferral
seam of ReturnsTask<TResult>(Task<TResult>). No-op for null/disposed.
public static void Suspend(NDScope scope)
Parameters
scopeNDScope