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

Performance compared to other implementations & supported types #63

Open
johanbluecreek opened this issue Sep 6, 2022 · 4 comments
Open

Comments

@johanbluecreek
Copy link

Hi,

I noticed that if I implement my own log function where I take care of the branch-cut by hand, it performs better than the NaNMath.log:

julia> using NaNMath

julia> using BenchmarkTools

julia> function log_nan(x::T)::T where {T<:Real}
           x <= T(0) && return T(NaN)
           return log(x)
       end
log_nan (generic function with 1 method)

julia> X = rand(Float64, 50_000_000) .* 2 .- 1;

julia> @btime NaNMath.log.(X);
  594.144 ms (5 allocations: 381.47 MiB)

julia> @btime log_nan.(X);
  436.285 ms (5 allocations: 381.47 MiB)

this shows that NaNMath.log performs some ~36% slower than log_nan. (This issue was discovered in the discussion here [1])

An implementation like log_nan above also provides support for Float16 and BigFloat, which both gives StackOverflowError error for NaNMath.log:

julia> NaNMath.log(Float16(0.123))
ERROR: StackOverflowError:
Stacktrace:
 [1] log(x::Float16) (repeats 79984 times)
   @ NaNMath ~/.julia/packages/NaNMath/fmhcd/src/NaNMath.jl:10

julia> NaNMath.log(BigFloat(0.123))
ERROR: StackOverflowError:
Stacktrace:
 [1] log(x::BigFloat) (repeats 79984 times)
   @ NaNMath ~/.julia/packages/NaNMath/fmhcd/src/NaNMath.jl:10

I have only tested log, other functions in NaNMath may have similar issues.

[1] MilesCranmer/SymbolicRegression.jl#109

@MilesCranmer
Copy link

Seems like this is the same for other functions which make ccalls to libm:

for f in (:sin, :cos, :tan, :asin, :acos, :acosh, :atanh, :log, :log2, :log10,
:lgamma, :log1p)
@eval begin
($f)(x::Float64) = ccall(($(string(f)),libm), Float64, (Float64,), x)
($f)(x::Float32) = ccall(($(string(f,"f")),libm), Float32, (Float32,), x)
($f)(x::Real) = ($f)(float(x))
end
end

e.g., a trivial implementation of sin_nan is much faster than NaNMath.sin:

function sin_nan(x::T)::T where {T}
     isfinite(x) || return T(NaN)
     return sin(x)
 end
@btime NaNMath.sin(1.0)
# > 9.758 ns (0 allocations: 0 bytes)
@btime sin_nan(1.0)
# >  1.492 ns (0 allocations: 0 bytes)

However, some functions are the same performance, like NaNMath.sqrt, but, looking at the code, this is a function which is written with a domain check:

sqrt(x::Real) = x < 0.0 ? NaN : Base.sqrt(x)

(side note - why doesn't that sqrt convert NaN to the same type as the input?)

@hurricane007
Copy link

this method

       function log_nan(x::T)::T where {T<:Real}
           x <= T(0) && return T(NaN)
           return log(x)
       end

is indeed faster

julia> @benchmark log_nan(-0.1)
BenchmarkTools.Trial: 10000 samples with 1000 evaluations.
 Range (min … max):  0.800 ns … 132.400 ns  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     1.000 ns               ┊ GC (median):    0.00%
 Time  (mean ± σ):   1.142 ns ±   1.780 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%

           █
  ▂▁▁▁▂▁▁▁▁█▁▁▁▃▁▁▁▁▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▂▁▁▁▁▁▁▁▁▂▁▁▁▃ ▂
  0.8 ns          Histogram: frequency by time         2.1 ns <

 Memory estimate: 0 bytes, allocs estimate: 0.

julia> @benchmark NaNMath.log(-0.1)
BenchmarkTools.Trial: 10000 samples with 1000 evaluations.
 Range (min … max):  2.800 ns … 123.000 ns  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     3.400 ns               ┊ GC (median):    0.00%
 Time  (mean ± σ):   3.425 ns ±   2.681 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%

  ▁ █ ▅▂  ▇ ▇                                                 ▂
  █▃█▁██▁▇█▁█▆▄▁▅▁▁▇▆▁▄▄▁▃▄▁▁▁▁▁▃▁▁▃▁▁▁▁▁▁▃▃▁▁▁▁▁▁▆▁▆▃▁▄▁▁▃█▆ █
  2.8 ns       Histogram: log(frequency) by time       6.9 ns <

 Memory estimate: 0 bytes, allocs estimate: 0.

but there will be an error:

julia> log_nan(-1)
ERROR: InexactError: Int64(NaN)

@MilesCranmer
Copy link

@hurricane007 not sure I see your issue. Just define

log_nan(x::Integer) = log_nan(Float64(x))

log won't return integers anyways

@hurricane007
Copy link

@MilesCranmer Hi Miles, it is about the method from @johanbluecreek

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants