From 6bc22b2411209c1440505883b537a5a83e4eb58d Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sun, 1 Nov 2026 13:14:41 +0000 Subject: [PATCH 1/2] A union of solves solves each equation once Unite and Intersect asked Any() of their sequence and then folded it with Aggregate, which enumerates it a second time. The solvers hand Unite a lazy Select of solves, one per root of a substitution or per factor, so the first of them was solved twice. A Select over a list or a range knows its count without computing anything, which is why it went unseen; over a plain iterator Any() computes the first element. Each is now enumerated once into an array and folded. Measured per Solve after a warm-up, the answers unchanged: x^5 = |x^3 + 1| + 1 allocates 140 MB where it allocated 733 MB, and takes 362 ms where it took 721; e^(2x) - 3e^x + 2 = 0 403 KB where 668 KB, sqrt(x + 1) = x - 1 829 KB where 1205 KB. Co-Authored-By: Claude Opus 5.5 (1M context) --- Sources/.editorconfig | 3 + .../Convenience/AngouriMathExtensions.cs | 8 ++- .../Convenience/FoldEnumerationTest.cs | 69 +++++++++++++++++++ 3 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 Sources/Tests/UnitTests/Convenience/FoldEnumerationTest.cs diff --git a/Sources/.editorconfig b/Sources/.editorconfig index 062284068..8edc021c0 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -96,6 +96,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [Tests/UnitTests/Convenience/EmptySequenceFoldTest.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n +[Tests/UnitTests/Convenience/FoldEnumerationTest.cs] +file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n + [Tests/UnitTests/Algebra/ExponentialTwoBasesTest.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n diff --git a/Sources/AngouriMath/Convenience/AngouriMathExtensions.cs b/Sources/AngouriMath/Convenience/AngouriMathExtensions.cs index bff929ebf..0083bd803 100644 --- a/Sources/AngouriMath/Convenience/AngouriMathExtensions.cs +++ b/Sources/AngouriMath/Convenience/AngouriMathExtensions.cs @@ -219,7 +219,10 @@ public static FiniteSet ToSet(this IEnumerable elements) /// /// public static Set Unite(this IEnumerable sets) - => sets.Any() ? sets.Aggregate((a, b) => MathS.Union(a, b)) : Empty; + // Enumerated once. Asking Any and then Aggregate of a lazy sequence computed its first + // element twice, and the solvers pass one: each root's equation in a union of solves + // was solved once to see that there was a first and again to unite it. + => sets.ToArray() is { Length: > 0 } array ? array.Aggregate((a, b) => MathS.Union(a, b)) : Empty; /// /// Computes the intersection of your 's and makes it one . @@ -243,7 +246,8 @@ public static Set Unite(this IEnumerable sets) /// /// public static Set Intersect(this IEnumerable sets) - => sets.Any() ? sets.Aggregate((a, b) => MathS.Intersection(a, b)) : Empty; + // Enumerated once, as Unite is. + => sets.ToArray() is { Length: > 0 } array ? array.Aggregate((a, b) => MathS.Intersection(a, b)) : Empty; /// /// Parses the expression into . diff --git a/Sources/Tests/UnitTests/Convenience/FoldEnumerationTest.cs b/Sources/Tests/UnitTests/Convenience/FoldEnumerationTest.cs new file mode 100644 index 000000000..d41737800 --- /dev/null +++ b/Sources/Tests/UnitTests/Convenience/FoldEnumerationTest.cs @@ -0,0 +1,69 @@ +// +// Copyright (c) 2019-2026 Angouri. +// AngouriMath is licensed under MIT. +// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. +// Website: https://am.angouri.org. +// + +using System.Collections.Generic; +using System.Linq; +using AngouriMath.Extensions; +using Xunit; +using static AngouriMath.Entity; +using static AngouriMath.Entity.Set; + +namespace AngouriMath.Tests.Convenience +{ + /// + /// A union or an intersection of a sequence computes each element once. Asking whether + /// there was a first and then folding enumerated a lazy sequence twice, and the solvers hand + /// these a sequence of solves, so the first of them was solved twice. No issue was filed. + /// + [Trait("Area", "Convenience")] + public sealed class FoldEnumerationTest + { + /// + /// Computed only as it is enumerated, as a solver's sequence of solves is. A selection + /// over a range or a list would not do: its count is known without computing anything, + /// so asking whether it has a first element computes none. + /// + private sealed class Computed + { + internal int Count { get; private set; } + + internal IEnumerable Sets(params Set[] sets) + { + foreach (var set in sets) + { + Count++; + yield return set; + } + } + } + + [Fact] + public void AUnionComputesEachSetOnce() + { + var computed = new Computed(); + var union = computed.Sets(new FiniteSet((Entity)1), new FiniteSet((Entity)2), new FiniteSet((Entity)3)).Unite(); + Assert.Equal("{ 1, 2, 3 }".ToEntity(), union.InnerSimplified); + Assert.Equal(3, computed.Count); + } + + [Fact] + public void AnIntersectionComputesEachSetOnce() + { + var computed = new Computed(); + var intersection = computed.Sets(new FiniteSet((Entity)1, 2), new FiniteSet((Entity)1, 3), new FiniteSet((Entity)1)).Intersect(); + Assert.Equal("{ 1 }".ToEntity(), intersection.InnerSimplified); + Assert.Equal(3, computed.Count); + } + + [Fact] + public void TheEmptyFoldsAreStillEmpty() + { + Assert.Equal(Empty, Enumerable.Empty().Unite()); + Assert.Equal(Empty, Enumerable.Empty().Intersect()); + } + } +} From dcbf619e0449dcd2249c8f206dfb05d643770100 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sun, 1 Nov 2026 13:44:45 +0000 Subject: [PATCH 2/2] Record SolveHard's and SolveMediumHard's lower allocation in the kernel performance baseline The gate reported IMPROVED: SolveHard allocates 9,526,556 B/op, 4.7% below the 9,996,973 recorded at 5edd6a1f on 2026-10-21, and SolveMediumHard 1,366,964, 3.1% below 1,410,754. The file is the one this pull request's own Benchmark job wrote, measured-CommonFunctionsInterVersion.json from its artifact, copied over the baseline as version_performance_control.md describes. Every other gated row is within its band: SimplifyHard +2.4%, SolveEasy -2.0%. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../DotnetBenchmark/performance-baseline.json | 84 +++++++++---------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/Sources/Tests/DotnetBenchmark/performance-baseline.json b/Sources/Tests/DotnetBenchmark/performance-baseline.json index 7f8d240b8..661a0effb 100644 --- a/Sources/Tests/DotnetBenchmark/performance-baseline.json +++ b/Sources/Tests/DotnetBenchmark/performance-baseline.json @@ -1,94 +1,94 @@ { "comment": "Allocated bytes and mean nanoseconds per operation for the popular use cases of https://github.com/asc-community/AngouriMath/issues/746. Checked by PerformanceGate.cs; see Sources/AngouriMath/Docs/WhatsNew/version_performance_control.md for when updating it is legitimate.", "benchmark": "CommonFunctionsInterVersion", - "commit": "5edd6a1f8a0e9032b314b2ceeae8e80d648e6358", - "measuredOn": "2026-10-21", - "runtime": ".NET 10.0.13", - "machine": "Ubuntu 24.04.5 LTS, X64, 4 logical cores", + "commit": "190a6c8427a0e2d5e8e1c9e58368453e82891067", + "measuredOn": "2026-11-01", + "runtime": ".NET 10.0.14", + "machine": "Ubuntu 26.04.1 LTS, X64, 4 logical cores", "cases": { "CompileEasy": { - "allocatedBytes": 11003, - "meanNanoseconds": 330806.3571777344 + "allocatedBytes": 13826, + "meanNanoseconds": 409149.02092633926 }, "CompileHard": { - "allocatedBytes": 20398, - "meanNanoseconds": 577621.3629557291 + "allocatedBytes": 27888, + "meanNanoseconds": 499996.85323660716 }, "Derivate": { - "allocatedBytes": 56245, - "meanNanoseconds": 27684.57133131761 + "allocatedBytes": 56139, + "meanNanoseconds": 28628.765409197127 }, "EvalEasy": { "allocatedBytes": 0, - "meanNanoseconds": 4.0538319910948095 + "meanNanoseconds": 3.744399822716202 }, "EvalPolynomialFresh": { - "allocatedBytes": 12536, - "meanNanoseconds": 8084.578495279948 + "allocatedBytes": 12392, + "meanNanoseconds": 8228.309937540691 }, "EvalPolynomialFresh15Digits": { - "allocatedBytes": 12064, - "meanNanoseconds": 8616.347739083427 + "allocatedBytes": 11920, + "meanNanoseconds": 9051.311197553363 }, "EvalTranscendentalFresh": { - "allocatedBytes": 34760, - "meanNanoseconds": 30158.402158610024 + "allocatedBytes": 34640, + "meanNanoseconds": 30436.87562561035 }, "EvalTrig": { - "allocatedBytes": 69944, - "meanNanoseconds": 58949.92800467355 + "allocatedBytes": 69888, + "meanNanoseconds": 59969.564767690805 }, "EvalTrigPrecise": { - "allocatedBytes": 336553, - "meanNanoseconds": 721994.4565104167 + "allocatedBytes": 336498, + "meanNanoseconds": 717843.7869140625 }, "ParseEasy": { - "allocatedBytes": 18412, - "meanNanoseconds": 11023.764210292271 + "allocatedBytes": 18386, + "meanNanoseconds": 11232.588527134487 }, "ParseHard": { - "allocatedBytes": 60023, - "meanNanoseconds": 55330.677834647046 + "allocatedBytes": 59751, + "meanNanoseconds": 56299.28791910807 }, "RunEasy": { "allocatedBytes": 0, - "meanNanoseconds": 49.59855388317789 + "meanNanoseconds": 48.487677480493275 }, "RunHard": { "allocatedBytes": 0, - "meanNanoseconds": 571.6311563650767 + "meanNanoseconds": 505.936243057251 }, "RunMedium": { "allocatedBytes": 0, - "meanNanoseconds": 322.09172677993774 + "meanNanoseconds": 312.37248986562093 }, "SimplifyEasy": { - "allocatedBytes": 79681, - "meanNanoseconds": 87608.15931803385 + "allocatedBytes": 79689, + "meanNanoseconds": 87464.0671142578 }, "SimplifyHard": { - "allocatedBytes": 157895384, - "meanNanoseconds": 166394909.7647059 + "allocatedBytes": 161716240, + "meanNanoseconds": 175207981.33333334 }, "SolveEasy": { - "allocatedBytes": 453424, - "meanNanoseconds": 436456.99151141825 + "allocatedBytes": 444247, + "meanNanoseconds": 435638.4725435697 }, "SolveEasyMedium": { - "allocatedBytes": 66661, - "meanNanoseconds": 37503.79045003255 + "allocatedBytes": 66538, + "meanNanoseconds": 38170.384669712614 }, "SolveHard": { - "allocatedBytes": 9996973, - "meanNanoseconds": 110732013.08888888 + "allocatedBytes": 9526556, + "meanNanoseconds": 121279380.16666667 }, "SolveMedium": { - "allocatedBytes": 458456, - "meanNanoseconds": 534248.4147385817 + "allocatedBytes": 459544, + "meanNanoseconds": 528266.8291666667 }, "SolveMediumHard": { - "allocatedBytes": 1410754, - "meanNanoseconds": 16481988.640625 + "allocatedBytes": 1366964, + "meanNanoseconds": 16442697.847916666 } }, "ungated": {