From fabcf563869294ac331e52239f793de56807ee4b Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sun, 9 Aug 2026 17:11:57 +0000 Subject: [PATCH] Benchmark trigonometry against precision, and stop discarding the allocations Both halves of what #167 asked for, after Happypig375 said to add the benchmarks and to profile memory as well. **Trigonometry against precision.** CommonFunctionsInterVersion had no trigonometric case at any precision. EvalTrig and EvalTrigPrecise evaluate sin(1) + cos(1) + tan(1) at the default hundred digits and at five hundred. Measured while writing them: 3.8 ms, 10.2 ms at two hundred, 25.4 ms at five hundred and 146 ms at a thousand, so the issue's premise holds and now has a row that will show it moving. Both build their nodes on every call rather than evaluating a shared instance. That is not a stylistic choice: an Entity caches its own Evaled, so the obvious form of this benchmark measures a dictionary lookup. A hundred thousand evaluations of one instance take a millisecond in total; a thousand evaluations of fresh nodes take a second. Which is also what EvalEasy has been measuring since that cache landed, and why it reads two nanoseconds. Left as it is so the column history stays comparable, with the trap written down in both the benchmark and the document. **Allocations.** The class has carried [MemoryDiagnoser] all along, so the figure was collected on every run and then dropped, because Program.cs asked for Mean, Error and StdDev and nothing else. Now it asks for Allocated too. The column header is exactly "Allocated" -- verified by running BenchmarkDotNet and printing the headers, since TableToString skips a header it cannot find without complaining. Co-Authored-By: Claude Opus 5 (1M context) --- .../WhatsNew/version_performance_control.md | 26 +++++++++++++++++++ .../CommonFunctionsInterVersion.cs | 21 +++++++++++++++ Sources/Tests/DotnetBenchmark/Program.cs | 4 ++- 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/Sources/AngouriMath/Docs/WhatsNew/version_performance_control.md b/Sources/AngouriMath/Docs/WhatsNew/version_performance_control.md index b6f944c60..90acb12af 100644 --- a/Sources/AngouriMath/Docs/WhatsNew/version_performance_control.md +++ b/Sources/AngouriMath/Docs/WhatsNew/version_performance_control.md @@ -21,6 +21,32 @@ on a path that does not exist and had not run since 2026-01-02 -- ninety-eight m requests went unmeasured. Fixed in [#775](https://github.com/asc-community/AngouriMath/issues/775). +**Allocation is reported alongside the timings** from the column after the 1620th. The +benchmark class has carried `[MemoryDiagnoser]` all along, so the figure was measured on every +run and then discarded, because the reporting code asked for `Mean`, `Error` and `StdDev` and +nothing else. Asked for by [#167](https://github.com/asc-community/AngouriMath/issues/167). + +**`EvalTrig` and `EvalTrigPrecise`** are new in the same column and are the other half of +[#167](https://github.com/asc-community/AngouriMath/issues/167): what arbitrary precision costs +in trigonometry. Both evaluate `sin(1) + cos(1) + tan(1)`, the first at the default hundred +digits and the second at five hundred. Measured while writing them, on one machine, so treat +these as the shape rather than as the numbers: + +| digits | per evaluation | +|---|---| +| 100 (default) | 3.8 ms | +| 200 | 10.2 ms | +| 500 | 25.4 ms | +| 1000 | 146 ms | + +**Read `EvalEasy` with care.** It evaluates one `Entity` instance, and an `Entity` caches its +own `Evaled`, so from the moment that cache landed the row stopped measuring evaluation and +started measuring a lookup -- which is what the fall to single-digit nanoseconds is, not a +speed-up of the arithmetic. Measured directly: a hundred thousand evaluations of one instance +take a millisecond in total, while a thousand evaluations of freshly built nodes take a second. +The row is kept as it is so the column history stays comparable, and the two trigonometric +benchmarks build their expression afresh on every call for exactly this reason. + | Method | [331st](https://github.com/asc-community/AngouriMath/commit/10e6e5a90e7270336b68dc5fd6aa36f3e0e65d2b) | [380th](https://github.com/asc-community/AngouriMath/commit/73ae36488ddb863c1d6f35db5ed2f5dcf1484a26) | [391st](https://github.com/asc-community/AngouriMath/commit/c7e08e6936bfdc2373377bec81ffd160e406244f) | [410th](https://github.com/asc-community/AngouriMath/commit/20814936bc740a9f410af4a4368e9895eab7aaf7) | [483rd](https://github.com/asc-community/AngouriMath/commit/355963dcdf0ff9da568e9f1144ad2b7b68c19584) | [520th](https://github.com/asc-community/AngouriMath/commit/70aa71acb73307c9f7df0aac006faae31b06058c) | [690th](https://github.com/asc-community/AngouriMath/commit/5cc894939cb3657f0aa7ef5a25fd55011058929f) | [826th](https://github.com/asc-community/AngouriMath/commit/87e33ec3590a95dd4ec59ff5c1f77064a64196d1) | [914th](https://github.com/asc-community/AngouriMath/commit/6134338df083a908369b6bcfb69e70a4269ec51b) | [920th](https://github.com/asc-community/AngouriMath/commit/501a0a3a9b2e07cddf92c4446b73ad6e2748253a) | [1034th](https://github.com/asc-community/AngouriMath/commit/a3f48b47795b2dc2b3435152989a6e15639a65b4) | [1066th](https://github.com/asc-community/AngouriMath/commit/a33746651f56a380b6c17913aa844f162f258d8c) | [1090th](https://github.com/asc-community/AngouriMath/commit/9530c5b04484e98023941f7693bbeb1a3282cee6) | [1446th](https://github.com/asc-community/AngouriMath/commit/2abb2b537c03977281f3fc2cab1da2c78c36a5f5) | [1620th](https://github.com/asc-community/AngouriMath/commit/e05d71797f53a9ac7dbdad6075cd7302a91036e7) | |---------------- |--------------:|---------------:|---------------:|--------------:|----------------:|--------------:|--------------:|--------------:|--------------:|--------------:|--------------:|--------------:|--------------:|-----------------:|--------------:| | ParseEasy | 28,599 | 73,669 | 134,120 | 44,328 | 54,675 | 21,722 | 32,212 | 32,138 | 34,702 | 32,199 | 33,008 | 27,483 | 33,043 | 34,664 | 11,760 ns | diff --git a/Sources/Tests/DotnetBenchmark/CommonFunctionsInterVersion.cs b/Sources/Tests/DotnetBenchmark/CommonFunctionsInterVersion.cs index 05ad33c33..346e1b318 100644 --- a/Sources/Tests/DotnetBenchmark/CommonFunctionsInterVersion.cs +++ b/Sources/Tests/DotnetBenchmark/CommonFunctionsInterVersion.cs @@ -11,6 +11,7 @@ using AngouriMath.Extensions; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Exporters.Csv; +using PeterO.Numbers; namespace DotnetBenchmark { @@ -52,9 +53,29 @@ public class CommonFunctionsInterVersion private static readonly Entity toSolveHard = "(sin(cos(x) + sin(x) + c) + sqr(sin(cos(x) + sin(x) + c)) + a)4 + (sin(cos(x) + sin(x) + c) + sqr(sin(cos(x) + sin(x) + c)) + a) + b"; // Testing evaluation + // NB: evalEasy is one instance and an Entity caches its own Evaled, so from the + // moment that cache was added this stopped measuring evaluation and started + // measuring a dictionary lookup -- which is why the row reads single-digit + // nanoseconds. Kept as it is so the column history stays comparable; the two + // benchmarks below build their expression afresh each call for that reason. [Benchmark] public void EvalEasy() => evalEasy.EvalNumerical(); private static readonly Entity evalEasy = "1 + 2 + log(2, 3) + sqrt(4) - 4 ^ 7 + e * pi"; + // Testing trigonometry against precision, which is what #167 asked for. Building + // the nodes is included in the measurement and costs microseconds against + // milliseconds of arithmetic, so it does not move the number; evaluating a cached + // instance instead would move it to zero. + [Benchmark] public void EvalTrig() + => (MathS.Sin(1) + MathS.Cos(1) + MathS.Tan(1)).EvalNumerical(); + + [Benchmark] public void EvalTrigPrecise() + { + using var _ = MathS.Settings.DecimalPrecisionContext.Set(fiveHundredDigits); + (MathS.Sin(1) + MathS.Cos(1) + MathS.Tan(1)).EvalNumerical(); + } + private static readonly EContext fiveHundredDigits = + new(500, ERounding.HalfUp, -500, 5000, false); + // Testing compilation [Benchmark] public void CompileEasy() => toCompileEasy.Compile("x"); private static readonly Entity toCompileEasy = "x + x / 2 + sin(x)"; diff --git a/Sources/Tests/DotnetBenchmark/Program.cs b/Sources/Tests/DotnetBenchmark/Program.cs index 89aef4a17..73d10dbb0 100644 --- a/Sources/Tests/DotnetBenchmark/Program.cs +++ b/Sources/Tests/DotnetBenchmark/Program.cs @@ -76,7 +76,9 @@ public static void Main(string[] args) arg switch { "RAMUsageTest" => GetReportByBenchmark(typeof(RAMUsageTest), "Gen 0", "Gen 1", "Gen 2", "Allocated"), - "CommonFunctionsInterVersion" => GetReportByBenchmark(typeof(CommonFunctionsInterVersion), "Mean", "Error", "StdDev"), + // Allocated: the class has carried [MemoryDiagnoser] all along, so the + // figure was being collected on every run and then dropped here. + "CommonFunctionsInterVersion" => GetReportByBenchmark(typeof(CommonFunctionsInterVersion), "Mean", "Error", "StdDev", "Allocated"), // Allocated as well as Mean: the regressions this one exists to catch // show up in allocation and are invisible in the timings. "TransformationLayer" => GetReportByBenchmark(typeof(TransformationLayer), "Mean", "Error", "StdDev", "Allocated"),