Skip to content

Commit 82d8fc1

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 d93b542 commit 82d8fc1

File tree

3 files changed

+131
-110
lines changed

3 files changed

+131
-110
lines changed

lib/bigdecimal.rb

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

5494
# call-seq:
@@ -202,9 +242,7 @@ def sqrt(prec)
202242
ex = exponent / 2
203243
x = _decimal_shift(-2 * ex)
204244
y = BigDecimal(Math.sqrt(x.to_f), 0)
205-
precs = [prec + BigDecimal.double_fig]
206-
precs << 2 + precs.last / 2 while precs.last > BigDecimal.double_fig
207-
precs.reverse_each do |p|
245+
Internal.newton_loop(prec + BigDecimal.double_fig) do |p|
208246
y = y.add(x.div(y, p), p).div(2, p)
209247
end
210248
y._decimal_shift(ex).mult(1, prec)
@@ -239,59 +277,32 @@ def self.log(x, prec)
239277
return BigDecimal(0) if x == 1
240278

241279
prec2 = prec + BigDecimal.double_fig
242-
BigDecimal.save_limit do
243-
BigDecimal.limit(0)
244-
if x > 10 || x < 0.1
245-
log10 = log(BigDecimal(10), prec2)
246-
exponent = x.exponent
247-
x = x._decimal_shift(-exponent)
248-
if x < 0.3
249-
x *= 10
250-
exponent -= 1
251-
end
252-
return (log10 * exponent).add(log(x, prec2), prec)
253-
end
254-
255-
x_minus_one_exponent = (x - 1).exponent
256280

257-
# log(x) = log(sqrt(sqrt(sqrt(sqrt(x))))) * 2**sqrt_steps
258-
sqrt_steps = [Integer.sqrt(prec2) + 3 * x_minus_one_exponent, 0].max
259-
260-
lg2 = 0.3010299956639812
261-
sqrt_prec = prec2 + [-x_minus_one_exponent, 0].max + (sqrt_steps * lg2).ceil
262-
263-
sqrt_steps.times do
264-
x = x.sqrt(sqrt_prec)
265-
end
266-
267-
# Taylor series for log(x) around 1
268-
# log(x) = -log((1 + X) / (1 - X)) where X = (x - 1) / (x + 1)
269-
# log(x) = 2 * (X + X**3 / 3 + X**5 / 5 + X**7 / 7 + ...)
270-
x = (x - 1).div(x + 1, sqrt_prec)
271-
y = x
272-
x2 = x.mult(x, prec2)
273-
1.step do |i|
274-
n = prec2 + x.exponent - y.exponent + x2.exponent
275-
break if n <= 0 || x.zero?
276-
x = x.mult(x2.round(n - x2.exponent), n)
277-
y = y.add(x.div(2 * i + 1, n), prec2)
278-
end
281+
if x < 0.1 || x > 10
282+
exponent = (3 * x).exponent - 1
283+
x = x._decimal_shift(-exponent)
284+
return log(10, prec2).mult(exponent, prec2).add(log(x, prec2), prec)
285+
end
279286

280-
y.mult(2 ** (sqrt_steps + 1), prec)
287+
# Solve exp(y) - x = 0 with Newton's method
288+
# Repeat: y -= (exp(y) - x) / exp(y)
289+
y = BigDecimal(BigDecimal::Internal.float_log(x), 0)
290+
exp_additional_prec = [-(x - 1).exponent, 0].max
291+
BigDecimal::Internal.newton_loop(prec2) do |p|
292+
expy = exp(y, p + exp_additional_prec)
293+
y = y.sub(expy.sub(x, p).div(expy, p), p)
281294
end
295+
y.mult(1, prec)
282296
end
283297

284-
# Taylor series for exp(x) around 0
285-
private_class_method def self._exp_taylor(x, prec) # :nodoc:
286-
xn = BigDecimal(1)
287-
y = BigDecimal(1)
288-
1.step do |i|
289-
n = prec + xn.exponent
290-
break if n <= 0 || xn.zero?
291-
xn = xn.mult(x, n).div(i, n)
292-
y = y.add(xn, prec)
293-
end
294-
y
298+
private_class_method def self._exp_binary_splitting(x, prec) # :nodoc:
299+
return BigDecimal(1) if x.zero?
300+
# Find k that satisfies x**k / k! < 10**(-prec)
301+
log10 = Math.log(10)
302+
logx = BigDecimal::Internal.float_log(x.abs)
303+
step = (1..).bsearch { |k| Math.lgamma(k + 1)[0] - k * logx > prec * log10 }
304+
# exp(x)-1 = x*(1+x/2*(1+x/3*(1+x/4*(1+x/5*(1+...)))))
305+
1 + BigDecimal::Internal.taylor_sum_binary_splitting(x, [*1..step], prec)
295306
end
296307

297308
# call-seq:
@@ -316,11 +327,21 @@ def self.exp(x, prec)
316327
prec2 = prec + BigDecimal.double_fig + cnt
317328
x = x._decimal_shift(-cnt)
318329

319-
# Calculation of exp(small_prec) is fast because calculation of x**n is fast
320-
# Calculation of exp(small_abs) converges fast.
321-
# exp(x) = exp(small_prec_part + small_abs_part) = exp(small_prec_part) * exp(small_abs_part)
322-
x_small_prec = x.round(Integer.sqrt(prec2))
323-
y = _exp_taylor(x_small_prec, prec2).mult(_exp_taylor(x.sub(x_small_prec, prec2), prec2), prec2)
330+
# Decimal form of bit-burst algorithm
331+
# Calculate exp(x.xxxxxxxxxxxxxxxx) as
332+
# exp(x.xx) * exp(0.00xx) * exp(0.0000xxxx) * exp(0.00000000xxxxxxxx)
333+
x = x.mult(1, prec2)
334+
n = 2
335+
y = BigDecimal(1)
336+
BigDecimal.save_limit do
337+
BigDecimal.limit(0)
338+
while x != 0 do
339+
partial_x = x.truncate(n)
340+
x -= partial_x
341+
y = y.mult(_exp_binary_splitting(partial_x, prec2), prec2)
342+
n *= 2
343+
end
344+
end
324345

325346
# calculate exp(x * 10**cnt) from exp(x)
326347
# 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
@@ -73,6 +73,37 @@ def sqrt(x, prec)
7373
end
7474
end
7575

76+
private_class_method def _sin_binary_splitting(x, prec) # :nodoc:
77+
return x if x.zero?
78+
x2 = x.mult(x, prec)
79+
# Find k that satisfies x2**k / (2k+1)! < 10**(-prec)
80+
log10 = Math.log(10)
81+
logx = BigDecimal::Internal.float_log(x.abs)
82+
step = (1..).bsearch { |k| Math.lgamma(2 * k + 1)[0] - 2 * k * logx > prec * log10 }
83+
# Construct denominator sequence for binary splitting
84+
# sin(x) = x*(1-x2/(2*3)*(1-x2/(4*5)*(1-x2/(6*7)*(1-x2/(8*9)*(1-...)))))
85+
ds = (1..step).map {|i| -(2 * i) * (2 * i + 1) }
86+
x.mult(1 + BigDecimal::Internal.taylor_sum_binary_splitting(x2, ds, prec), prec)
87+
end
88+
89+
private_class_method def _sin_around_zero(x, prec) # :nodoc:
90+
# Divide x into several parts
91+
# sin(x.xxxxxxxx...) = sin(x.xx + 0.00xx + 0.0000xxxx + ...)
92+
# Calculate sin of each part and restore sin(0.xxxxxxxx...) using addition theorem.
93+
sin = BigDecimal(0)
94+
cos = BigDecimal(1)
95+
n = 2
96+
while x != 0 do
97+
partial_x = x.truncate(n)
98+
x -= partial_x
99+
s = _sin_binary_splitting(partial_x, prec)
100+
c = (1 - s * s).sqrt(prec)
101+
sin, cos = (sin * c).add(cos * s, prec), (cos * c).sub(sin * s, prec)
102+
n *= 2
103+
end
104+
sin
105+
end
106+
76107
# call-seq:
77108
# sin(decimal, numeric) -> BigDecimal
78109
#
@@ -88,26 +119,9 @@ def sin(x, prec)
88119
BigDecimal::Internal.validate_prec(prec, :sin)
89120
x = BigDecimal::Internal.coerce_to_bigdecimal(x, prec, :sin)
90121
return BigDecimal::Internal.nan_computation_result if x.infinite? || x.nan?
91-
n = prec + BigDecimal.double_fig
92-
one = BigDecimal("1")
93-
two = BigDecimal("2")
122+
n = prec + BigDecimal.double_fig
94123
sign, x = _sin_periodic_reduction(x, n)
95-
x1 = x
96-
x2 = x.mult(x,n)
97-
y = x
98-
d = y
99-
i = one
100-
z = one
101-
while d.nonzero? && ((m = n - (y.exponent - d.exponent).abs) > 0)
102-
m = BigDecimal.double_fig if m < BigDecimal.double_fig
103-
x1 = -x2.mult(x1,n)
104-
i += two
105-
z *= (i-one) * i
106-
d = x1.div(z,m)
107-
y += d
108-
end
109-
y = BigDecimal("1") if y > 1
110-
y.mult(sign, prec)
124+
_sin_around_zero(x, n).mult(sign, prec)
111125
end
112126

113127
# call-seq:
@@ -125,8 +139,9 @@ def cos(x, prec)
125139
BigDecimal::Internal.validate_prec(prec, :cos)
126140
x = BigDecimal::Internal.coerce_to_bigdecimal(x, prec, :cos)
127141
return BigDecimal::Internal.nan_computation_result if x.infinite? || x.nan?
128-
sign, x = _sin_periodic_reduction(x, prec + BigDecimal.double_fig, add_half_pi: true)
129-
sign * sin(x, prec)
142+
n = prec + BigDecimal.double_fig
143+
sign, x = _sin_periodic_reduction(x, n, add_half_pi: true)
144+
_sin_around_zero(x, n).mult(sign, prec)
130145
end
131146

132147
# call-seq:
@@ -164,28 +179,21 @@ def atan(x, prec)
164179
x = BigDecimal::Internal.coerce_to_bigdecimal(x, prec, :atan)
165180
return BigDecimal::Internal.nan_computation_result if x.nan?
166181
n = prec + BigDecimal.double_fig
167-
pi = PI(n)
182+
return PI(n).div(2 * x.infinite?, prec) if x.infinite?
183+
168184
x = -x if neg = x < 0
169-
return pi.div(neg ? -2 : 2, prec) if x.infinite?
170-
return pi.div(neg ? -4 : 4, prec) if x.round(prec) == 1
171-
x = BigDecimal("1").div(x, n) if inv = x > 1
172-
x = (-1 + sqrt(1 + x.mult(x, n), n)).div(x, n) if dbl = x > 0.5
173-
y = x
174-
d = y
175-
t = x
176-
r = BigDecimal("3")
177-
x2 = x.mult(x,n)
178-
while d.nonzero? && ((m = n - (y.exponent - d.exponent).abs) > 0)
179-
m = BigDecimal.double_fig if m < BigDecimal.double_fig
180-
t = -t.mult(x2,n)
181-
d = t.div(r,m)
182-
y += d
183-
r += 2
185+
x = BigDecimal(1).div(x, n) if inv = x < -1 || x > 1
186+
187+
# Solve tan(y) - x = 0 with Newton's method
188+
# Repeat: y -= (tan(y) - x) * cos(y)**2
189+
y = BigDecimal(Math.atan(x.to_f), 0)
190+
BigDecimal::Internal.newton_loop(n) do |p|
191+
s = sin(y, p)
192+
c = (1 - s * s).sqrt(p)
193+
y = y.sub(c * (s.sub(c * x.mult(1, p), p)), p)
184194
end
185-
y *= 2 if dbl
186-
y = pi / 2 - y if inv
187-
y = -y if neg
188-
y.mult(1, prec)
195+
y = PI(n) / 2 - y if inv
196+
y.mult(neg ? -1 : 1, prec)
189197
end
190198

191199
# call-seq:

test/bigdecimal/test_bigmath.rb

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -138,27 +138,19 @@ def test_exp
138138

139139
def test_log
140140
assert_equal(0, BigMath.log(BigDecimal("1.0"), 10))
141-
assert_in_epsilon(Math.log(10)*1000, BigMath.log(BigDecimal("1e1000"), 10))
141+
assert_in_epsilon(1000 * Math.log(10), BigMath.log(BigDecimal("1e1000"), 10))
142+
assert_in_epsilon(19999999999999 * Math.log(10), BigMath.log(BigDecimal("1E19999999999999"), 10))
143+
assert_in_epsilon(-19999999999999 * Math.log(10), BigMath.log(BigDecimal("1E-19999999999999"), 10))
142144
assert_in_exact_precision(
143145
BigDecimal("2.3025850929940456840179914546843642076011014886287729760333279009675726096773524802359972050895982983419677840422862"),
144146
BigMath.log(BigDecimal("10"), 100),
145147
100
146148
)
147149
assert_converge_in_precision {|n| BigMath.log(BigDecimal("2"), n) }
148-
assert_converge_in_precision {|n| BigMath.log(BigDecimal("1e-30") + 1, n) }
149-
assert_converge_in_precision {|n| BigMath.log(BigDecimal("1e-30"), n) }
150+
assert_converge_in_precision {|n| BigMath.log(1 + SQRT2 * BigDecimal("1e-30"), n) }
151+
assert_converge_in_precision {|n| BigMath.log(SQRT2 * BigDecimal("1e-30"), n) }
150152
assert_converge_in_precision {|n| BigMath.log(BigDecimal("1e30"), n) }
151153
assert_converge_in_precision {|n| BigMath.log(SQRT2, n) }
152154
assert_raise(Math::DomainError) {BigMath.log(BigDecimal("-0.1"), 10)}
153-
assert_separately(%w[-rbigdecimal], <<-SRC)
154-
begin
155-
x = BigMath.log(BigDecimal("1E19999999999999"), 10)
156-
rescue FloatDomainError
157-
else
158-
unless x.infinite?
159-
assert_in_epsilon(Math.log(10)*19999999999999, x)
160-
end
161-
end
162-
SRC
163155
end
164156
end

0 commit comments

Comments
 (0)