tinyasm: avoid copying the block dofmap in BlockJacobi::solve() - #5348
tinyasm: avoid copying the block dofmap in BlockJacobi::solve()#5348manishpaulish wants to merge 3 commits into
Conversation
dofsPerBlock is a vector<vector<PetscInt>>, so `auto dofmap = dofsPerBlock[p]` deduces by value and heap-allocates + copies the block's dofmap on every block, on every application of the preconditioner. solve() runs once per Krylov iteration and block counts are typically in the thousands, so this is on the order of a million small allocations per linear solve. Bind by const reference instead. No behavioural change: dofmap is only read. A standalone microbenchmark mirroring the loop structure (20k blocks, dof in [4,20], 50 applications, -O2) measures the copy at 11-13% of solve() runtime.
|
It'd be good to motivate this PR with an actual benchmark and more context on how and why you came across this bug. |
|
Fair on both counts, and pushing on the benchmark was the right call. It changed the answer. How I came across it. I was looking for the C++ in Firedrake, since that is my background, and The number in the description was misleading and I should correct it. I generated block sizes uniformly in [4, 20], which I picked out of the air. Real vertex-star patches are not distributed like that. Rebuilding the benchmark on patches from an actual mesh, a 12^3 cube Kuhn-subdivided into 10368 tets giving 2197 vertex-star patches:
Same loop as So my original 11 to 13 percent was roughly right for small blocks and overstates the case that matters most by several times. At P2 the Two honest caveats. This is still not a Firedrake solve. The matrices are random rather than assembled, and more importantly this is a percentage of What the change does buy, independent of the timing, is 2197 heap allocations per preconditioner application, one per block, going to zero. That is the part I would still defend: the loop allocates once per block per application for a value it only reads. Whether 2 to 3 percent of the apply justifies the churn is your call and I will not argue if the answer is no. Happy to add the benchmark under |
connorjward
left a comment
There was a problem hiding this comment.
Assuming that tests pass I see no issue with this and can believe that this affects performance.
|
Tests should hopefully pass if you merge in |
|
CI finished. Two jobs are red and I do not think either comes from this PR.
That test builds two DG0 spaces, interpolates, calls Happy to push an empty commit to re-run, or leave it if you would rather look first. If the |
|
Yeah CI appears to be in a bad place currently distinct from this PR. I will aim to merge this once upstream things are resolved. Thanks! |
|
I think this PR fixes that CI error #5337 |
|
@manishpaulish sorry to keep asking but please merge in |
Merge upstream main into perf/tinyasm-dofmap-copy
|
Done, merged |
dofsPerBlockis avector<vector<PetscInt>>, soauto dofmap = dofsPerBlock[p];deduces by value and heap-allocates plus copies the block's dofmap on every block, on every application of the preconditioner.
solve()runs once per Krylov iteration and block counts are typically in the thousands, so this is on the order of a million small allocations per linear solve.Binding by const reference instead. No behavioural change,
dofmapis only read.A benchmark driving the same loop, including the
dof < 6branch, withvertex-star patch sizes taken from a real tetrahedral mesh rather than
invented ones, puts the copy at roughly 14% of
solve()for P1 (medianblock 15 dofs) and 2 to 3% for P2 (median block 65 dofs). Details and
caveats in the comments below. Note this is a fraction of
solve(), notof an end-to-end solve.
Independent of the timing, the change removes one heap allocation per
block per preconditioner application, for a value the loop only reads.