Skip to content

Commit 50b80b1

Browse files
authored
BigMath.exp overflow/underflow check (#523)
* BigMath.exp overflow/underflow check `BigMath.exp(BigDecimal('1e100000000'), 20)` was slow. Skip calculation when x.exponent >= 21 which always result in overflow or underflow. * Add underflow check to BigMath.erfc
1 parent fc54487 commit 50b80b1

File tree

5 files changed

+46
-4
lines changed

5 files changed

+46
-4
lines changed

lib/bigdecimal.rb

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ def self.infinity_computation_result # :nodoc:
5757
BigDecimal::INFINITY
5858
end
5959

60+
def self.underflow_computation_result # :nodoc:
61+
if BigDecimal.mode(BigDecimal::EXCEPTION_ALL).anybits?(BigDecimal::EXCEPTION_UNDERFLOW)
62+
raise FloatDomainError, 'Exponent underflow'
63+
end
64+
BigDecimal(0)
65+
end
66+
6067
def self.nan_computation_result # :nodoc:
6168
if BigDecimal.mode(BigDecimal::EXCEPTION_ALL).anybits?(BigDecimal::EXCEPTION_NaN)
6269
raise FloatDomainError, "Computation results to 'NaN'"
@@ -350,7 +357,17 @@ def exp(x, prec)
350357
prec = BigDecimal::Internal.coerce_validate_prec(prec, :exp)
351358
x = BigDecimal::Internal.coerce_to_bigdecimal(x, prec, :exp)
352359
return BigDecimal::Internal.nan_computation_result if x.nan?
353-
return x.positive? ? BigDecimal::Internal.infinity_computation_result : BigDecimal(0) if x.infinite?
360+
if x.infinite? || x.exponent >= 21 # exp(10**20) and exp(-10**20) overflows/underflows 64-bit exponent
361+
if x.positive?
362+
return BigDecimal::Internal.infinity_computation_result
363+
elsif x.infinite?
364+
# exp(-Infinity) is +0 by definition, this is not an underflow.
365+
return BigDecimal(0)
366+
else
367+
return BigDecimal::Internal.underflow_computation_result
368+
end
369+
end
370+
354371
return BigDecimal(1) if x.zero?
355372

356373
# exp(x * 10**cnt) = exp(x)**(10**cnt)

lib/bigdecimal/math.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -637,7 +637,7 @@ def erfc(x, prec)
637637
return BigDecimal::Internal.nan_computation_result if x.nan?
638638
return BigDecimal(1 - x.infinite?) if x.infinite?
639639
return BigDecimal(1).sub(erf(x, prec + BigDecimal::Internal::EXTRA_PREC), prec) if x < 0.5
640-
return BigDecimal(0) if x > 5000000000 # erfc(5000000000) < 1e-10000000000000000000 (underflow)
640+
return BigDecimal::Internal.underflow_computation_result if x > 5000000000 # erfc(5000000000) < 1e-10000000000000000000 (underflow)
641641

642642
if x >= 8
643643
y = _erfc_asymptotic(x, prec)

test/bigdecimal/helper.rb

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def assert_infinite_calculation(positive:)
7171
BigDecimal.mode(BigDecimal::EXCEPTION_INFINITY, false)
7272
positive ? assert_positive_infinite(yield) : assert_negative_infinite(yield)
7373
BigDecimal.mode(BigDecimal::EXCEPTION_INFINITY, true)
74-
assert_raise_with_message(FloatDomainError, /Infinity/) { yield }
74+
assert_raise_with_message(FloatDomainError, /infinity|overflow/i) { yield }
7575
end
7676
end
7777

@@ -83,6 +83,19 @@ def assert_negative_infinite_calculation(&block)
8383
assert_infinite_calculation(positive: false, &block)
8484
end
8585

86+
def assert_underflow_calculation(accept_overflow: false)
87+
BigDecimal.save_exception_mode do
88+
BigDecimal.mode(BigDecimal::EXCEPTION_UNDERFLOW, false)
89+
BigDecimal.mode(BigDecimal::EXCEPTION_OVERFLOW, false)
90+
assert_equal(BigDecimal(0), yield)
91+
BigDecimal.mode(BigDecimal::EXCEPTION_UNDERFLOW, true)
92+
BigDecimal.mode(BigDecimal::EXCEPTION_OVERFLOW, true)
93+
# Accept internal overflow (e.g. overflow calculating denominator part)
94+
pattern = accept_overflow ? /underflow|overflow/i : /underflow/i
95+
assert_raise_with_message(FloatDomainError, pattern) { yield }
96+
end
97+
end
98+
8699
def assert_nan_calculation(&block)
87100
BigDecimal.save_exception_mode do
88101
BigDecimal.mode(BigDecimal::EXCEPTION_NaN, false)

test/bigdecimal/test_bigdecimal.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2296,13 +2296,24 @@ def test_exp_with_negative
22962296
end
22972297

22982298
def test_exp_with_negative_infinite
2299+
# exp(-infinity) is exactly zero. This is not an underflow.
22992300
assert_equal(0, BigMath.exp(NEGATIVE_INFINITY, 20))
23002301
end
23012302

23022303
def test_exp_with_positive_infinite
23032304
assert_positive_infinite_calculation { BigMath.exp(BigDecimal::INFINITY, 20) }
23042305
end
23052306

2307+
def test_exp_with_overflow_underflow
2308+
assert_underflow_calculation { BigMath.exp(-1e+100, 20) }
2309+
assert_underflow_calculation { BigMath.exp(-0.9e+20, 20) }
2310+
assert_positive_infinite_calculation { BigMath.exp(1e+100, 20) }
2311+
assert_positive_infinite_calculation { BigMath.exp(0.9e+20, 20) }
2312+
huge = BigDecimal("0.1e#{EXPONENT_MAX / 100}")
2313+
assert_positive_infinite_calculation { BigMath.exp(huge, 20) }
2314+
assert_underflow_calculation { BigMath.exp(-huge, 20) }
2315+
end
2316+
23062317
def test_exp_with_nan
23072318
assert_nan_calculation { BigMath.exp(BigDecimal::NAN, 20) }
23082319
end

test/bigdecimal/test_bigmath.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,8 @@ def test_erfc
521521
end
522522
assert_equal(0, BigMath.erfc(PINF, N))
523523
assert_equal(2, BigMath.erfc(MINF, N))
524-
assert_equal(0, BigMath.erfc(BigDecimal('1e400'), 10))
524+
assert_underflow_calculation(accept_overflow: true) { BigMath.erfc(4999999999, 10) }
525+
assert_underflow_calculation { BigMath.erfc(BigDecimal('1e400'), 10) }
525526
assert_equal(2, BigMath.erfc(BigDecimal('-1e400'), 10))
526527
assert_equal(1, BigMath.erfc(BigDecimal('1e-400'), N))
527528

0 commit comments

Comments
 (0)