Math
Basalt implements its own f32 transcendentals. Sin, Cos, Tan, Atan, Atan2, Exp, Log and Pow are written from scratch, with no dependency on the C runtime’s math library. Each one exists twice under the same name: a scalar version that takes one value, and a batch version that takes a span.
The accuracy of every function is a measured number, not a claim. Each unary function was swept over all 232 possible f32 inputs, and the worst error found is the published contract.
One name, two shapes
Section titled “One name, two shapes”The scalar and batch forms are overloads, so you pick by how much data you have:
f32 angle = Sin(t); // one value
Span<const f32> angles = /* 10 000 angles */;Span<f32> results = arena.Push<f32>(angles.size);Sin(angles, results); // the same function, eight lanes at a timeThis exists because the two cases have genuinely different best answers, not to be tidy. A single Sin call has no internal parallelism — there is nothing in one sine to vectorise. Ten thousand sines have a great deal, because eight independent inputs fit in one AVX2 register. The only way to reach that is to hand over the whole group.
Scalar Sin(f32) |
Batch Sin(Span, Span) |
|
|---|---|---|
| Accuracy | ≤ 0.5009 ULP | ≤ 4 ULP contracted |
| Width | 1 value | 8 values per step, on AVX2 |
| Speed vs the C runtime | comparable | 10.5×–27.4× on MSVC, 7.8×–17.4× on clang |
| Falls back to | — | the scalar tier, when AVX2 is absent |
Pow is the exception on speed: 4.6× on MSVC and 2.6× on clang.
Accuracy is a contract, in ULP
Section titled “Accuracy is a contract, in ULP”ULP — unit in the last place — is the distance between one f32 and the next representable one. It is the natural unit for floating-point error, because absolute error is meaningless without knowing the magnitude: an error of 0.001 is catastrophic near zero and invisible near a million.
An error of 0.5 ULP means correctly rounded: the answer is the closest f32 to the true mathematical result. No function can do better.
Scalar tier
Section titled “Scalar tier”| Function | Max ULP | Swept exhaustively | Domain |
|---|---|---|---|
Sin |
0.5009 | yes | |x| ≤ 1e5 |
Cos |
0.5009 | yes | |x| ≤ 1e5 |
Tan |
0.7998 | yes | |x| ≤ 1e5 |
Atan |
0.8521 | yes | all f32 |
Atan2 |
0.5000 | sampled | all pairs |
Exp |
0.5016 | yes | all f32 |
Log |
0.8177 | yes | x > 0 |
Pow |
0.7111 | sampled | all pairs |
Batch tier
Section titled “Batch tier”| Function | Max ULP | Swept exhaustively | Domain |
|---|---|---|---|
Sin |
1.5598 | yes | |x| ≤ 1e5 |
Cos |
1.5617 | yes | |x| ≤ 1e5 |
Tan |
3.3177 | yes | |x| ≤ 1e5 |
Atan |
1.4679 | yes | all f32 |
Atan2 |
1.7596 | sampled | all pairs |
Exp |
1.9536 | yes | all f32 |
Log |
1.7265 | yes | x > 0 |
Pow |
0.9688 | sampled | all pairs |
The two binary functions are sampled rather than swept, because 264 input pairs cannot be enumerated. That limit is stated rather than hidden.
Every figure is identical on MSVC 19.43 and clang 21.1.8 — same worst-case error, at the same input bit pattern, on both compilers.
The exact family carries no error term
Section titled “The exact family carries no error term”Seven functions are not approximations at all. Sqrt, Abs, Floor, Ceil, Truncate, Round and CopySign each compile to a single hardware instruction, correctly rounded as IEEE-754 requires.
They have no ULP figure because they have no error, and they are bit-identical on every conforming machine. They are also inline, so they cost nothing to call.
Domains, and why a violation gives NaN
Section titled “Domains, and why a violation gives NaN”Each function states the range it is contracted over.
| Function | Contract | Outside the contract |
|---|---|---|
Sin Cos Tan |
|radians| ≤ 1e5 | Asserts in debug; quiet NaN in release |
Atan, Atan2 |
all inputs | — |
Exp |
all inputs | +inf on overflow, +0 on underflow |
Log |
x > 0 | Log(±0) is -inf; Log(x<0) is NaN |
Pow |
all pairs | The full C99 special-value table |
The trig limit of 1e5 radians is about 15 915 full turns. A caller with a larger angle wraps it first.
Two decisions inside that table are worth understanding:
- The limit is the range that was measured, not the range the algorithm survives. The reduction stays sound much further, but letting the scalar version answer beyond where the batch version is gated would make the result depend on which overload you happened to call.
- A violation returns NaN rather than a best-effort number. Past roughly 4.3e9 the reduction saturates and the polynomial overflows to
inf— a value outside sine’s range that propagates silently through clamps and comparisons. NaN is louder. A domain violation is a programming error, so debug builds assert, and release builds refuse to invent a plausible answer.
Special values follow C99 exactly and are verified bit-for-bit, including signed zeros: Sin(-0.0f) is -0.0f.
Why these functions are compiled, not header-inline
Section titled “Why these functions are compiled, not header-inline”Every other numeric helper in Basalt is inline. The eight transcendentals are not: they live in a .cpp and you link them.
The reason is that a client build can silently destroy them. Compiling with /fp:fast — or -ffast-math — permits the compiler to re-associate floating-point arithmetic. The polynomial kernels depend on exact operation order to keep their error terms small. Measured: under /fp:fast, Exp and Pow degrade from under 1 ULP to about 41 000 ULP.
No portable pragma reliably defends a header against the flags of the translation unit that includes it. Compiling the kernels once, in a unit whose own flags are controlled, is the only mechanism that holds. This is the same reason the system libm ships as a binary rather than a header.
Instruction-set dispatch
Section titled “Instruction-set dispatch”The batch tier uses AVX2 where the processor has it. The decision is made once at run time, by querying CPU features — you do not compile a separate build for it, and you do not need to check anything at the call site.
When AVX2 is absent, the batch functions fall back to the validated scalar tier. So correctness never depends on the instruction set — only speed does.
if (IsBatchMathAccelerated()){ // the 8-wide kernels are in use}Two instruction-set levels are not optional, and they are not the same kind of claim. SSE2 is what the x86-64 ABI itself guarantees, and the transcendentals need nothing beyond it. SSE4.1 is a requirement Basalt imposes on top of that, because the exact family compiles to roundss, an instruction SSE4.1 introduced. It is not probed for at run time — it is a build prerequisite.
Constants and bit access
Section titled “Constants and bit access”Angles are in radians everywhere, with DegreesToRadians and RadiansToDegrees at the boundary where humans write numbers.
BitsFromFloat and FloatFromBits reinterpret an f32 as a u32 and back, without the undefined behaviour of a pointer cast. They are the basis of the classification helpers and are useful for exact comparison in tests.
API summary
Section titled “API summary”Accuracy, domain and special-value behaviour for each function are documented in basalt/core/Math.h.
Constants
Section titled “Constants”| Name | Value |
|---|---|
PI, TAU, HALF_PI, QUARTER_PI, INVERSE_PI |
Circle constants. TAU is a full turn. |
EULER, LN_2, SQRT_2 |
Common irrationals. |
F32_MAX, F32_MIN, F32_EPSILON |
Range and precision limits. |
Exact — one instruction, no error
Section titled “Exact — one instruction, no error”| Signature | Purpose |
|---|---|
Sqrt(v) |
Square root. |
Abs(v) |
Absolute value. |
Floor(v) / Ceil(v) / Truncate(v) / Round(v) |
Rounding, four ways. |
CopySign(magnitude, sign) |
The magnitude of one value with the sign of another. |
Classification and bits
Section titled “Classification and bits”| Signature | Purpose |
|---|---|
IsNaN(v) / IsInfinite(v) / IsFinite(v) |
Classification. |
IsNegative(v) |
Sign-bit test. Distinguishes -0.0f from +0.0f. |
QuietNaN() / Infinity() |
Produce the special values. |
BitsFromFloat(v) / FloatFromBits(bits) |
Reinterpret between f32 and u32. |
DegreesToRadians(d) / RadiansToDegrees(r) |
Angle conversion. constexpr. |
Transcendentals — scalar
Section titled “Transcendentals — scalar”| Signature | Domain |
|---|---|
Sin(radians) / Cos(radians) / Tan(radians) |
|radians| ≤ 1e5 |
Atan(v) |
all f32 |
Atan2(y, x) |
all pairs |
Exp(v) |
all f32 |
Log(v) |
v > 0 |
Pow(base, exponent) |
all pairs |
Transcendentals — batch
Section titled “Transcendentals — batch”| Signature | Purpose |
|---|---|
Sin(in, out) / Cos(in, out) / Tan(in, out) |
8-wide trig over spans. |
Atan(in, out) |
8-wide arctangent. |
Atan2(y, x, out) |
Two input spans, one output. |
Exp(in, out) / Log(in, out) |
8-wide exponential and logarithm. |
Pow(bases, exponents, out) |
Two input spans, one output. |
IsBatchMathAccelerated() |
Whether the wide kernels are active on this machine. |