Repository navigation
Description
Typed numeric Array.pop() returns its value through Dynamic, allocating a numeric box even when the generated caller immediately converts the result back to Int or Float. Stack-like workloads pay allocation and GC costs for values already stored unboxed in the array.
Measured on Windows x64, MSVC 19.44.35213, Haxe 4.3.7 and libhxcpp d734565, release build with HXCPP_M64 and the default collector:
| Remove one million last elements | Int | Float |
|---|---|---|
| Typed pop(), then accumulate | 6.2797 ms | 5.8961 ms |
| Read last element, resize length down by one, then accumulate | 1.9848 ms | 2.1693 ms |
A second run gave 6.3431 vs 1.9621 ms for Int, and 5.7814 vs 2.1456 ms for Float. These are focused operation measurements rather than application-wide speedup estimates. The indexed control is about 3.2x and 2.7x faster in the first run; it is a comparison of current operations, not a measurement of an implemented fix.
Each case prepares the same million-element array outside the timer, runs a full collection before measurement, drains the array, and verifies its checksum and final length. GC remains enabled. The median is taken from seven samples after one warm-up. Integer values are 1000 + (i & 1023), outside the small integer cache. Float values are 1000.25 + (i & 1023). The timed pop loop explicitly assigns the return value to a typed local:
while (values.length > 0) {
var value:Int = values.pop();
sum += value;
}The control is:
while (values.length > 0) {
var value = values[values.length - 1];
values.resize(values.length - 1);
sum += value;
}In include/Array.h, ReturnNull, ReturnNull and ReturnNull select Dynamic. Array_obj<ELEM_>::pop reads the typed element, clears the removed slot through resize, then converts that element to the Dynamic return type. Dynamic's constructors in src/Dynamic.cpp allocate numeric objects for these tested values. The generated C++ caller is simply int value = (int)(values->pop()), or its Float equivalent, so the box is immediately unwrapped.
Investigate a runtime-header nullable numeric result that carries null/value state without allocating, converting to Dynamic only when the caller needs an actual dynamic value. The generated typed call above should be able to use a compatible result without a compiler or standard library change. Confirm compatibility across generated call sites before selecting a representation. Reflective __pop and CPPIA entry points still need their existing Dynamic semantics. Do not replace an empty result with numeric zero. Array.shift shares ReturnNull, but shift throughput was not measured here.
Conventional local coverage was added at test/regression/NumericArrayPop/{Main.hx,build.hxml,stdout.txt}. It passes on the current runtime and checks integer boundaries, zero and cached values, fractional floats, NaN, infinities, negative zero, Bool false versus empty null, exact Int64 values above 2^53, nullable elements and empty results. It contains no timing assertions. Add reflected calls and CPPIA coverage if the return representation changes.
Related: #294 concerns boxing inputs to specialized dynamic-array push. This issue concerns nullable outputs from typed arrays. No runtime implementation change has been made.
Activity
dimensionscape commentedon Nov 7, 2026
Moving this past 6.0.0. I tried a return type that keeps the value unboxed, and it can't replace Dynamic without a change in the generator.
The generator emits
pop()andshift()of a typed array in about 25 shapes. Only the cast,( (int)(a->pop()) ), would gain. The rest use the result as a Dynamic:::Dynamic n = a->pop(),takeDyn(a->pop()),Std.string(a->pop())a->push(a->pop()), which converts the Dynamic to the element typeHX_("v") + a->pop(),a->pop() + 1,x = (x + a->pop())::hx::IsNull,IsEq,IsNotEqandIsGreateron it,setFixed(..., a->pop())andswitchon itA
NullReturn<T>holding the value and a null flag, converting implicitly to bothTand Dynamic, compiles most of these. Two broke.setFixedtakes acpp::Variant, two conversions away.int + pop()is ambiguous between the built-in+and Dynamic's.Both are fixable, but the type would then have to give the same results as Dynamic in every operator. Dynamic's are uneven.
int + Dynamicis adouble, whileDynamic + intis a boxed Dynamic that stays an Int. Null is 0 in arithmetic and "null" in a string. A type that differs in any of them changes what generated code computes, with nothing at compile time to show it. Native code that callspop()on a typed array and uses the result as a Dynamic (.mPtr,->) would stop compiling.The cast is the one shape that wants the unboxed value, and only the generator knows it is there. With a typed pop in the runtime (
__popInt, say), Haxe could call it when it unwraps the result straight away. The runtime side is small once the generator change exists.