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"); + } + } +}