Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement math.factorial(n) to calculate very large factorials with speed #2558

Closed
wants to merge 5 commits into from
Closed
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
188 changes: 159 additions & 29 deletions src/runtime/math.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,167 @@ def modf(x: f64) -> tuple[f64, f64]:
"""
return (x - f64(int(x)), float(int(x)))

@overload
def factorial(x: i32) -> i32:
"""
Computes the factorial of `x`.
"""

result: i32
result = 0
if x < 0:
return result
result = 1
i: i32
for i in range(1, x+1):
result *= i
return result
def factorial(n: i32) -> i64:
"""Computes the factorial of `n`."""
MAX_LOOKUP_VALUE: i32 = 20
FACTORIAL_LOOKUP_TABLE: list[i64] = [
i64(1),
i64(1),
i64(2),
i64(6),
i64(24),
i64(120),
i64(720),
i64(5040),
i64(40320),
i64(362880),
i64(3628800),
i64(39916800),
i64(479001600),
i64(6227020800),
i64(87178291200),
i64(1307674368000),
i64(20922789888000),
i64(355687428096000),
i64(6402373705728000),
i64(121645100408832000),
i64(2432902008176640000),
]
if n < 0:
# Exceptions are not implemented currently
# raise ValueError("factorial() not defined for negative values")
assert 1 == 0, "factorial() not defined for negative values."
elif n < MAX_LOOKUP_VALUE:
return FACTORIAL_LOOKUP_TABLE[n]
else:
f: list[i32] = [0] * 4300
f[0] = 0
f[1] = 0
f[2] = 0
f[3] = 0
f[4] = 4
f[5] = 6
f[6] = 6
f[7] = 7
f[8] = 1
f[9] = 8
f[10] = 0
f[11] = 0
f[12] = 2
f[13] = 0
f[14] = 9
f[15] = 2
f[16] = 3
f[17] = 4
f[18] = 2

f_size: i32 = 19

i: i32 = 21
while i <= n:
index: i32 = 0
carry: i32 = 0
while index < f_size:
product: i32 = f[index] * i + carry
f[index] = product % 10

carry = product // 10
index += 1

while carry > 0:
f[f_size] = carry % 10
carry = carry // 10
f_size += 1
i += 1

result: str = ""
idx: i32
for idx in range(f_size - 1, -1, -1):
result += str(f[idx])
return i64(0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do understand the approach, here the final result is printed out using concatenation.
I have not looked into the pre-requisites but even after a conversion from string to int, how will you return an integer here? The highest supported annotation we have is i64, which has a maximum value of 2^63 - 1.
This is not large enough to store anything over 20!, which means this will still encounter an overflow and return a garbage value.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have tried something similar in another project, let me know if there are any other approaches. Thanks :)

Copy link
Contributor Author

@kmr-srbh kmr-srbh Feb 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@faze-geek I understand your point. Internally in LPython, the method used for handling values larger than 2 ^ 63 - 1 is handled by the BigInt module we have. The part of the module which does the handling for large numbers is broken and returns a pointer to the value instead. I had worked on a related issue #1356 and proposed implementing the vector<char> method for handling large values. Only then will we be able to handle values larger than 2 ^ 63 - 1.

I had forgotten to mention this prerequisite. Thanks for reminding me. I am adding it above.

Copy link
Contributor Author

@kmr-srbh kmr-srbh Feb 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have tried something similar in another project, let me know if there are any other approaches. Thanks :)

Oh yes there are! As I had mentioned above, the industry standard for scientific computing applications is to use the PrimeSwing algorithm. The Python implementation is available online, but I do not want to type something I do not understand. 😄

The Python math.factorial(n) uses this algorithm.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Will surely explore this once the BigInt module gets working.



@overload
def factorial(n: i64) -> i64:
"""Computes the factorial of `n`."""
MAX_LOOKUP_VALUE: i64 = i64(20)
FACTORIAL_LOOKUP_TABLE: list[i64] = [
i64(1),
i64(1),
i64(2),
i64(6),
i64(24),
i64(120),
i64(720),
i64(5040),
i64(40320),
i64(362880),
i64(3628800),
i64(39916800),
i64(479001600),
i64(6227020800),
i64(87178291200),
i64(1307674368000),
i64(20922789888000),
i64(355687428096000),
i64(6402373705728000),
i64(121645100408832000),
i64(2432902008176640000),
]
if n < i64(0):
# Exceptions are not implemented currently
# raise ValueError("factorial() not defined for negative values")
assert 1 == 0, "factorial() not defined for negative values."
elif n < MAX_LOOKUP_VALUE:
return FACTORIAL_LOOKUP_TABLE[n]
else:
f: list[i32] = [0] * 4300
f[0] = 0
f[1] = 0
f[2] = 0
f[3] = 0
f[4] = 4
f[5] = 6
f[6] = 6
f[7] = 7
f[8] = 1
f[9] = 8
f[10] = 0
f[11] = 0
f[12] = 2
f[13] = 0
f[14] = 9
f[15] = 2
f[16] = 3
f[17] = 4
f[18] = 2

f_size: i32 = 19

i: i32 = 21
while i64(i) <= n:
index: i32 = 0
carry: i32 = 0
while index < f_size:
product: i32 = f[index] * i + carry
f[index] = product % 10

carry = product // 10
index += 1

while carry > 0:
f[f_size] = carry % 10
carry = carry // 10
f_size += 1
i += 1

result: str = ""
idx: i32
for idx in range(f_size - 1, -1, -1):
result += str(f[idx])
return i64(0)

@overload
def factorial(x: i64) -> i64:
"""
Computes the factorial of `x`.
"""
result: i64
result = i64(0)
if x < i64(0):
return result
result = i64(1)
i: i64
for i in range(i64(1), x + i64(1)):
result *= i64(i)
return result

@overload
def floor(x: i32) -> i32:
Expand Down Expand Up @@ -457,7 +588,6 @@ def ldexp(x: f64, i: i32) -> f64:
return result



def mod(a: i32, b: i32) -> i32:
"""
Returns a%b
Expand Down
Loading