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
33 changes: 33 additions & 0 deletions Sources/AngouriMath/Functions/InternalAMExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -1156,6 +1162,33 @@ public static EInteger Factorial(int n)
}
}

/// <summary>
/// The largest factorial kept in <see cref="factorialCache"/>. 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.
/// </summary>
private const int MaxCachedFactorial = 1000;

/// <summary>
/// <c>from (from + 1) ... to</c>, for <c>1 &lt;= from &lt;= to + 1</c>, 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
/// <see cref="System.Numerics.BigInteger"/>, 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
/// <see cref="EInteger"/>.
/// </summary>
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())
Expand Down
60 changes: 60 additions & 0 deletions Sources/Tests/UnitTests/Common/ALargeFactorialTest.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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 <c>(10^5)!</c> 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
/// </summary>
[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");
}
}
}
Loading