diff --git a/Sources/AngouriMath/Functions/InternalAMExtensions.cs b/Sources/AngouriMath/Functions/InternalAMExtensions.cs
index 848eb7704..c83818653 100644
--- a/Sources/AngouriMath/Functions/InternalAMExtensions.cs
+++ b/Sources/AngouriMath/Functions/InternalAMExtensions.cs
@@ -1135,6 +1135,12 @@ public static EInteger Factorial(int n)
{
if (n < 0)
throw new ArgumentOutOfRangeException(nameof(n), "Illegal factorial(n) for n < 0: n = " + n);
+ // Past the cached ones it is a balanced product, and it is not kept. The cache held
+ // every factorial up to the largest asked for, so (10^5)! left 8.6 GB behind after a
+ // full collection, and took 21 s multiplying the whole running product by one factor
+ // at a time. https://github.com/asc-community/AngouriMath/pull/2327#issuecomment-6575158568
+ if (n > MaxCachedFactorial)
+ return EInteger.FromBytes(ProductOfRange(1, n).ToByteArray(), littleEndian: true);
// One volatile read, then an array that is never written again.
var cache = factorialCache;
if (n < cache.Length)
@@ -1156,6 +1162,33 @@ public static EInteger Factorial(int n)
}
}
+ ///
+ /// The largest factorial kept in . Every one below it is kept as
+ /// well, half a megabyte in all at this bound; their total size grows as the square of it.
+ ///
+ private const int MaxCachedFactorial = 1000;
+
+ ///
+ /// from (from + 1) ... to, for 1 <= from <= to + 1, as a balanced tree of
+ /// products, so that the multiplications are of numbers of like size. Multiplying a running
+ /// product by one small factor at a time copies the whole product every time. In
+ /// , whose multiplication of large numbers is
+ /// subquadratic: 1 2 ... 10^5 takes 0.1 s and 7 MB there, and 0.44 s and 453 MB in
+ /// .
+ ///
+ private static System.Numerics.BigInteger ProductOfRange(int from, int to)
+ {
+ if (to - from < 16)
+ {
+ var product = System.Numerics.BigInteger.One;
+ for (var i = from; i <= to; i++)
+ product *= i;
+ return product;
+ }
+ var middle = from + (to - from) / 2;
+ return ProductOfRange(from, middle) * ProductOfRange(middle + 1, to);
+ }
+
public static EInteger Factorial(this EInteger n) =>
n.CanFitInInt32()
? Factorial(n.ToInt32Checked())
diff --git a/Sources/Tests/UnitTests/Common/ALargeFactorialTest.cs b/Sources/Tests/UnitTests/Common/ALargeFactorialTest.cs
new file mode 100644
index 000000000..75e9a5b60
--- /dev/null
+++ b/Sources/Tests/UnitTests/Common/ALargeFactorialTest.cs
@@ -0,0 +1,60 @@
+//
+// 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;
+using System.Numerics;
+using AngouriMath;
+using Xunit;
+using static AngouriMath.Entity.Number;
+
+namespace AngouriMath.Tests.Common
+{
+ ///
+ /// An exact factorial. Every factorial up to the largest one asked for was kept, each made from
+ /// the one before by a single multiplication, so (10^5)! took 21 s and left 8.6 GB
+ /// behind after a full collection. Past the small ones it is now a balanced product and is not
+ /// kept. https://github.com/asc-community/AngouriMath/pull/2327#issuecomment-6575158568
+ ///
+ [Trait("Area", "Common")]
+ public sealed class ALargeFactorialTest
+ {
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ [InlineData(17)]
+ [InlineData(999)]
+ [InlineData(1000)]
+ [InlineData(1001)]
+ [InlineData(1017)]
+ [InlineData(2500)]
+ public void IsTheProductOfTheWholeNumbersUpToIt(int n)
+ {
+ var product = BigInteger.One;
+ for (var i = 2; i <= n; i++)
+ product *= i;
+ Assert.Equal(Integer.Create(PeterO.Numbers.EInteger.FromString(product.ToString())), ((Entity)$"{n}!").Evaled);
+ }
+
+ // 1000003 is prime, and the remainder is Python's math.factorial(10**5) % 1000003.
+ [Fact]
+ public void TheRemainderOfAHundredThousandFactorial()
+ => Assert.Equal(Integer.Create(353746), ((Entity)"(10^5)! mod 1000003").Evaled);
+
+ // The running product copied the whole of itself once per factor: 276 MB for 20000!, which
+ // is 33 KB. The balanced product allocates about 1 MB. Bytes allocated on this thread are
+ // what is measured, so other tests running beside it do not move the figure.
+ [Fact]
+ public void IsNotMadeOneFactorAtATime()
+ {
+ Entity factorial = "20000!";
+ var before = GC.GetAllocatedBytesForCurrentThread();
+ _ = factorial.Evaled;
+ var allocated = GC.GetAllocatedBytesForCurrentThread() - before;
+ Assert.True(allocated < 20_000_000, $"20000! allocated {allocated} bytes");
+ }
+ }
+}