Skip to content

Commit 4e7d0ac

Browse files
2.5.0: the performance pair, and the breaking changes under their own heading (#1231)
The release checklist owes three things this commit provides. A performance pair. v2.4.0 (de7189b, the 1845th) re-measured beside the release commit (6a97c07, the 1954th), both on 2026-09-09, on one machine, one entry per run. Allocation: SolveHard 1,446,794,112 -> 11,883,024 -99.18% SolveMediumHard 164,549,808 -> 1,449,266 -99.12% SimplifyHard 3,632,019,136 -> 331,756,152 -90.87% SimplifyEasy 128,100 -> 79,282 -38.11% SolveEasyMedium 97,195 -> 65,648 -32.46% SolveMedium 661,994 -> 455,925 -31.13% Three rows went up -- ParseHard +1.77%, ParseEasy +0.84%, Derivate +0.79% -- and neither is bisected, so both are recorded as unattributed rather than explained. The composition was checked rather than assumed, because this file records a case where it failed: the rule-set exchange measured free or better at every step and summed to +13%. SimplifyHard's four steps predict -90.2% composed; measured end-to-end it is -90.87%. They compose here, and that is a measurement. The determinism claim held again, with its two exceptions confirmed. v2.4.0 was measured for the 1930th on 2026-09-07 and again today: eleven of fourteen rows reproduce within 0.021% and four to the byte. The only two that moved further are CompileEasy and CompileHard, which performance-baseline.json already marks ungated because their allocation includes JIT-compiling a delegate. Today's spread sits inside the 3.6% it records there. The breaking changes move from "Unreleased" to "2.5.0 -- since 2.4.0", before the tag rather than after it. That is the one thing the 2.4.0 section records against itself: its entries sat under "Unreleased" while 2.4.0 was tagged and published, so a reader on that version could not tell from the file what they had. A section for the concurrency fix, which has no was/is table on purpose. A single-threaded caller computed these values before and computes them now; what changed is that a concurrent caller does too. The one thing a working program can observe is that the determinant and inverse now serialise where the polynomial elimination declines the matrix, and that the elementwise operators do not. key-commits.txt gains v2.5.0 so the next release measures against this one. Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 6a97c07 commit 4e7d0ac

3 files changed

Lines changed: 117 additions & 1 deletion

File tree

‎BREAKING-CHANGES.md‎

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,12 @@ is the primary spelling anyway and is what the library prints.
4141

4242
---
4343

44-
## Unreleased — since 2.4.0
44+
## 2.5.0 — since 2.4.0
45+
46+
Released 2026-09-09. This heading was renamed from “Unreleased” *before* the tag was cut, which is
47+
the one thing the 2.4.0 section below records against itself: those entries sat under “Unreleased”
48+
while 2.4.0 was tagged and published, so a reader on that version could not tell from this file what
49+
they had.
4550

4651
### At a glance
4752

@@ -124,6 +129,7 @@ is the primary spelling anyway and is what the library prints.
124129
| **Silent** | `"integral((x - floor(x))^2, x, 0, n)".ToEntity().Simplify()`, and every such integral with a symbolic bound or a condition against a symbol | `(n - floor(n)) ^ 3 / 3` — wrong for every whole `n` but 0; `piecewise(x provided x < a, 0)` from 0 to 2 was `piecewise(2 provided x < a, 0)` | left as written |
125130
| **Silent** | `"sum(piecewise(k provided k < a, 0), k, 0, 5)".ToEntity().Simplify()`, and every piecewise carrying a case whose predicate entails an earlier one | 32 cases, most of them unreachable | 6 — the unreachable ones are dropped, and the value at every `a` is unchanged |
126131
| | `"integral((x - floor(x)) / floor(x)!, x, 1, +oo)".ToEntity().Simplify()`, and `integral(floor(x), x, 0, 5)` | left as written | `(e - 1) / 2`, `10` |
132+
| **Silent** | `Determinant`, `Inverse` and `Adjugate` of a symbolic matrix, and the elementwise operators, called from more than one thread | a well-formed but **wrong** entity carrying another computation's values — 38 of 40 determinants disagreed with their single-threaded selves; the elementwise operators threw on a corrupted dictionary | the same answer a single thread gets. Unchanged on one thread; the determinant and inverse now serialise across threads where the polynomial elimination declines the matrix |
127133

128134
### A piecewise case that can never be reached is dropped
129135

@@ -1339,6 +1345,39 @@ depend on where the jumps fall. Offered only where every piece resolves. Questio
13391345
| `"integral(floor(x), x, 1/2, 2)"`, `integral(x - floor(x), x, 0, 5/2)`, `integral(ceil(x) - x, x, 1/2, 2)` | left as written | `1`, `9/8`, `5/8` |
13401346
| `"integral(floor(x^2), x, 0, 2)"` | left as written | the same — a floor of something other than the variable is not a step this reads. `integral(2^(-floor(x)), x, 0, +oo)` was left as written here for want of a geometric series, and is `2` as of the section above |
13411347

1348+
### Matrix operations called from more than one thread answer correctly
1349+
1350+
`MathS.Multithreading` documents concurrent use as supported, and three caches did not honour it.
1351+
Each was a table grown under a lock and read outside one, so a reader could see a new count against
1352+
an old array and come back with **a different element** — a well-formed number, silently wrong, with
1353+
nothing thrown. The factorial cache
1354+
([#1227](https://github.com/asc-community/AngouriMath/pull/1227)) and the prime cache
1355+
([#1229](https://github.com/asc-community/AngouriMath/pull/1229)) are AngouriMath's own. The third
1356+
is not: GenericTensor 1.0.4 hands **every caller the same scratch matrix** for a given size and then
1357+
writes into it, reported as [GenericTensor#40](https://github.com/asc-community/GenericTensor/issues/40)
1358+
and guarded here in [#1230](https://github.com/asc-community/AngouriMath/pull/1230).
1359+
1360+
**A single-threaded caller sees no change at all**, which is why there is no was/is table: the same
1361+
input produced these values before and produces them now. What changes is that a concurrent caller
1362+
now gets them too. Measured on `c2c8eb83`, forty 4x4 matrices with non-polynomial entries, each
1363+
computed once sequentially and then rebuilt and recomputed under `Parallel.For`:
1364+
1365+
| | disagreements, before |
1366+
|---|---|
1367+
| `Determinant` | 38 of 40 |
1368+
| `Inverse` | 18 of 40 |
1369+
| `Adjugate` | 11 of 40 |
1370+
| `m1 + m2`, `m1 - m2`, `PointwiseMultiplication` | threw, on a corrupted `Dictionary` |
1371+
1372+
**The one thing that is visible to a working program is throughput.** `Determinant`, `Inverse` and
1373+
`Adjugate` now serialise across threads where the polynomial elimination declines the matrix and the
1374+
Laplace fallback is taken — a matrix of polynomials never reaches it. The elementwise operators do
1375+
**not** serialise: the set of compiled loops this library can ask for is fixed at three, so they are
1376+
filled once and read without a lock thereafter.
1377+
1378+
The guard is a stand-in and says so in its own doc comment. It should be deleted when a GenericTensor
1379+
release carrying [GenericTensor#41](https://github.com/asc-community/GenericTensor/pull/41) exists.
1380+
13421381
## 2.4.0 — since 2.3.0
13431382

13441383
Released 2026-08-28. These entries sat under “Unreleased” while 2.4.0 was tagged and

‎Sources/AngouriMath/Docs/WhatsNew/version_performance_control.md‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,82 @@ honest reason -- a compiled delegate over `Complex` has nothing to put on the he
180180

181181
---
182182

183+
## The 1845th and the 1954th, measured together on one machine — the pair for 2.5.0
184+
185+
The pair the release checklist owes: `v2.4.0` (`de7189b1`, the 1845th) re-measured beside the
186+
release commit (`6a97c071`, the 1954th), both on 2026-09-09, on one machine, with nothing else on
187+
it, one entry per run. The period is the whole of 2.5.0 — 108 commits.
188+
189+
### Allocation
190+
191+
| benchmark | v2.4.0 | 1954th | change |
192+
|---|--:|--:|--:|
193+
| `CompileEasy` | 11,042 | 11,003 | −0.35% |
194+
| `CompileHard` | 20,776 | 20,473 | −1.46% |
195+
| `Derivate` | 52,824 | 53,240 | **+0.79%** |
196+
| `EvalTrig` | 1,341,377 | 1,341,457 | +0.01% |
197+
| `EvalTrigPrecise` | 12,742,205 | 12,742,285 | +0.00% |
198+
| `ParseEasy` | 18,112 | 18,264 | **+0.84%** |
199+
| `ParseHard` | 3,522,481 | 3,584,841 | **+1.77%** |
200+
| `SimplifyEasy` | 128,100 | 79,282 | **−38.11%** |
201+
| `SimplifyHard` | 3,632,019,136 | 331,756,152 | **−90.87%** |
202+
| `SolveEasy` | 8,853,735 | 8,865,872 | +0.14% |
203+
| `SolveEasyMedium` | 97,195 | 65,648 | **−32.46%** |
204+
| `SolveHard` | 1,446,794,112 | 11,883,024 | **−99.18%** |
205+
| `SolveMedium` | 661,994 | 455,925 | **−31.13%** |
206+
| `SolveMediumHard` | 164,549,808 | 1,449,266 | **−99.12%** |
207+
208+
Bytes allocated. `EvalEasy`, `RunEasy`, `RunMedium` and `RunHard` allocate nothing in both columns
209+
and are left out.
210+
211+
### The individually-measured steps compose, and that was checked rather than assumed
212+
213+
This file records a case where they did not: the rule-set exchange was measured step by step, each
214+
step came back free or better, and the sum was **+13%**. So the five allocation changes in this
215+
release are worth checking against where the release started rather than trusting their own
216+
figures.
217+
218+
`SimplifyHard` was measured at −44% ([#1205](https://github.com/asc-community/AngouriMath/pull/1205)),
219+
−64% ([#1207](https://github.com/asc-community/AngouriMath/pull/1207)),
220+
−35% ([#1210](https://github.com/asc-community/AngouriMath/pull/1210)) and
221+
−25% ([#1211](https://github.com/asc-community/AngouriMath/pull/1211)), each against the commit in
222+
front of it. Composed, those predict 9.8% of the original surviving, or −90.2%. **Measured
223+
end-to-end against `v2.4.0`: −90.87%**, 0.7 percentage points from the prediction. `SolveHard` was
224+
−98.6% for [#1209](https://github.com/asc-community/AngouriMath/pull/1209) alone and is −99.18%
225+
across the release. They compose here. That is a measurement, not an expectation, and the +13% case
226+
is why it was taken.
227+
228+
### Three rows went up, and none is attributed
229+
230+
`ParseHard` +1.77%, `ParseEasy` +0.84% and `Derivate` +0.79%. The parser gained three alternatives
231+
this release — the `divides` keyword, `#` for cardinality and the lambda arrow — which is consistent
232+
with the two parse rows, and `Derivate` has no such candidate. **Neither is bisected**, so both are
233+
recorded as unattributed rather than explained. A move nobody has attributed is a finding, not a
234+
footnote.
235+
236+
### The determinism claim, held again — and its two exceptions confirmed
237+
238+
`v2.4.0` was measured on 2026-09-07 for the 1930th and again today, two days and one run apart.
239+
Eleven of the fourteen rows reproduce to within **0.021%**, four of them to the byte —
240+
`EvalTrig`, `EvalTrigPrecise`, `ParseEasy`, `ParseHard`, `SolveMediumHard`.
241+
242+
The only two rows that moved further are `CompileEasy` (+0.354%) and `CompileHard` (+1.674%), which
243+
are exactly the two `performance-baseline.json` marks **ungated**, for the reason it records there:
244+
their measured allocation includes the runtime building and JIT-compiling a delegate, which is not
245+
reproducible, at a spread of 3.6% over three runs of one unchanged build. Today's spread sits inside
246+
that. So this is an independent confirmation of a documented caveat rather than a new one — and it
247+
is worth stating that the blanket sentence "the same commit measured twice gives the same bytes"
248+
holds for every row this file gates and for neither of the two it does not.
249+
250+
### Timings
251+
252+
`SimplifyHard` −89.64%, `SolveHard` −87.29%, `SolveMediumHard` −83.83%, `SolveEasyMedium` −42.80%,
253+
`SimplifyEasy` −57.15%. These are reproduced here, where the 1930th's deliberately were not,
254+
because the 1844th section's objection is that this machine cannot resolve a move of a few per
255+
cent — its measured run-to-run spread on mean time is up to 51.8%. A move of 84 to 90 per cent is
256+
outside that band by a wide margin and agrees in sign and rough size with the allocation column
257+
beside it. The small rows from the same run are still not worth reading, and are not quoted.
258+
183259
## The 1930th, and every release beside it on one machine
184260

185261
The second column measured by `Sources/Utils/benchmark_key_commits.sh` reaching all five entries in

‎Sources/Tests/DotnetBenchmark/key-commits.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,5 @@ v2.1.0
3939
v2.2.0
4040
v2.3.0
4141
v2.4.0
42+
v2.5.0
4243
master

0 commit comments

Comments
 (0)