Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 50 additions & 13 deletions Sources/AngouriMath/Core/CostModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,32 +147,69 @@ private static double Feature(Entity expr, Func<Entity, bool> predicate)
private const double HeavyWeight = 8.0;
private const double ExtraHeavyWeight = 12.0;

// Recurses through Entity.DefaultCostCached rather than through itself, so a subtree is
// costed once per instance however many candidates share it; and walks children by
// index rather than through Nodes or a LINQ Sum, so that costing a node allocates
// nothing. The arithmetic is the same expression in the same order, so every rate is
// the same double it was -- the selection between candidates cannot move.
internal static double DefaultCost(Entity expr) => expr switch
{
// Weigh provided predicates much less but nested provideds heavy
Providedf(var inner, var predicate) =>
DefaultCost(inner) + 0.1 * DefaultCost(predicate) + ExtraHeavyWeight * (inner.Nodes.Count(n => n is Providedf) + predicate.Nodes.Count(n => n is Providedf)),
inner.DefaultCostCached + 0.1 * predicate.DefaultCostCached
+ ExtraHeavyWeight * (ProvidedsIn(inner) + ProvidedsIn(predicate)),
Piecewise { Cases: var cases } =>
cases.Sum(@case =>
DefaultCost(@case.Expression) + 0.1 * DefaultCost(@case.Predicate) + ExtraHeavyWeight * (@case.Expression.Nodes.Count(n => n is Providedf) + @case.Predicate.Nodes.Count(n => n is Providedf))),
@case.Expression.DefaultCostCached + 0.1 * @case.Predicate.DefaultCostCached
+ ExtraHeavyWeight * (ProvidedsIn(@case.Expression) + ProvidedsIn(@case.Predicate))),
Variable => Weight, // Number of variables
// A root in a denominator, which the rationalising rule clears out.
// Without a weight here the two forms tie -- 1 / (sqrt(3) + 5) and
// (sqrt(3) - 5) / (-22) are the same rate -- and a tie is settled by
// whichever candidate was generated first, which is not a preference
// so much as an accident. This states the preference instead.
// https://github.com/asc-community/AngouriMath/issues/205
Divf(_, var divisor) when divisor.Nodes.Any(node => node is Powf(_, Rational and not Integer))
=> MinorWeight + Weight + expr.DirectChildren.Sum(DefaultCost),
Divf => MinorWeight + expr.DirectChildren.Sum(DefaultCost), // Number of divides
Rational(Integer(1 or -1), _) and not Integer => Weight + expr.DirectChildren.Sum(DefaultCost), // Number of rationals with unit numerator
Powf(_, Real { IsNegative: true }) => HeavyWeight + expr.DirectChildren.Sum(DefaultCost), // Number of negative powers
Logf => TinyWeight + expr.DirectChildren.Sum(DefaultCost), // Number of logarithms
Phif => ExtraHeavyWeight + expr.DirectChildren.Sum(DefaultCost), // Number of phi functions
Real { IsNegative: true } => MajorWeight + expr.DirectChildren.Sum(DefaultCost), // Number of negative reals
ComparisonSign when expr.DirectChildren[0] == 0 => Weight + expr.DirectChildren.Sum(DefaultCost), // 0 < x is bad. x > 0 is good.
Notf(Equalsf eq) => -Weight + DefaultCost(eq), // (not x = 0) is equally complex as (x = 0)
_ => expr.DirectChildren.Sum(DefaultCost)
Divf(_, var divisor) when HasFractionalPower(divisor)
=> MinorWeight + Weight + ChildrenCost(expr),
Divf => MinorWeight + ChildrenCost(expr), // Number of divides
Rational(Integer(1 or -1), _) and not Integer => Weight + ChildrenCost(expr), // Number of rationals with unit numerator
Powf(_, Real { IsNegative: true }) => HeavyWeight + ChildrenCost(expr), // Number of negative powers
Logf => TinyWeight + ChildrenCost(expr), // Number of logarithms
Phif => ExtraHeavyWeight + ChildrenCost(expr), // Number of phi functions
Real { IsNegative: true } => MajorWeight + ChildrenCost(expr), // Number of negative reals
ComparisonSign when expr.DirectChildren[0] == 0 => Weight + ChildrenCost(expr), // 0 < x is bad. x > 0 is good.
Notf(Equalsf eq) => -Weight + eq.DefaultCostCached, // (not x = 0) is equally complex as (x = 0)
_ => ChildrenCost(expr)
} + Weight; // Number of nodes

/// <summary>The children's costs, summed in order -- what <c>DirectChildren.Sum(DefaultCost)</c> summed.</summary>
private static double ChildrenCost(Entity expr)
{
var children = expr.DirectChildren;
var sum = 0d;
for (var i = 0; i < children.Count; i++)
sum += children[i].DefaultCostCached;
return sum;
}

/// <summary>How many nodes of the tree, the root included, are a <c>Providedf</c> -- what <c>Nodes.Count(n => n is Providedf)</c> counted.</summary>
private static int ProvidedsIn(Entity expr)
{
var count = expr is Providedf ? 1 : 0;
var children = expr.DirectChildren;
for (var i = 0; i < children.Count; i++)
count += ProvidedsIn(children[i]);
return count;
}

/// <summary>Whether any node of the tree, the root included, is a power to a non-whole rational -- what the <c>Nodes.Any</c> asked.</summary>
private static bool HasFractionalPower(Entity expr)
{
if (expr is Powf(_, Rational and not Integer)) return true;
var children = expr.DirectChildren;
for (var i = 0; i < children.Count; i++)
if (HasFractionalPower(children[i])) return true;
return false;
}
}
}
11 changes: 11 additions & 0 deletions Sources/AngouriMath/Core/Entity/Entity.Definition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,17 @@ public double SimplifiedRate
}
private LazyPropertyA<double> simplifiedRate;

/// <summary>
/// <see cref="CostModel.DefaultCost"/> of this node, computed once per instance -- the
/// same slot <see cref="SimplifiedRate"/> fills on its unset path, so the two never
/// disagree. <c>DefaultCost</c> recurses through this rather than through itself: a
/// candidate the simplifier ranks shares most of its subtrees with the candidates
/// before it, and re-walking a shared subtree for every candidate was 3.66 GB of
/// allocation per <c>SimplifyHard</c>, some 315 whole-tree walks of 11.6 MB each.
/// </summary>
internal double DefaultCostCached
=> simplifiedRate.GetValue(MathS.Settings.ComplexityCriteria.Default, this);

/// <summary>Checks whether the given expression contains variable</summary>
/// <example>
/// <code>
Expand Down
31 changes: 31 additions & 0 deletions Sources/AngouriMath/Docs/WhatsNew/version_performance_control.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,37 @@ large rows reproduce to the byte or nearly; the compile rows sit inside their fl
Timings from the same run are in the generated table and are deliberately not reproduced here,
for the reason the 1844th section gives.

## The 1931st: a candidate registered once

The same afternoon, on the same machine. `Simplificator.Alternate` registered every candidate it
generated twice — once as it was and once as `expr.Rewrite(InvertNegativePowers)`, each
registration a `CanonicalOrder` rewrite, an inner simplification and two ratings — and on an
input with nothing to invert the second tree is the first tree, so the second registration
re-sorted an identical tree to offer the history set an entity it already held. Found by
attributing `SimplifyHard`'s bytes to the steps of one level: the registrations were 862 MB of
976. The second registration now runs only where the inversion produced a different tree, and
`CostModel.DefaultCost` costs a subtree once per instance instead of re-walking it for every
candidate that shares it. No answer moves — the same candidates reach a set that already
deduplicated the repeat, and every rate is the same double.

| benchmark | 1930th | 1931st | |
|---|--:|--:|--:|
| `SimplifyHard` | 3,662,375,240 | 2,027,771,744 | **−44.6%** |
| `SolveHard` | 1,452,700,104 | 1,057,996,016 | **−27.2%** |
| `SolveMediumHard` | 165,611,184 | 126,162,152 | **−23.8%** |
| `SimplifyEasy` | 128,460 | 115,763 | −9.9% |

Bytes allocated; every other row unchanged or inside its floor. The solvers move because `Solve`
simplifies inside. Timings, one run against one run and so carrying this file's usual rider:
`SimplifyHard` 1.60 s → 0.92 s, `SolveHard` 879 → 772 ms, `SolveMediumHard` 88 → 78 ms,
`SimplifyEasy` 110 → 89 µs. The gate's baseline was taken from this run, which is the case its
section above names as the one where a red gate means nothing is wrong.

What is left of a `SimplifyHard` level, for whoever comes next: the remaining registration
(431 MB), and the three expansion candidates — `res.Expand().Simplify(-level)`, the rule-based
factorisation at level 2, and the opened angles expanded — at 177, 170 and 82 MB, each a
recursive simplification of an expanded tree.

## The 1915th, and every release beside it on one machine

The first column measured by `Sources/Utils/benchmark_key_commits.sh` reaching all five entries in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,14 @@ void __IterAddHistory(Entity expr)
history[ncompl] = new HashSet<Entity> { n };
}
__IterAddHistory(expr);
__IterAddHistory(expr.Rewrite(RewriteRules.InvertNegativePowers));
// Only where inverting the negative powers produced a different tree: on the
// same tree the second registration re-sorts it, inner-simplifies it and rates it
// again to add an entity the set already holds. Measured on SimplifyHard, the
// registrations were 862 MB of a level's 976 MB, and this input never has a
// negative power to invert.
var inverted = expr.Rewrite(RewriteRules.InvertNegativePowers);
if (!inverted.Equals(expr))
__IterAddHistory(inverted);

MultithreadingFunctional.ExitIfCancelled();
}
Expand Down
66 changes: 33 additions & 33 deletions Sources/Tests/DotnetBenchmark/performance-baseline.json
Original file line number Diff line number Diff line change
@@ -1,82 +1,82 @@
{
"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": "6b93b4012d54c354537821cc8e768ed8570563ce",
"measuredOn": "2026-08-23",
"commit": "5408dc4967d7e259e7411019c66aa265fe865092",
"measuredOn": "2026-09-07",
"runtime": ".NET 10.0.10",
"machine": "Ubuntu 26.04 LTS, X64, 8 logical cores",
"cases": {
"CompileEasy": {
"allocatedBytes": 16230,
"meanNanoseconds": 217683.91618652345
"allocatedBytes": 10970,
"meanNanoseconds": 191900.1083984375
},
"CompileHard": {
"allocatedBytes": 37733,
"meanNanoseconds": 373193.7995876736
"allocatedBytes": 20396,
"meanNanoseconds": 317031.0014272836
},
"Derivate": {
"allocatedBytes": 52824,
"meanNanoseconds": 17214.95599332063
"allocatedBytes": 52823,
"meanNanoseconds": 14309.438617960612
},
"EvalEasy": {
"allocatedBytes": 0,
"meanNanoseconds": 0.984684884129092
"meanNanoseconds": 1.894969120089497
},
"EvalTrig": {
"allocatedBytes": 1341376,
"meanNanoseconds": 867492.3806423611
"allocatedBytes": 1341377,
"meanNanoseconds": 707465.154296875
},
"EvalTrigPrecise": {
"allocatedBytes": 12742205,
"meanNanoseconds": 26845828.055484693
"meanNanoseconds": 22813158.20535714
},
"ParseEasy": {
"allocatedBytes": 18064,
"meanNanoseconds": 6918.623293346829
"allocatedBytes": 18125,
"meanNanoseconds": 5989.073775155203
},
"ParseHard": {
"allocatedBytes": 3522432,
"meanNanoseconds": 1706600.9333379837
"allocatedBytes": 3531225,
"meanNanoseconds": 1542264.4018229167
},
"RunEasy": {
"allocatedBytes": 0,
"meanNanoseconds": 23.025308667736894
"meanNanoseconds": 20.112695195845195
},
"RunHard": {
"allocatedBytes": 0,
"meanNanoseconds": 345.98671662012737
"meanNanoseconds": 294.40666042055403
},
"RunMedium": {
"allocatedBytes": 0,
"meanNanoseconds": 202.93889632432357
"meanNanoseconds": 169.9120227779661
},
"SimplifyEasy": {
"allocatedBytes": 128100,
"meanNanoseconds": 102040.96768465909
"allocatedBytes": 115763,
"meanNanoseconds": 89245.44518025716
},
"SimplifyHard": {
"allocatedBytes": 3620668728,
"meanNanoseconds": 2160215953.7802196
"allocatedBytes": 2027771744,
"meanNanoseconds": 923699884.4666667
},
"SolveEasy": {
"allocatedBytes": 8852269,
"meanNanoseconds": 6083866.387073863
"allocatedBytes": 8853834,
"meanNanoseconds": 4907726.978125
},
"SolveEasyMedium": {
"allocatedBytes": 95795,
"meanNanoseconds": 37080.269161224365
"allocatedBytes": 97335,
"meanNanoseconds": 30335.062217203777
},
"SolveHard": {
"allocatedBytes": 1431860248,
"meanNanoseconds": 1056388966.8536586
"allocatedBytes": 1057996016,
"meanNanoseconds": 771550577.6666666
},
"SolveMedium": {
"allocatedBytes": 658130,
"meanNanoseconds": 567993.1599434095
"allocatedBytes": 662923,
"meanNanoseconds": 462935.250788762
},
"SolveMediumHard": {
"allocatedBytes": 162304592,
"meanNanoseconds": 99270727.66666667
"allocatedBytes": 126162152,
"meanNanoseconds": 77942153.13333334
}
},
"ungated": {
Expand Down
Loading