Change owners, step through generations, pinning and finalization, or compare memory under load. The model explains concepts; the SDK examples measure real runtime observations.
Open the Memory Lab ↗The memory contract
An allocation counter and a retained-memory measurement answer different questions. GC.GetAllocatedBytesForCurrentThread reports managed allocation attributed to the current thread over its lifetime; a difference between two observations can describe a bounded interval. It does not report how many bytes survived collection, include every native allocation, or automatically summarize work on other threads. Availability and accounting details depend on the target. A useful experiment records the runtime and measurement scope instead of treating one numeric value as a universal property of a source-code line.
Trace the actual program
The sample reads the counter, creates a 256-element byte array, and reads the counter again before formatting its output. KeepAlive keeps the payload observably needed through that boundary. The printed delta is deliberately runtime-dependent and has no fixed expected value. If the platform reports the counter as unsupported, the program prints an explicit unavailable label. The requested payload length is not a promise that the delta equals 256, because headers, alignment, implementation behavior, and surrounding runtime effects can influence the observation.
How the mechanism works
Keep a micro-observation synchronous if the metric is tied to a thread. An await can resume elsewhere in some runtimes, making before-and-after thread totals an inappropriate comparison. Warmup, JIT or interpreter activity, generic initialization, and measurement instrumentation can also contaminate small intervals. Formatting the report after the second observation avoids including that report's string work in this particular interval, but it does not create a complete benchmark harness. Multiple controlled runs and target-aware tools are needed before claiming a stable optimization benefit.
Boundaries and misconceptions
A smaller delta is not automatically better if it increases retained memory, hides required work, or changes correctness. Pooling can reduce one allocation metric while keeping more backing storage alive. Likewise, GC.GetTotalMemory and operating-system process memory have different meanings from this per-thread counter. Do not compare their numbers as though they should match. SharpRune's browser observation must be labeled as such; a desktop CoreCLR result may be a useful separate experiment, but it is not the app's reported measurement.
Practice: change one ownership path
First change only the requested payload to 512 elements and record the new observation on the same target. Do not assert a particular numeric difference across all runtimes. Hint: retain the same placement of the two counter reads and the KeepAlive boundary. Repeat a small bounded number of times and note variation instead of selecting only a favorable run. State which parts of the program are inside the measured interval and which report formatting remains outside it.
Practice: test a second boundary
Next place a bounded loop of several allocations inside the interval and retain them in a local list until the final observation. Hint: make the logical workload explicit and small; do not create memory pressure merely to force a dramatic number. Compare an equivalent workload using a deliberately owned reusable buffer, checking outputs before performance. Record allocation rate and retained capacity separately. If the API is unsupported on a target, preserve that result honestly and select a supported diagnostic tool rather than inventing a substitute number.
Review the evidence
A useful result includes runtime identity, build configuration, workload, metric definition, interval, and observed variability. This sample intentionally exposes an observation rather than an exact-output assertion. It teaches how to ask a narrow allocation question while keeping retention, native resources, process memory, and correctness distinct. Before recommending a change, reproduce the actual symptom and choose evidence that corresponds to it. A single unqualified byte number is much weaker than a documented measurement with clear limits. Report unsupported or surprising observations without replacing them with an expected textbook object size.
- The numeric delta is observational and intentionally has no fixed expected output.
- Allocated bytes are not retained bytes or process memory.
- A thread-scoped metric needs a matching synchronous measurement boundary.
A working example
using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Buffers;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
try
{
long before = GC.GetAllocatedBytesForCurrentThread();
byte[] payload = new byte[256];
long after = GC.GetAllocatedBytesForCurrentThread();
Console.WriteLine($"observed-allocated-bytes:{after - before}");
GC.KeepAlive(payload);
}
catch (PlatformNotSupportedException)
{
Console.WriteLine("allocation-counter-unavailable");
}
Generation counts and allocation measurements can vary. Follow the lesson’s observation steps rather than matching a fixed transcript.
The numeric delta is observational and intentionally has no fixed expected output.