Skip to content

scalar CRT math where it wins; float2string via Luau's emitter - byte-identical, 2.4x - #3908

Merged
borisbat merged 9 commits into
masterfrom
bbatkin/scalar-math-crt
Aug 31, 2026
Merged

scalar CRT math where it wins; float2string via Luau's emitter - byte-identical, 2.4x#3908
borisbat merged 9 commits into
masterfrom
bbatkin/scalar-math-crt

Conversation

@borisbat

Copy link
Copy Markdown
Collaborator

Behavior change: scalar float exp/exp2/log2/pow results shift a few ulps in interp and AOT (they now call the CRT, matching the JIT bit-for-bit; log2 was a 5th-order estimate and is now exact); float-to-string keeps its spelling byte-for-byte but runs ~2.4x faster; a -nan float literal in a Metal kernel is now rejected instead of emitting invalid MSL.

Two wins the dasProfile records asked for, one borrowed emitter, and the review round's hardening.

What changed, numbers, validation

Scalar math: CRT where it wins

SimPolicy<float>::Exp/Exp2/Log2/Pow scalar arms call expf/exp2f/log2f/powf; the vec4f arms stay on vecmath, where four lanes pay for the setup. Winner table from a two-box probe of the exact AOT shape (ns/iter, 1e6 inputs, best of 7; lane = the old v_extract_x(...(v_set_x(x))) composition):

fn 3990X MSVC SSE2 lane / CRT M1 clang lane / CRT kept
exp 9.45 / 3.26 (records: AOT 2.2x over JIT) CRT
exp2 4.50 / 2.47 2.27 / 1.51 CRT
pow 11.72 / 11.48 5.64 / 3.46 CRT (exact vs estimate at tie speed)
log2 2.31 / 2.71 1.93 / 1.96 CRT - v_log2_est_p5 was an estimate where the JIT is exact; the ~0.4 ns x64 cost is ledgered in include/daScript/simulate/ARCHITECTURE.md (sanctioned hot-path additions)
log / sin / cos / tan lane wins lane wins vecmath lane

modules/dasUnitTest binds the 8 CRT twins (crt_expf...); benchmarks/core/math/scalar_crt.das races every pair (interp + JIT), benchmarks/core/math/exp.das carries the exp rows; tests/math/test_crt_twins.das pins the switched builtins to the CRT exactly and the twins against the double CRT. The two benchmark arms of scalar_crt.das share their loop shape deliberately (the exp.das row pattern).

Full re-profile (dasProfile, 5 samples/lane, cold boxes) - das rows that moved: zen2 exp loop AOT 9215 -> 3412 us (level with JIT 3383, C++ 3240); M1 exp loop interp 9279 -> 7347 us. The M1's AOT exp loop moved only 3592 -> 3414 with exp verifiably CRT - the residual is the AOT loop shape (das_iterator + the kept rcp_est lane), ledgered in plans/benchmark_followups.md.

float2string: Luau's emitter over fmt's dragonbox

The presentation half of Luau's lnumprint.cpp (MIT; src/misc/LUAU.LICENSE vendored, installed as LUAU.LICENSE, smoke-list enforced) drives the dragonbox digits fmt already instantiates; fmt's crossover and spelling are kept. Both sinks swap: string(f) (das_lexical_cast_fp_f/_d) and "{f}" (StringWriter); the {:.9} FIXEDFP path stays on fmt. One change covers all three tiers (the JIT calls the runtime).

  • Byte-identity proven: tests-cpp/small/test_float2string.cpp - sampled sweep per PR (~460k floats + 2M doubles + specials + 2-digit-scientific boundaries), and an exhaustive arm over every finite float32 (all 2^32 bit patterns) gated by DASLANG_F2S_EXHAUSTIVE=1. No CI lane sets the env; run manually on both boxes: DASLANG_F2S_EXHAUSTIVE=1 bin/Release/tests-cpp-small.exe --test-case="*every finite float32*" - 3990X 128 workers all clean, M1 10 workers all clean.
  • Special values classify from the IEEE bits, not isfinite/isnan/signbit - a -ffinite-math-only build (the Dagor/EASTL config) folds those; src/misc/REVIEW.md now bans them there.
  • DAS_F2S_BUFFER_SIZE (48) in float2string.h sizes every caller's buffer, so the overshoot invariant propagates by compilation.
  • Records: float2string zen2 interp/AOT/JIT 4252/3636/3685 -> 2640/2276/2273 us; M1 1824/1640 -> 993/1091 us (AOT/JIT) - das is now the fastest float2string lane on both boards, ahead of Luau --codegen (zen2 2530, M1 2047).

Metal

msl_float_literal rejects non-finite values via is_finite(v) instead of matching printed spellings (the swap surfaced that -nan - x86's default quiet NaN under fmt's sign handling - slipped through as -nanf). tests/msl/_fail_closed/_fc_nan_literal.das + its needle pin the rejection; mutation-verified (removing the guard went green before, red now).

Validation

  • make-pr chain green: sync (20 files), REVIEW.das gates, review-md walk, dupes (only the benchmark's own paired arms), jit-smoke; preflight --full 18 passed / 0 failed / 4 environmental skips (no clang-cl on the box; DLL-flavor relink gates - CI covers them).
  • Suites re-run on the final binary: tests/msl 99/99 (with the new needle), tests/glsl 121/121, tests/math 135/135 (with the twins pins), tests/debug sprint pins 31/31; two full interp sweeps earlier in the arc (14073/14082, 0 failed); tests-cpp small 32 + 128 assertions green.
  • Review round: five checklist auditors, tdd (16 emitter mutations, every one caught by the byte-identity test; the three clusters it flagged untested are all pinned in this PR), style-hygiene, codex, and two dragon passes over the checklist edits.
  • dasProfile records for both boxes captured on this branch's SDK; they go to borisbat/dasProfile after this merges (same order as the bench-parity arc).

Claims - stated, not tested

  • Perf numbers are MSVC/3990X and Apple-clang/M1; glibc was not probed (CI validates correctness there; the winner set is a single global choice by the two-box ruling).
  • Scalar log2/pow are now exact while the float4 arms keep the vecmath estimates - float and float4 diverge for those two; deliberate, ledgered, the vec arms are the follow-up if it bites.
  • exp_std stays bound although it now equals exp - it is the extern-vs-intrinsic A/B row under JIT.
  • DASLANG_F2S_EXHAUSTIVE has no row in skills/internal/environment_variables.md - that index covers variables daslang itself reads; test-binary knobs (DASLANG_HUGE_HEAP_TESTS precedent) stay out.

Not done

  • The M1 AOT exp-loop residual (loop shape, not math) - plans/benchmark_followups.md.
  • The vec4f estimate arms (v_log2_est_p5, v_pow) stay; ledgered.
  • exp_est as a JIT intrinsic (sub-ns, vectorized) - ledgered, no consumer today.

🤖 Generated with Claude Code

https://claude.ai/code/session_01J97ymQXmMxGTmHYUhLrgVd

borisbat and others added 9 commits August 30, 2026 12:44
…wins the benchmarks race

The vecmath lane trick (extract, set_x, polynomial, extract) is 2-3x the CRT on the scalar path:
the exp loop's 9.45 ns/iter against expf's 3.26 under the AOT unit's own flags on the 3990X, with
the same shape on the M1. Winners by two-box probe: exp, exp2, pow (exact at tie speed) and log2
(v_log2_est_p5 was an estimate where the JIT is exact) go CRT; log, sin, cos, tan stay on the lane
where it wins. benchmarks/core/math/scalar_crt.das races every pair through the UnitTest binds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J97ymQXmMxGTmHYUhLrgVd
…on the hot path, byte-identical

The presentation half of Luau's lnumprint.cpp (MIT, LUAU.LICENSE vendored and installed) drives the
dragonbox digits fmt already instantiates; fmt's crossover and spelling are kept, so every finite
float32 - all 2^32 of them, swept in tests-cpp - and sampled doubles print byte-for-byte what
fmt::format_to("{}") produced. Both sinks swap: string(f) in runtime_string.cpp and "{f}" in
string_writer.cpp; the {:.9} FIXEDFP path stays on fmt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J97ymQXmMxGTmHYUhLrgVd
fmt honours the sign bit and x86's default quiet NaN is negative, so "{0f/0f}" prints -nan; the
guard compared against "nan" alone and emitted -nanf into MSL instead of raising the error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J97ymQXmMxGTmHYUhLrgVd
…nd as rules, the bit patterns get names

The test's narrowing ban lands in tests-cpp/small/REVIEW.md; the fixed-length-copy invariants in
src/misc/REVIEW.md; the fixture bit patterns become named constants; the UnitTest twins keep a
one-line contract doc; the header's buffer clause reads one way now.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J97ymQXmMxGTmHYUhLrgVd
…r constant, the -nan fixture, the twins and wiring pins, six rule documents hardened

The emitter classifies specials from the bits (a -ffinite-math-only build folds isfinite/isnan/signbit);
DAS_F2S_BUFFER_SIZE sizes every caller buffer by compilation; the msl guard asks is_finite(v) and its
rejection gets the _fc_nan_literal fixture and needle; tests/math/test_crt_twins.das pins the switched
scalars to the CRT exactly and validates all eight twin binds; sprint-format pins the lexical-cast
wiring; the byte-identity test splits by width, prints its bits in hex, names its strides, and gains
the two-digit-scientific boundaries; ci, src/misc, tests-cpp, tests-cpp/small and dasMetal checklists
re-worded under two dragon passes; the simulate ledger records the log2 trade.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J97ymQXmMxGTmHYUhLrgVd
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J97ymQXmMxGTmHYUhLrgVd
…s live in the ledger

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J97ymQXmMxGTmHYUhLrgVd
Copilot AI lite review requested due to automatic review settings August 30, 2026 21:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Aligns scalar float transcendental math across tiers by switching selected scalar ops to CRT implementations, and replaces fmt’s default float-to-string path with a faster emitter that preserves byte-identical spelling; also hardens the Metal MSL emitter to reject non-finite float literals.

Changes:

  • Switch scalar float Exp/Exp2/Log2/Pow in SimPolicy to CRT (expf/exp2f/log2f/powf) while keeping vec4 paths on vecmath.
  • Introduce float2string/double2string (Luau-derived presentation + fmt dragonbox digits) and route default float/double formatting through it in runtime sinks.
  • Add/extend tests and “fail-closed” fixtures, plus license + bundle-gate updates for the vendored Luau emitter code.

Reviewed changes

Copilot reviewed 28 out of 28 changed files in this pull request and generated no comments.

Show a summary per file
File Description
tests/msl/test_msl_fail_closed.das Adds a fail-closed assertion for non-finite float literals in MSL emission.
tests/msl/_fail_closed/_fc_nan_literal.das New fixture that triggers a non-finite float constant for MSL rejection.
tests/math/test_crt_twins.das New tests pin scalar math builtins to CRT “twin” functions.
tests/debug/test_sprint_format.das Adds a small pin for default float/double string spelling via formatting + string(...).
tests-cpp/small/test_float2string.cpp New C++ tests enforcing byte-identical output vs fmt for float/double conversion (plus exhaustive optional sweep).
tests-cpp/small/REVIEW.md Documents that weakening the byte-for-byte float2string vs fmt comparison is a defect.
tests-cpp/REVIEW.md Broadens checklist rule for any conditionally-skipping C++ tests (env vars, missing artifacts, etc.).
src/simulate/runtime_string.cpp Routes float/double lexical casts through float2string/double2string.
src/misc/string_writer.cpp Routes default (non-fixed) float/double StringWriter output through float2string/double2string.
src/misc/REVIEW.md Adds guardrails for fixed-size copies and special-value classification in luau_float2string.cpp.
src/misc/LUAU.LICENSE Adds Luau MIT license file for vendored emitter code.
src/misc/luau_float2string.cpp New float/double shortest-print implementation (Luau-derived emitter + fmt dragonbox digits).
plans/benchmark_followups.md Records follow-up plan for M1 AOT exp-loop residual and notes scalar/vector divergence.
modules/dasUnitTest/unitTest.h Adds inline CRT wrapper functions (crt_*f) for benchmarking/testing.
modules/dasUnitTest/test_handles.cpp Registers CRT math bindings in the UnitTest module initialization.
modules/dasUnitTest/test_crt_math.cpp Implements Module_UnitTest::addCrtMath bindings for CRT wrappers.
modules/dasUnitTest/module_unitTest.h Declares addCrtMath on Module_UnitTest.
modules/dasUnitTest/CMakeLists.txt Adds the new CRT binding source file to the module build.
modules/dasMetal/REVIEW.md Tightens/clarifies checklist wording for newly rejected constructs requiring fail-closed fixtures.
modules/dasMetal/metal/msl_emit.das Changes float literal emission to reject non-finite values via is_finite.
include/daScript/simulate/sim_policy.h Switches scalar Exp/Exp2/Log2/Pow to CRT functions.
include/daScript/simulate/ARCHITECTURE.md Documents/sanctions the hot-path scalar CRT switch and its motivation.
include/daScript/misc/float2string.h Adds public header for the new float-to-string API and buffer sizing contract.
CMakeLists.txt Wires new header/source into the build and installs the Luau license into bundles.
ci/smoke_test_bundle.sh Extends third-party license presence gate to require LUAU.LICENSE.
ci/REVIEW.md Updates checklist wording for bundle gate “must not shrink rejects” rule.
benchmarks/README.md Documents the new scalar CRT benchmark.
benchmarks/core/math/scalar_crt.das New benchmark comparing builtins vs CRT twins across scalar math functions.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@borisbat
borisbat merged commit 032fa86 into master Aug 31, 2026
37 checks passed
@borisbat
borisbat deleted the bbatkin/scalar-math-crt branch August 31, 2026 00:20
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.

2 participants