Skip to content

Commit c44cb2c

Browse files
committed
Improve taylor series calculation of exp and sin by binary splitting method
exp and sin becomes orders of magnitude faster. To make log and atan also fast, log and atan now depends on exp and sin. log(x): solve exp(y)-x=0 by Newton's method atan(x): solve tan(y)-x=0 by Newton's method
1 parent 8dec4b7 commit c44cb2c

File tree

3 files changed

+145
-114
lines changed

3 files changed

+145
-114
lines changed

lib/bigdecimal.rb

Lines changed: 77 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,46 @@ def self.nan_computation_result # :nodoc:
6060
end
6161
BigDecimal::NAN
6262
end
63+
64+
# Iteration for Newton's method with increasing precision
65+
def self.newton_loop(prec, initial_precision: BigDecimal.double_fig / 2, safe_margin: 2) # :nodoc:
66+
precs = []
67+
while prec > initial_precision
68+
precs << prec
69+
prec = (precs.last + 1) / 2 + safe_margin
70+
end
71+
precs.reverse_each do |p|
72+
yield p
73+
end
74+
end
75+
76+
# Calculates Math.log(x.to_f) considering large or small exponent
77+
def self.float_log(x) # :nodoc:
78+
Math.log(x._decimal_shift(-x.exponent).to_f) + x.exponent * Math.log(10)
79+
end
80+
81+
# Calculating Taylor series sum using binary splitting method
82+
# Calculates f(x) = (x/d0)*(1+(x/d1)*(1+(x/d2)*(1+(x/d3)*(1+...))))
83+
# x.n_significant_digits or ds.size must be small to be performant.
84+
def self.taylor_sum_binary_splitting(x, ds, prec) # :nodoc:
85+
fs = ds.map {|d| [0, BigDecimal(d)] }
86+
# fs = [[a0, a1], [b0, b1], [c0, c1], ...]
87+
# f(x) = a0/a1+(x/a1)*(1+b0/b1+(x/b1)*(1+c0/c1+(x/c1)*(1+d0/d1+(x/d1)*(1+...))))
88+
while fs.size > 1
89+
# Merge two adjacent fractions
90+
# from: (1 + a0/a1 + x/a1 * (1 + b0/b1 + x/b1 * rest))
91+
# to: (1 + (a0*b1+x*(b0+b1))/(a1*b1) + (x*x)/(a1*b1) * rest)
92+
xn = xn ? xn.mult(xn, prec) : x
93+
fs = fs.each_slice(2).map do |(a, b)|
94+
b ||= [0, BigDecimal(1)._decimal_shift([xn.exponent, 0].max + 2)]
95+
[
96+
(a[0] * b[1]).add(xn * (b[0] + b[1]), prec),
97+
a[1].mult(b[1], prec)
98+
]
99+
end
100+
end
101+
BigDecimal(fs[0][0]).div(fs[0][1], prec)
102+
end
63103
end
64104

65105
# call-seq:
@@ -226,9 +266,7 @@ def sqrt(prec)
226266
ex = exponent / 2
227267
x = _decimal_shift(-2 * ex)
228268
y = BigDecimal(Math.sqrt(x.to_f), 0)
229-
precs = [prec + BigDecimal.double_fig]
230-
precs << 2 + precs.last / 2 while precs.last > BigDecimal.double_fig
231-
precs.reverse_each do |p|
269+
Internal.newton_loop(prec + BigDecimal.double_fig) do |p|
232270
y = y.add(x.div(y, p), p).div(2, p)
233271
end
234272
y._decimal_shift(ex).mult(1, prec)
@@ -264,59 +302,32 @@ def log(x, prec)
264302
return BigDecimal(0) if x == 1
265303

266304
prec2 = prec + BigDecimal.double_fig
267-
BigDecimal.save_limit do
268-
BigDecimal.limit(0)
269-
if x > 10 || x < 0.1
270-
log10 = log(BigDecimal(10), prec2)
271-
exponent = x.exponent
272-
x = x._decimal_shift(-exponent)
273-
if x < 0.3
274-
x *= 10
275-
exponent -= 1
276-
end
277-
return (log10 * exponent).add(log(x, prec2), prec)
278-
end
279-
280-
x_minus_one_exponent = (x - 1).exponent
281305

282-
# log(x) = log(sqrt(sqrt(sqrt(sqrt(x))))) * 2**sqrt_steps
283-
sqrt_steps = [Integer.sqrt(prec2) + 3 * x_minus_one_exponent, 0].max
284-
285-
lg2 = 0.3010299956639812
286-
sqrt_prec = prec2 + [-x_minus_one_exponent, 0].max + (sqrt_steps * lg2).ceil
287-
288-
sqrt_steps.times do
289-
x = x.sqrt(sqrt_prec)
290-
end
291-
292-
# Taylor series for log(x) around 1
293-
# log(x) = -log((1 + X) / (1 - X)) where X = (x - 1) / (x + 1)
294-
# log(x) = 2 * (X + X**3 / 3 + X**5 / 5 + X**7 / 7 + ...)
295-
x = (x - 1).div(x + 1, sqrt_prec)
296-
y = x
297-
x2 = x.mult(x, prec2)
298-
1.step do |i|
299-
n = prec2 + x.exponent - y.exponent + x2.exponent
300-
break if n <= 0 || x.zero?
301-
x = x.mult(x2.round(n - x2.exponent), n)
302-
y = y.add(x.div(2 * i + 1, n), prec2)
303-
end
306+
if x < 0.1 || x > 10
307+
exponent = (3 * x).exponent - 1
308+
x = x._decimal_shift(-exponent)
309+
return log(10, prec2).mult(exponent, prec2).add(log(x, prec2), prec)
310+
end
304311

305-
y.mult(2 ** (sqrt_steps + 1), prec)
312+
# Solve exp(y) - x = 0 with Newton's method
313+
# Repeat: y -= (exp(y) - x) / exp(y)
314+
y = BigDecimal(BigDecimal::Internal.float_log(x), 0)
315+
exp_additional_prec = [-(x - 1).exponent, 0].max
316+
BigDecimal::Internal.newton_loop(prec2) do |p|
317+
expy = exp(y, p + exp_additional_prec)
318+
y = y.sub(expy.sub(x, p).div(expy, p), p)
306319
end
320+
y.mult(1, prec)
307321
end
308322

309-
# Taylor series for exp(x) around 0
310-
private_class_method def _exp_taylor(x, prec) # :nodoc:
311-
xn = BigDecimal(1)
312-
y = BigDecimal(1)
313-
1.step do |i|
314-
n = prec + xn.exponent
315-
break if n <= 0 || xn.zero?
316-
xn = xn.mult(x, n).div(i, n)
317-
y = y.add(xn, prec)
318-
end
319-
y
323+
private_class_method def _exp_binary_splitting(x, prec) # :nodoc:
324+
return BigDecimal(1) if x.zero?
325+
# Find k that satisfies x**k / k! < 10**(-prec)
326+
log10 = Math.log(10)
327+
logx = BigDecimal::Internal.float_log(x.abs)
328+
step = (1..).bsearch { |k| Math.lgamma(k + 1)[0] - k * logx > prec * log10 }
329+
# exp(x)-1 = x*(1+x/2*(1+x/3*(1+x/4*(1+x/5*(1+...)))))
330+
1 + BigDecimal::Internal.taylor_sum_binary_splitting(x, [*1..step], prec)
320331
end
321332

322333
# call-seq:
@@ -341,11 +352,21 @@ def exp(x, prec)
341352
prec2 = prec + BigDecimal.double_fig + cnt
342353
x = x._decimal_shift(-cnt)
343354

344-
# Calculation of exp(small_prec) is fast because calculation of x**n is fast
345-
# Calculation of exp(small_abs) converges fast.
346-
# exp(x) = exp(small_prec_part + small_abs_part) = exp(small_prec_part) * exp(small_abs_part)
347-
x_small_prec = x.round(Integer.sqrt(prec2))
348-
y = _exp_taylor(x_small_prec, prec2).mult(_exp_taylor(x.sub(x_small_prec, prec2), prec2), prec2)
355+
# Decimal form of bit-burst algorithm
356+
# Calculate exp(x.xxxxxxxxxxxxxxxx) as
357+
# exp(x.xx) * exp(0.00xx) * exp(0.0000xxxx) * exp(0.00000000xxxxxxxx)
358+
x = x.mult(1, prec2)
359+
n = 2
360+
y = BigDecimal(1)
361+
BigDecimal.save_limit do
362+
BigDecimal.limit(0)
363+
while x != 0 do
364+
partial_x = x.truncate(n)
365+
x -= partial_x
366+
y = y.mult(_exp_binary_splitting(partial_x, prec2), prec2)
367+
n *= 2
368+
end
369+
end
349370

350371
# calculate exp(x * 10**cnt) from exp(x)
351372
# exp(x * 10**k) = exp(x * 10**(k - 1)) ** 10

lib/bigdecimal/math.rb

Lines changed: 49 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,37 @@ def sqrt(x, prec)
8888
end
8989
end
9090

91+
private_class_method def _sin_binary_splitting(x, prec) # :nodoc:
92+
return x if x.zero?
93+
x2 = x.mult(x, prec)
94+
# Find k that satisfies x2**k / (2k+1)! < 10**(-prec)
95+
log10 = Math.log(10)
96+
logx = BigDecimal::Internal.float_log(x.abs)
97+
step = (1..).bsearch { |k| Math.lgamma(2 * k + 1)[0] - 2 * k * logx > prec * log10 }
98+
# Construct denominator sequence for binary splitting
99+
# sin(x) = x*(1-x2/(2*3)*(1-x2/(4*5)*(1-x2/(6*7)*(1-x2/(8*9)*(1-...)))))
100+
ds = (1..step).map {|i| -(2 * i) * (2 * i + 1) }
101+
x.mult(1 + BigDecimal::Internal.taylor_sum_binary_splitting(x2, ds, prec), prec)
102+
end
103+
104+
private_class_method def _sin_around_zero(x, prec) # :nodoc:
105+
# Divide x into several parts
106+
# sin(x.xxxxxxxx...) = sin(x.xx + 0.00xx + 0.0000xxxx + ...)
107+
# Calculate sin of each part and restore sin(0.xxxxxxxx...) using addition theorem.
108+
sin = BigDecimal(0)
109+
cos = BigDecimal(1)
110+
n = 2
111+
while x != 0 do
112+
partial_x = x.truncate(n)
113+
x -= partial_x
114+
s = _sin_binary_splitting(partial_x, prec)
115+
c = (1 - s * s).sqrt(prec)
116+
sin, cos = (sin * c).add(cos * s, prec), (cos * c).sub(sin * s, prec)
117+
n *= 2
118+
end
119+
sin.clamp(BigDecimal(-1), BigDecimal(1))
120+
end
121+
91122
# call-seq:
92123
# cbrt(decimal, numeric) -> BigDecimal
93124
#
@@ -150,26 +181,9 @@ def sin(x, prec)
150181
prec = BigDecimal::Internal.coerce_validate_prec(prec, :sin)
151182
x = BigDecimal::Internal.coerce_to_bigdecimal(x, prec, :sin)
152183
return BigDecimal::Internal.nan_computation_result if x.infinite? || x.nan?
153-
n = prec + BigDecimal.double_fig
154-
one = BigDecimal("1")
155-
two = BigDecimal("2")
184+
n = prec + BigDecimal.double_fig
156185
sign, x = _sin_periodic_reduction(x, n)
157-
x1 = x
158-
x2 = x.mult(x,n)
159-
y = x
160-
d = y
161-
i = one
162-
z = one
163-
while d.nonzero? && ((m = n - (y.exponent - d.exponent).abs) > 0)
164-
m = BigDecimal.double_fig if m < BigDecimal.double_fig
165-
x1 = -x2.mult(x1,n)
166-
i += two
167-
z *= (i-one) * i
168-
d = x1.div(z,m)
169-
y += d
170-
end
171-
y = BigDecimal("1") if y > 1
172-
y.mult(sign, prec)
186+
_sin_around_zero(x, n).mult(sign, prec)
173187
end
174188

175189
# call-seq:
@@ -187,8 +201,9 @@ def cos(x, prec)
187201
prec = BigDecimal::Internal.coerce_validate_prec(prec, :cos)
188202
x = BigDecimal::Internal.coerce_to_bigdecimal(x, prec, :cos)
189203
return BigDecimal::Internal.nan_computation_result if x.infinite? || x.nan?
190-
sign, x = _sin_periodic_reduction(x, prec + BigDecimal.double_fig, add_half_pi: true)
191-
sign * sin(x, prec)
204+
n = prec + BigDecimal.double_fig
205+
sign, x = _sin_periodic_reduction(x, n, add_half_pi: true)
206+
_sin_around_zero(x, n).mult(sign, prec)
192207
end
193208

194209
# call-seq:
@@ -277,28 +292,21 @@ def atan(x, prec)
277292
x = BigDecimal::Internal.coerce_to_bigdecimal(x, prec, :atan)
278293
return BigDecimal::Internal.nan_computation_result if x.nan?
279294
n = prec + BigDecimal.double_fig
280-
pi = PI(n)
295+
return PI(n).div(2 * x.infinite?, prec) if x.infinite?
296+
281297
x = -x if neg = x < 0
282-
return pi.div(neg ? -2 : 2, prec) if x.infinite?
283-
return pi.div(neg ? -4 : 4, prec) if x.round(prec) == 1
284-
x = BigDecimal("1").div(x, n) if inv = x > 1
285-
x = (-1 + sqrt(1 + x.mult(x, n), n)).div(x, n) if dbl = x > 0.5
286-
y = x
287-
d = y
288-
t = x
289-
r = BigDecimal("3")
290-
x2 = x.mult(x,n)
291-
while d.nonzero? && ((m = n - (y.exponent - d.exponent).abs) > 0)
292-
m = BigDecimal.double_fig if m < BigDecimal.double_fig
293-
t = -t.mult(x2,n)
294-
d = t.div(r,m)
295-
y += d
296-
r += 2
298+
x = BigDecimal(1).div(x, n) if inv = x < -1 || x > 1
299+
300+
# Solve tan(y) - x = 0 with Newton's method
301+
# Repeat: y -= (tan(y) - x) * cos(y)**2
302+
y = BigDecimal(Math.atan(x.to_f), 0)
303+
BigDecimal::Internal.newton_loop(n) do |p|
304+
s = sin(y, p)
305+
c = (1 - s * s).sqrt(p)
306+
y = y.sub(c * (s.sub(c * x.mult(1, p), p)), p)
297307
end
298-
y *= 2 if dbl
299-
y = pi / 2 - y if inv
300-
y = -y if neg
301-
y.mult(1, prec)
308+
y = PI(n) / 2 - y if inv
309+
y.mult(neg ? -1 : 1, prec)
302310
end
303311

304312
# call-seq:

test/bigdecimal/test_bigmath.rb

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,13 @@ def test_sin
182182
assert_converge_in_precision {|n| sin(BigDecimal("1e-30"), n) }
183183
assert_converge_in_precision {|n| sin(BigDecimal(PI(50)), n) }
184184
assert_converge_in_precision {|n| sin(BigDecimal(PI(50) * 100), n) }
185-
assert_operator(sin(PI(30) / 2, 30), :<=, 1)
186-
assert_operator(sin(-PI(30) / 2, 30), :>=, -1)
185+
[:up, :down].each do |mode|
186+
BigDecimal.save_rounding_mode do
187+
BigDecimal.mode(BigDecimal::ROUND_MODE, mode)
188+
assert_operator(sin(PI(30) / 2, 30), :<=, 1)
189+
assert_operator(sin(-PI(30) / 2, 30), :>=, -1)
190+
end
191+
end
187192
end
188193

189194
def test_cos
@@ -205,8 +210,13 @@ def test_cos
205210
assert_converge_in_precision {|n| cos(BigDecimal("1e50"), n) }
206211
assert_converge_in_precision {|n| cos(BigDecimal(PI(50) / 2), n) }
207212
assert_converge_in_precision {|n| cos(BigDecimal(PI(50) * 201 / 2), n) }
208-
assert_operator(cos(PI(30), 30), :>=, -1)
209-
assert_operator(cos(PI(30) * 2, 30), :<=, 1)
213+
[:up, :down].each do |mode|
214+
BigDecimal.save_rounding_mode do
215+
BigDecimal.mode(BigDecimal::ROUND_MODE, mode)
216+
assert_operator(cos(PI(30), 30), :>=, -1)
217+
assert_operator(cos(PI(30) * 2, 30), :<=, 1)
218+
end
219+
end
210220
end
211221

212222
def test_tan
@@ -388,28 +398,20 @@ def test_exp
388398

389399
def test_log
390400
assert_equal(0, log(BigDecimal("1.0"), 10))
391-
assert_in_epsilon(Math.log(10)*1000, log(BigDecimal("1e1000"), 10))
401+
assert_in_epsilon(1000 * Math.log(10), log(BigDecimal("1e1000"), 10))
402+
assert_in_epsilon(19999999999999 * Math.log(10), log(BigDecimal("1E19999999999999"), 10))
403+
assert_in_epsilon(-19999999999999 * Math.log(10), log(BigDecimal("1E-19999999999999"), 10))
392404
assert_in_exact_precision(
393405
BigDecimal("2.3025850929940456840179914546843642076011014886287729760333279009675726096773524802359972050895982983419677840422862"),
394406
log(BigDecimal("10"), 100),
395407
100
396408
)
397409
assert_converge_in_precision {|n| log(BigDecimal("2"), n) }
398-
assert_converge_in_precision {|n| log(BigDecimal("1e-30") + 1, n) }
399-
assert_converge_in_precision {|n| log(BigDecimal("1e-30"), n) }
410+
assert_converge_in_precision {|n| log(1 + SQRT2 * BigDecimal("1e-30"), n) }
411+
assert_converge_in_precision {|n| log(SQRT2 * BigDecimal("1e-30"), n) }
400412
assert_converge_in_precision {|n| log(BigDecimal("1e30"), n) }
401413
assert_converge_in_precision {|n| log(SQRT2, n) }
402414
assert_raise(Math::DomainError) {log(BigDecimal("-0.1"), 10)}
403-
assert_separately(%w[-rbigdecimal], <<-SRC)
404-
begin
405-
x = BigMath.log(BigDecimal("1E19999999999999"), 10)
406-
rescue FloatDomainError
407-
else
408-
unless x.infinite?
409-
assert_in_epsilon(Math.log(10)*19999999999999, x)
410-
end
411-
end
412-
SRC
413415
end
414416

415417
def test_log2

0 commit comments

Comments
 (0)