Repository navigation
Eigen 3.4.0 -> 5.0.1: no measurable performance change on Apple Silicon - #1152
Merged
Merged
Conversation
5.0.1 (2025-11-11) is the latest official release; the mirror's tag is the same commit as GitLab's, bc3b39870ecb690a623a3f49149a358b95c5781d. Nothing in wmtk uses the APIs 5.0 removed or moved (Eigen::all/last, aligned_allocator's std::allocator base). One build break, in a dependency: the pinned libigl's mat_max.cpp/mat_min.cpp, which bounding_box_diagonal.h includes in header-only mode, call assert() without including <cassert>, and Eigen 3.4 had been including it for them. tetwild's orig/State.cpp, the one unit that reaches them, now includes <cassert> first. The ABI pin (EIGEN_MAX_ALIGN_BYTES/EIGEN_MAX_STATIC_ALIGN_BYTES=16) is unchanged and still honoured: 5.0's ConfigureVectorization.h guards both with #ifndef, and Memory.h picks malloc vs handmade_aligned_malloc the same way as 3.4. Performance on an M3 Max: no measurable difference. Over all 222 ctest tests, interleaved A/B, the sum of best times is 197.4 s on 3.4.0 and 198.1 s on 5.0.1 (1.004); every heavy test is within 0.992-1.011. Single-threaded tetwild on bunny is within 1% per phase. Results are not bit-identical (last-bit differences from the first simplification on) and the final quality matches to five digits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ULwCxzbAACoHpC1mnmkvr8
Both Windows jobs of #1152 stopped in polysolve's linear/Solver.cpp: "'_MatrixType': is not a class or namespace name", then "SparseQR: too few template arguments". polysolve forward-declared Eigen::SparseQR to give it a default ordering, naming the parameter _MatrixType. MSVC resolves that default against the parameter names of Eigen's own definition -- which were _MatrixType in 3.4, and are MatrixType_ in 5. Clang and GCC look at the declaration that wrote the default, so only Windows broke. polysolve fixed it upstream in 1b01e6cec1 (polysolve#109, "Update Eigen to 5.0.1 and fix resulting MSVC SparseQR build"), the one commit after the previous pin: an alias that spells the ordering out instead of the forward declaration. Its other changes do not reach wmtk: its own eigen and json recipes return early because wmtk creates both targets first, and the new allow_non_grad_convergence option (default false) only changes the level a finished solve is logged at. Checked on macOS with Eigen 5.0.1: all 222 ctest tests pass, and serial tetwild on bunny (both workloads of #1152) writes byte-identical meshes to the old pin, at the same speed (145.8 s vs 145.9 s, 120.4 s vs 120.4 s). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ULwCxzbAACoHpC1mnmkvr8
… sliver The test failed on arm64 Release (macOS and Linux) after main was merged into the Eigen 5.0.1 branch: the worst of its 2,000 points came back off by 1.26e-6 (Apple clang) and 1.93e-6 (GCC), over the 1e-6 bound. With Eigen 3.4 the same point comes back off by 3.5e-7, so the bound held by a factor of 3. The map is the same with both Eigen versions (same operations built, same pairs, 25 inverted operations, none of them on this point's path), and so is the mechanism. The point's error stays at rounding (1e-15) back to operation 513, which raises it to 5e-13 (3.4) or 2e-12 (5.0.1), and operation 235's radial map then magnifies it about 6e5 times: its ray leaves the patch through a face 3e-6 as high as it is long, at a grazing angle, and the cap map carries the point's coordinates on that face across. That is the "rays can graze a face of a sliver" the comment names; Eigen 5 only changes the last bits that reach it. 1e-5 leaves 5x over the worst measured, and is still three orders below what a map that is not a bijection would miss by, the size of a tet, 1e-2 and up there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ULwCxzbAACoHpC1mnmkvr8
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.
Bumps Eigen from 3.4.0 to 5.0.1 (2025-11-11), the latest official release. On an M3 Max, performance is unchanged: across all 222 ctest tests, the sum of best times is 197.4 s on 3.4.0 and 198.1 s on 5.0.1 (+0.4%). Every heavy test lands within -0.8%..+1.1%, and single-threaded tetwild is within 1% in every phase. All tests pass on both versions. Results are not bit-identical. The case for merging is staying current, not speed.
Changes
cmake/recipes/eigen.cmake:GIT_TAG 3.4.0->5.0.1, still from Google's mirror. The mirror's tag is the same commit as GitLab's,bc3b39870ecb690a623a3f49149a358b95c5781d, checked withgit ls-remoteagainst both.components/tetwild/.../orig/State.cpp: includes<cassert>beforeigl/bounding_box_diagonal.h. This was the only build break with Clang, and it comes from a dependency. In header-only mode the pinned libigl (2022,3ea7f948) includesmat_max.cpp/mat_min.cpp, which callassert()without including<cassert>. Eigen 3.4 had been including it for them; Eigen 5 does not. The include gets its own block so that clang-format's include sorting doesn't move it after the libigl header.cmake/recipes/polysolve.cmake:231f7e8->1b01e6cec1(Update Eigen to 5.0.1 and fix resulting MSVC SparseQR build polyfem/polysolve#109), polysolve's own Eigen 5.0.1 fix and the one commit after the previous pin. It fixes the MSVC-only break below. Its other changes don't reach wmtk: polysolve's eigen and json recipes return early because wmtk creates both targets first, and the newallow_non_grad_convergenceoption (default false) only changes the log level of a finished solve. On macOS with Eigen 5.0.1, all 222 ctest tests pass with the new pin, and serial tetwild (W1 and W2 below) writes byte-identical meshes to the old pin at the same speed: 145.8 s vs 145.9 s, and 120.4 s vs 120.4 s. The measurements below were taken with the old pin and still apply.components/simwild/.../test_simwild_tracking.cpp:tracking-example-simwild-volume's round-trip bound goes from 1e-6 to 1e-5. After main (with Shared holding of curves and points in the 2D and 3D bases (triwild, tetwild, simwild 2D and 3D) #1150) was merged in, it failed on arm64 Release only, macOS and Linux: the worst of its 2,000 points came back off by 1.26e-6 (Apple clang) and 1.93e-6 (GCC). On the same tree with Eigen 3.4, that point comes back off by 3.5e-7, so the old bound only had a 3x margin. Both versions build the same map, with identical stats, and the same point is the worst. I traced it operation by operation. Its error stays at rounding (1e-15) until one operation raises it to 1e-12. Then a radial map leaves its patch through a sliver face, 3e-6 as high as it is long, at a grazing angle, and magnifies the error about 6e5 times. That is the mechanism the test's own comment names. None of the inverted operations is on its path. 1e-5 is still about 1000x below what a map that isn't a bijection would miss by, which is a tet's size (1e-2 and up there). The two other 1e-6 round-trip bounds (test_simwild_tracking.cpp:342,test_tetwild_tracking.cpp:354) pass everywhere and are unchanged.Why the polysolve bump. The first CI run failed only on Windows, in polysolve's
linear/Solver.cpp, with'_MatrixType': is not a class or namespace name. polysolve forward-declaredEigen::SparseQRto give it a default ordering and named the parameter_MatrixType. MSVC resolves that default against the parameter names of Eigen's own definition, which were_MatrixTypein 3.4 and areMatrixType_in 5. Clang and GCC use the declaration that wrote the default, so only MSVC broke. The other CI jobs passed on that first run: Linux x86-64 and arm64, and macOS, in Debug and Release, plus pre-commit. With the new pin (ad522bad4d), every job passed, both Windows ones included.Nothing in wmtk uses the APIs 5.0 removed or moved:
Eigen::all/Eigen::last(nowEigen::placeholders::), andaligned_allocator'sstd::allocatorbase.The ABI pin still holds.
EIGEN_MAX_ALIGN_BYTES=16/EIGEN_MAX_STATIC_ALIGN_BYTES=16are honoured by 5.0'sConfigureVectorization.hexactly as in 3.4. Both macros are guarded by#ifndef, and the only override isEIGEN_DONT_ALIGN, which nothing defines. The build reaches 413 units with the pin, 138 of them also with ipc-toolkit'sEIGEN_DONT_VECTORIZE.wmtk_test_manifold_extractionandwmtk_test_topological_offsetpass; they are the tests that caught the split ABI before. #1151 carries over. #1151 has since landed, and this branch merges main without conflicts. On x86-64 it now setsEIGEN_MAX_ALIGN_BYTESto 32 (64 for AVX-512), so every unit allocates withhandmade_aligned_mallocat one alignment. Everywhere else it stays 16, so arm64, where everything below was measured, is unaffected. 5.0.1 applies the same rules as 3.4:EIGEN_DEFAULT_ALIGN_BYTESis the larger ofEIGEN_MAX_ALIGN_BYTESand the inferred ideal,Memory.htakesmalloconly where that is 16, andhandmade_aligned_malloc/handmade_aligned_reallocdefault to the calling unit's alignment. The block layout differs: 5.0.1 stores a one-byte offset before the block where 3.4 stored the original pointer. A build only ever contains one Eigen, so this does not matter. On macOS the merged tree passes all 223 ctest tests, among them #1151'seigen-heap-one-allocator-across-units. Only CI's x86 jobs run the 32-byte path. On the merge commit (3b36a69b71), Linux x86-64 and Windows, in Debug and Release, configured with "Eigen allocates by handmade_aligned_malloc() at 32 bytes", and every CI job passed all of its tests,eigen-heap-one-allocator-across-unitsincluded.Measurements
Setup. Apple M3 Max (12P + 4E), macOS 26.6.2, Apple clang 21.0.0, Release (
-O3 -DNDEBUG), default options (all components, unit and component tests, Python bindings), main atdea81789aa. Main has since gained #1151 and the isotropic_remeshing changes in #1137 and #1139; both sides of the A/B were built from the same base.There were two build directories from the same checkout. The Eigen 5.0.1 one differed only by
-DCPM_eigen_SOURCE=<5.0.1 checkout>. After normalizing paths, all 1045 compile commands are identical apart from the Eigen include path. A fresh configure of this branch with no override fetches the same commit, and its headers are byte-identical to the benchmarked ones.Method. The machine was shared with other jobs (load average 3-14), so the two builds ran interleaved, test by test, alternating which went first, and the tables compare the best time per test. Load only ever adds time, so the minimum is the cleanest signal. 604 timed runs, 0 failures.
ctest: all 222 tests
wmtk_test_tetwildwmtk_test_topological_offsetwmtk_test_simwildwmtk_test_isotropic_remeshingwmtk_test_triwildwmtk_test_trackingwmtk_test_qslimwmtk_test_shortest_edge_collapseTwo early numbers looked like real effects and turned out to be noise:
wmtk_test_tetwild16% slower on 5.0.1. Nearly all of that wastetwild-surface-folds-none-made, a full 8-thread run whose operation order depends on the schedule. That case ranges 25.5–35.1 s on 3.4.0 and 25.2–39.1 s on 5.0.1, with best times 25.45 s vs 25.17 s.wmtk_test_isotropic_remeshinglooked faster on 5.0.1. Eight more pairs on a quieter machine (load about 3) were very tight: 13.66–13.70 s vs 13.70–13.75 s, 5.0.1 between 0.2% and 0.6% slower in every pair.On the quieter machine, two more rounds of the heavy tests gave a sum of best times of 197.68 s vs 196.26 s (0.993).
tetwild, single-threaded (
num_threads: 0), bunny viawmtk_apptetwild-surface-folds-none-made:check_surface_folds,history_output,max_iterations: 10.3 runs per build, alternating order:
Best time per phase, from the
TETWILD_PHASESlog line, as 5.0.1 / 3.4.0:Peak RSS was 2–4% lower on 5.0.1: W2 373–378 MB vs 385–393 MB, W1 401–417 MB vs 418–424 MB. This is the
/usr/bin/time -lmaximum resident set size, and the two versions build slightly different meshes (see below), so read it as "not worse" rather than as a win.Results are not bit-identical
Each build is deterministic: across its own serial runs, only the timing lines of the log differ. Between 3.4.0 and 5.0.1, the outputs differ from the first stage on.
#V/#T. W2: max 39.4487 and average 6.31673 on both.No test depends on exact output, and all of them pass.
Python tests
Same pass counts on both versions. These are not registered by ctest here, because Homebrew's Python 3.15 has no NumPy, so I ran them from a venv with NumPy 2.5.4:
test_memory_io.pytest_tracking.pytest_tracking_correspondences.pyNot measured
🤖 Generated with Claude Code
https://claude.ai/code/session_01ULwCxzbAACoHpC1mnmkvr8