Repository navigation
Speed up the portable 4x4 inverse, adjugate and quaternion multiply - #76
Merged
Merged
Conversation
Guided by the Benchmarks package: - 4x4 adjugate, determinant and portable inverse now share one Laplace expansion by 2x2 minors (12 shared factors) instead of re-deriving every 3x3 minor and computing the determinant separately, and build the result from columns instead of 16 element-wise subscript writes. adjugate is FirebladeMath's own code on every platform. - The portable quaternion Hamilton product uses four 4-wide multiply-adds instead of 16 scalar multiplies. Mat4x4f p50 (M1 Max): portable inverse 21 -> 16 ns, adjugate 17 -> 15 ns (both paths), portable Quat4f multiply 13 -> 12 ns. A fully vectorized adjugate measured slower (16/17 ns) and was not kept. Tests: Matrix4x4CofactorTests compares adjugate, determinant and inverse against an independent cofactor expansion on 32 matrices in Float and Double, plus a singular matrix. Claude-Session: https://claude.ai/code/session_011WSUtoLptjea3s3iC5wTNC
The Swift 6.1 and 6.2 compilers (Linux, Windows, macOS 6.1 CI) failed with "unable to type-check this expression in reasonable time" on the determinant sum in adjugateAndDeterminant and on the vectorized quaternion product. Explicitly type every intermediate and split the sums into short sub-expressions. Grouping the quaternion product as (term0 + term1) + (term2 - term3) also shortens its dependency chain: portable Quat4f.multiply 12 -> 10 ns, portable Mat4x4f.inverse 16 -> 15 ns (p50, M1 Max). Claude-Session: https://claude.ai/code/session_011WSUtoLptjea3s3iC5wTNC
A mutation run over the changed code (22 injected single-point bugs) showed that three faults in the portable Quat4d Hamilton product went unnoticed: the existing Quat4d tests only multiplied identity by i. QuaternionMultiplicationTests checks Float and Double against an independent scalar reference on 64 pseudo-random pairs, the basis rules (i*j = k, j*i = -k, i*i = -1, ...) and |p*q| = |p|*|q|. With it, all 22 mutations are caught. Claude-Session: https://claude.ai/code/session_011WSUtoLptjea3s3iC5wTNC
PortableVsSIMDTests runs FirebladeMath's portable 4x4 inverse, determinant, adjugate and quaternion multiplication (Float and Double) against Apple's simd, which is what FirebladeMath forwards to when the simd trait is on. Inputs: 256 pseudo-random matrices / quaternion pairs plus identity, diagonal, permutation, rotation+translation, small and large scale, singular matrices (determinant only) and the quaternion basis. simd has no adjugate, so the reference is inverse * determinant. Tolerances are ~10x the worst measured rounding difference. The suite is only compiled when simd is importable but the trait is off, so the macOS CI job gets a second test run with --disable-default-traits. On its own, the suite catches all 22 injected single-point bugs in the changed code. Claude-Session: https://claude.ai/code/session_011WSUtoLptjea3s3iC5wTNC
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Optimizations chosen by the benchmarks from #75. Only FirebladeMath's own implementations are changed.
Changes
adjugate,determinantand portableinversenow share one Laplace expansion by 2×2 minors, a new internaladjugateAndDeterminanthelper. It computes 12 shared sub-determinants once and builds the result directly from columns.inversecomputed a full adjugate (about 48 2×2 minors, many of them duplicates) plus a separate full determinant.adjugateis FirebladeMath's own code on every platform.determinantandinversechange only on the portable path; with simd they still forward tosimd_determinantandsimd_inverse.multiply: the Hamilton product is now four 4-wide multiply-adds instead of 16 scalar multiplies.Benchmark results
M1 Max, Swift 6.4, p50 over 3 runs each. Allocations stay at 0 everywhere.
Mat4x4f.inverse(portable)Mat4x4f.adjugate(simd and portable)Quat4f.multiply(portable)The benchmark harness itself costs about 5 ns per matrix and 3 ns per quaternion; I measured that with temporary pass-through benchmarks. Subtracting it, the portable inverse's own computation went from ~16 to ~10 ns (−37%), the portable quaternion multiply from ~10 to ~7 ns, and the adjugate is now on par with Apple's simd.
Tried and discarded: a fully vectorized adjugate (three 4-wide multiply-adds per column) measured slower, 16/17 ns. The scalar form is already vectorized well by the compiler.
Unchanged and not worth pursuing:
Quat4f.actNormalizeis 27 ns on both paths and dominated bynormalize's square root and division. The other benchmarks are already on par with simd.Compatibility with older compilers
Swift 6.1 and 6.2 (the Linux and Windows CI jobs, and macOS 6.1) couldn't type-check the long generic determinant sum or the vectorized quaternion product in reasonable time. Every intermediate is now explicitly typed and split into short sub-expressions, following the approach from #59. Grouping the quaternion product as
(term0 + term1) + (term2 - term3)also shortened its dependency chain, which is where the extra speed in the table comes from.Tests
PortableVsSIMDTests: simd as the reference. It compares the portable 4×4inverse,determinantandadjugateand quaternionmultiply(FloatandDouble) against Apple's simd, which is exactly what FirebladeMath calls when thesimdtrait is on.inverse · determinant.Doubleinverse 5e−13 andFloatinverse 2e−4.--disable-default-traits.Matrix4x4CofactorTests: compares against an independent cofactor expansion on 32 matrices, plus a singular matrix.QuaternionMultiplicationTests: compares against an independent scalar Hamilton product on 64 pairs, plus the basis rules (i·j = k,j·i = −k,i² = −1) and|p·q| = |p|·|q|.inverse.PortableVsSIMDTestsalone also catches all 22.Quat4dbugs went unnoticed, because the oldQuat4dtests only multiplied identity byi.check-inlinableconfigurations pass, and lint is clean.The same simd comparison for all other functions that have both implementations (108 in total) will come in a separate PR.
https://claude.ai/code/session_011WSUtoLptjea3s3iC5wTNC