Performance and footprint
Every figure below is a measurement with its conditions stated, and where a change cost time to buy correctness that is said too. Read the ratios rather than the absolute times: several campaigns were run on Debug builds or on small machines, deliberately, because a paired A/B on one machine answers “did this get faster” far more reliably than an unpaired Release number on a fast one.
Against the last 3.1 set
Paired on one 32-logical-processor Linux workstation, deployed stacks, whole NC programs, steps per second. The 3.1 side is HardNc (the 3.1 default); the 3.2 side is SoftNc (the 3.2 default). Throughput on this line is 2.5×–18.8×; the ratio grows with program length and mesh fineness. Memory is mixed: a cutting-dominated long program peaked higher on 3.2, a large-surface short-NC job peaked lower.
The six-core ceiling that used to cap the 3.1 pipeline is not where that gap comes from. On the same workstation, pinning 3.2's worker counts to the products of that ceiling moved a ~193,000-step play from 15.0 s to 15.9 s (+6%); at six available cores the version advantage is still 2.8×–3.8×, and at two cores 2.73×. 3.1 saturates from eight cores; 3.2 from twelve. On that fixture, 3.1 allocated 6–8× more managed memory, and the 3.1 allocation climbed as the mesh got finer while 3.2 stayed almost flat.
Conditions. Deployed x64 stacks, one machine, whole programs, steps/s (not wall-clock — a defect that inflated 3.2 step counts on an earlier drop is fixed on this line). Core-count figures used
DOTNET_PROCESSOR_COUNTplus CPU affinity, so they are “restricted cores on a large machine”, not a small one: memory bandwidth and cache stay those of the 32-LP host.
Milling physics in the native kernel
The per-step physics moved into core.dll in stages — engagement scan conversion, the force kernel,
then the sequential temperature and wear chain. Measured as a same-day paired A/B, managed leg
against native leg, on one circular test program:
| Per touched step | managed | native | |
|---|---|---|---|
| Engagement build | 46.0 ms | 5.03 ms | ~9× |
Force (GetMillingFoce) |
17.1 ms | 0.75 ms | ~20× |
| Temperature and wear chain | 3.97 ms | 0.24 ms | ~16× |
| Whole play | 59.3 s / 16.1 GB allocated | 40.6 s / 3.4 GB allocated | −32% wall, −79% allocation |
Allocation is where the migration bites hardest: engagement construction alone fell from about 5.2 GB to 29 MB per step.
Conditions. Debug x64,
EnablePhysicson, collision off, one force worker, a two-core / four-thread laptop, ±10% thermal-throttle noise, single paired run per leg. Absolute times are not representative of a customer machine — the ratios are the claim.
Numerical parity. Engagement, force and brief are bit-identical between the two legs on Windows. The Linux build is not bit-identical (different libm), so a cross-platform comparison should use a tolerance, not equality.
Playing a long program stays linear
NcOptOption.Equals ended on a null-propagating comparison of a dictionary that is created on
demand and is null on virtually every option, so the whole comparison collapsed to false — an
option compared unequal even to a copy of itself. Both record-on-change guards built on it were
therefore dead: the session appended an option-map entry for every played act instead of only at
change points, and the step rewrote unchanged entries. Reading the last recorded option through a
LINQ LastOrDefault over a SortedList<,> — which has no indexed fast path — then walked the whole
map each time, so the two defects together made a long play quadratic.
Measured on a 2.35-million-line Siemens program: the option map now holds 1 entry instead of one per line, and the per-100,000-line rate stays flat instead of degrading from 73 s at the start of the file to about 11 minutes by 1.9 million lines.
This applies to every runner and to sessions doing no optimization at all, because the call site
is the session-level play loop. GetHashCode drops the dictionary in the same change, since it
hashed by reference and would otherwise disagree with Equals — relevant if you use NcOptOption
as a dictionary key.
Where the time actually goes
Worth knowing before you tune anything. After the migration, on the measured workload the whole parallel physics stage is about 3.3% of wall time, while the single-worker volume subtraction is about 77% — and that subtraction is single-worker as a correctness requirement, not as an oversight. Raising the force-worker count therefore buys nothing on any machine; the bottleneck moved rather than disappearing.
What did change in the worker derivation is narrower than it sounds. The six-core ceiling was removed, and on a 32-logical-processor machine that is about 6% of the 3.1→3.2 wall-time gap measured above — force workers are unchanged everywhere, and the sweep-worker count only differs on machines with nine or more logical processors. The rest of the gap is allocation and the rest of the pipeline, not the extra cores.
Queue depths became fixed item budgets (120 geometry, 3840 physics) rather than scaling with the core count, because those queues bound per-item memory: uncapped, a 64-core machine would have been handed 40,960-deep physics queues. On machines with fewer than six cores this is a small increase in bounded-queue memory (from 80 / 2560), which is the honest cost of the change.
Loading a large STL workpiece
Building the topology from an STL was quadratic in triangle count — a pointer-derived hash collapsed into a handful of buckets, so lookups degenerated into linear scans. On one 935,000-triangle binary STL, topology construction was 99.6% of the entire load; reading the file off disk was 0.16% of it. With a multiplicative hash mix the build is linear:
| Triangles | before | after | worst bucket | |
|---|---|---|---|---|
| 100,000 | 8,711 ms | 1,305 ms | 6.7× | 2,245 → 21 |
| 300,000 | 89,036 ms | 4,750 ms | 18.7× | 6,593 → 23 |
Per-triangle cost is now flat (0.013 → 0.016 ms/tri across a 3× size increase), which is the real result: the cost grows with the mesh instead of with its square. Extrapolated to the full 935,000-triangle mesh that is roughly 14 minutes → 15 seconds.
Deduplication and the resulting topology are unchanged — the equality predicate was not touched, and the 300,000-triangle case produces an identical 899,997 lines before and after. Separately, the managed-to-native STL handoff dropped from three full copies of the buffer (about 86 MB each, plus around twenty doubling reallocations) to two.
Conditions. Native test harness, debug CRT — which inflates container-operation constants, so the absolute milliseconds are an upper bound. The composition breakdown and the complexity change are build-configuration independent. The full-mesh figure is an extrapolation, not a measured run.
Re-triangulating after a cut
The marching-cubes step gained a lookup table, and produces fewer triangles for the same surface:
| Tree | before | after | triangles | |
|---|---|---|---|---|
| 17 MB diagnostic | 0.63 s | 0.29 s | 2.17× | −35% |
| 30 MB demo | 1.29 s | 0.60 s | 2.15× | −35% |
| 309 MB customer part | 14.68 s | 6.51 s | 2.25× | −43% |
Scope. This lands on the rebuild burst after a cut invalidates cached geometry, not on steady-state rendering, which draws from the display cache and is unchanged.
It is also an approximation change, not purely a speedup: a non-finite cut drops its triangles, so a sub-voxel feature vanishes at that level of detail instead of being capped. That is what fixed the broken-face slabs seen on RTCP paths. The contact-loop extraction used by milling physics deliberately still uses the previous walk, so physics results are untouched.
Session memory: a long program no longer exhausts the client
A session retains every executed NC block for its whole lifetime. Once a block leaves the executing window its piece is now frozen to compact UTF-8.
Measured on a 25,018-block play: session retention 406 MB → 142 MB, about 2.9×.
The encoding itself is smaller than that ratio suggests — roughly 12 KB per line live against 1.6 KB frozen, about 12.9× — because a retained piece carries more than its JSON. The 2.9× is the figure that matters for whether a program fits in memory.
The trade is explicit: after the freeze the JsonObject getter re-parses on every call and returns a
fresh read-only snapshot, with no caching and no write-back. Code that reads the same piece
repeatedly should hold the snapshot in a local. The switch is
FreezeExecutedPieces, on by default.
Sizing a meshed workpiece
A cube tree costs roughly 3.2× its file size in RAM while loaded — a 10 GB .wct at 0.125 mm
resolution is about 95 million nodes, holding around 25 GB of tree plus 6 GB of index. Tearing down a
tree that size used to block the caller for over two minutes; disposal now runs serialized on a
background chain, so the thread that dropped it does not wait. The remaining cost is genuine work:
the live renderer must not be left showing geometry that no longer exists.
Cutter-location files at production scale
Three costs were removed from the CL-to-machine path, and on a production-scale file they are the
difference between replaying and appearing to hang: tool-offset resolution walked back to the distant
LOAD block for every motion (O(N²), now O(1) through a modal section), the documentary
program-to-Pn stamp did the same walk (now stamped once per run), and the program-zero query
deep-cloned the whole equipment assembly on every motion block (now a cached per-run matrix over the
live assembly). These are complexity changes; they have not been separately timed.
Smaller footprint
The embedded default font is handed to the display engine from memory, so an 11 MB .ttf is no
longer written into the process working directory on startup. The packages are x64-only, and
HiNc-Resource no longer ships a duplicated nested copy of the CT-350 STL set.
Things that cost more, on purpose
Five-axis inverse kinematics. Tightening the orientation envelope from about 1.4e-3 rad to about
1e-6 rad — measured maxima 1.5e-8 rad on the hot path and 2.1e-8 on teleport, a tip deviation of
0.05 µm on a 50 mm tool — costs roughly eleven solver iterations where one used to do, so a
posture-changing call went from about 23 µs to about 251 µs, and a teleport from 418 µs to 1607 µs.
Only posture-changing RTCP and arc steps pay it: three-axis programs and constant-posture segments
are exempt through the McLinear downgrade. In absolute terms a 1,432-step five-axis replay spends
about 0.36 s in the solver. (Debug build including measurement overhead, so those microseconds are an
upper bound.)
Machine-coordinate linear stepping. ActMcXyzLinearContour derives its
step count from the euclidean length of the machine XYZ delta rather than the largest single-axis
component, so LinearResolution_mm caps actual tool-tip travel per step. A diagonal move therefore
produces up to √3× more steps than before at the same setting — more work, for a sampling density
that now means what the setting says. Lower the resolution if the old step count was what you were
budgeting for.
A tuning cliff worth knowing about
MillingCycleDivisionNum saturates. Raising it past roughly 180 buys no additional training accuracy
while the cost keeps climbing: a training run that takes about four minutes at 180 takes hours at 720
and needs on the order of 100 GB of RAM to do it. The default of 36 is for ordinary simulation; raise
it for training, but not past the point where the curve flattens.