Skip to content

Commit

Permalink
power uses Float64 exponents for integers (#53967)
Browse files Browse the repository at this point in the history
Improve performance of `^(::Float64, n::Integer)` in the case of `abs(n)
> 2^13`.

While `pow_body` is unreliable for `abs(n) > 2^25` this implementation
provides errors of a few ULPs, while runtime is capped to that of the
`Float64` implementation.

Fixes  #53881
See also #53886.
  • Loading branch information
KlausC authored Apr 22, 2024
1 parent c1b3661 commit fe49d56
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 13 deletions.
52 changes: 41 additions & 11 deletions base/math.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1261,6 +1261,10 @@ function modf(x::T) where T<:IEEEFloat
return (rx, ix)
end

@inline function use_power_by_squaring(n::Integer)
-2^12 <= n <= 3 * 2^13
end

# @constprop aggressive to help the compiler see the switch between the integer and float
# variants for callers with constant `y`
@constprop :aggressive function ^(x::Float64, y::Float64)
Expand All @@ -1273,24 +1277,33 @@ end
y = sign(y)*0x1.8p62
end
yint = unsafe_trunc(Int64, y) # This is actually safe since julia freezes the result
y == yint && return @noinline x^yint
2*xu==0 && return abs(y)*Inf*(!(y>0)) # if x==0
x<0 && throw_exp_domainerror(x) # |y| is small enough that y isn't an integer
!isfinite(x) && return x*(y>0 || isnan(x)) # x is inf or NaN
yisint = y == yint
if yisint
yint == 0 && return 1.0
use_power_by_squaring(yint) && return @noinline pow_body(x, yint)
end
2*xu==0 && return abs(y)*Inf*(!(y>0)) # if x === +0.0 or -0.0 (Inf * false === 0.0)
s = 1
if x < 0
!yisint && throw_exp_domainerror(x) # y isn't an integer
s = ifelse(isodd(yint), -1, 1)
end
!isfinite(x) && return copysign(x,s)*(y>0 || isnan(x)) # x is inf or NaN
return copysign(pow_body(abs(x), y), s)
end

@assume_effects :foldable @noinline function pow_body(x::Float64, y::Float64)
xu = reinterpret(UInt64, x)
if xu < (UInt64(1)<<52) # x is subnormal
xu = reinterpret(UInt64, x * 0x1p52) # normalize x
xu &= ~sign_mask(Float64)
xu -= UInt64(52) << 52 # mess with the exponent
end
return pow_body(xu, y)
end

@inline function pow_body(xu::UInt64, y::Float64)
logxhi,logxlo = _log_ext(xu)
xyhi, xylo = two_mul(logxhi,y)
xylo = muladd(logxlo, y, xylo)
hi = xyhi+xylo
return Base.Math.exp_impl(hi, xylo-(hi-xyhi), Val(:ℯ))
return @inline Base.Math.exp_impl(hi, xylo-(hi-xyhi), Val(:ℯ))
end

@constprop :aggressive function ^(x::T, y::T) where T <: Union{Float16, Float32}
Expand All @@ -1314,12 +1327,29 @@ end
return T(exp2(log2(abs(widen(x))) * y))
end

# compensated power by squaring
@constprop :aggressive @inline function ^(x::Float64, n::Integer)
x^clamp(n, Int64)
end
@constprop :aggressive @inline function ^(x::Float64, n::Int64)
n == 0 && return one(x)
return pow_body(x, n)
if use_power_by_squaring(n)
return pow_body(x, n)
else
s = ifelse(x < 0 && isodd(n), -1.0, 1.0)
x = abs(x)
y = float(n)
if y == n
return copysign(pow_body(x, y), s)
else
n2 = n % 1024
y = float(n - n2)
return pow_body(x, y) * copysign(pow_body(x, n2), s)
end
end
end

# compensated power by squaring
# this method is only reliable for -2^20 < n < 2^20 (cf. #53881 #53886)
@assume_effects :terminates_locally @noinline function pow_body(x::Float64, n::Integer)
y = 1.0
xnlo = ynlo = 0.0
Expand Down
2 changes: 1 addition & 1 deletion base/special/exp.jl
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ end
twopk = (k + UInt64(53)) << 52
return reinterpret(T, twopk + reinterpret(UInt64, small_part))*0x1p-53
end
#k == 1024 && return (small_part * 2.0) * 2.0^1023
k == 1024 && return (small_part * 2.0) * 2.0^1023
end
twopk = Int64(k) << 52
return reinterpret(T, twopk + reinterpret(Int64, small_part))
Expand Down
2 changes: 1 addition & 1 deletion test/compiler/codegen.jl
Original file line number Diff line number Diff line change
Expand Up @@ -866,7 +866,7 @@ if Sys.ARCH === :x86_64
foo52079() = Core.Intrinsics.have_fma(Float64)
if foo52079() == true
let io = IOBuffer()
code_native(io,^,(Float64,Float64), dump_module=false)
code_native(io,Base.Math.exp_impl,(Float64,Float64,Val{:ℯ}), dump_module=false)
str = String(take!(io))
@test !occursin("fma_emulated", str)
@test occursin("vfmadd", str)
Expand Down
19 changes: 19 additions & 0 deletions test/math.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1470,6 +1470,25 @@ end
# two cases where we have observed > 1 ULP in the past
@test 0.0013653274095082324^-97.60372292227069 == 4.088393948750035e279
@test 8.758520413376658e-5^70.55863059215994 == 5.052076767078296e-287

# issue #53881
c53881 = 2.2844135865398217e222 # check correctness within 2 ULPs
@test prevfloat(1.0) ^ -Int64(2)^62 c53881 atol=2eps(c53881)
@test 2.0 ^ typemin(Int) == 0.0
@test (-1.0) ^ typemin(Int) == 1.0
Z = Int64(2)
E = prevfloat(1.0)
@test E ^ (-Z^54) 7.38905609893065
@test E ^ (-Z^62) 2.2844135865231613e222
@test E ^ (-Z^63) == Inf
@test abs(E ^ (Z^62-1) * E ^ (-Z^62+1) - 1) <= eps(1.0)
n, x = -1065564664, 0.9999997040311492
@test abs(x^n - Float64(big(x)^n)) / eps(x^n) == 0 # ULPs
@test E ^ (big(2)^100 + 1) == 0
@test E ^ 6705320061009595392 == nextfloat(0.0)
n = Int64(1024 / log2(E))
@test E^n == Inf
@test E^float(n) == Inf
end

# Test that sqrt behaves correctly and doesn't exhibit fp80 double rounding.
Expand Down

0 comments on commit fe49d56

Please sign in to comment.