Skip to content

Conversation

Keno
Copy link
Member

@Keno Keno commented Jan 24, 2022

Details are in the commit messages, but the overall goal here is for a post-#43852 compiler to be able to figure out that all of these are :effect_free and :terminates as well as :nothrow where appropriate. This is achieved by moving the code around a bit and avoiding inference loops. The primary benefit of this is not seen until #43852 is merged, but the change is independent and will be marginally better on the current compiler as well.

xs = xu & ~sign_mask(T)
xs >= exponent_mask(T) && return x # NaN or Inf
k = Int(xs >> significand_bits(T))
k = (xs >> significand_bits(T)) % Int
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the difference between these?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Int throws if the sign bit is set and inference does not have any range modeling to know that that is impossible.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. That's somewhat unfortunate, but this is a good fix then.

# internal use only. Could be written as
# @assume_effects :nothrow exponent()
# but currently this form is easier on the compiler.
function _exponent_finite_nonzero(x::T) where T<:IEEEFloat
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This deserves a better name.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's exponent, but only for for finite and nonzero values. Seems perfectly descriptive to me.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

understood, but given that it's something that I will probably want to use, I think something like nothrow_exponent with documentation as to when it gives wrong answers would be better.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we don't have any precondition enforcement, I do like having the precondition in the name, so it's obvious to people reading it at the callsite what is being asserted here.

@oscardssmith
Copy link
Member

Can you post the performance differences?

@Keno
Copy link
Member Author

Keno commented Jan 24, 2022

It lets the compiler reason about them and delete them if unused (and nothrow), e.g.:

master:

julia> function foo(x)
           x ^ 2.0
           return x
       end
foo (generic function with 1 method)

julia> @code_typed foo(1.0)
CodeInfo(
1 ─     invoke Main.:^(x::Float64, 2.0::Float64)::Any
└──     return x
) => Float64

julia> using BenchmarkTools
[ Info: Precompiling BenchmarkTools [6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf]

julia> @benchmark foo(1.0)
BenchmarkTools.Trial: 10000 samples with 1000 evaluations.
 Range (min … max):  1.697 ns … 17.979 ns  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     1.705 ns              ┊ GC (median):    0.00%
 Time  (mean ± σ):   1.777 ns ±  0.506 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%

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

 Memory estimate: 0 bytes, allocs estimate: 0.

PR + #43899:

julia> function foo(x)
           x ^ 2.0
           return x
       end
foo (generic function with 1 method)

julia> @code_typed foo(1.0)
CodeInfo(
1 ─     return x
) => Float64

julia> using BenchmarkTools
[ Info: Precompiling BenchmarkTools [6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf]

julia> @benchmark foo(1.0)
BenchmarkTools.Trial: 10000 samples with 1000 evaluations.
 Range (min … max):  0.023 ns … 11.227 ns  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     0.025 ns              ┊ GC (median):    0.00%
 Time  (mean ± σ):   0.027 ns ±  0.112 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%

  ▆  █  ▇  ▇  ▇   ▅  ▁  ▁  ▂         ▁               ▂  ▁    ▂
  █▁▁█▁▁█▁▁█▁▁█▁▁▁█▁▁█▁▁█▁▁█▁▁█▁▁▁█▁▁█▁▁█▁▁▄▁▁▁▁▁▁▇▁▁█▁▁█▁▁▄ █
  0.023 ns     Histogram: log(frequency) by time    0.041 ns <

 Memory estimate: 0 bytes, allocs estimate: 0.

@Keno
Copy link
Member Author

Keno commented Jan 24, 2022

Note that this is quite fancy, because the non-throwness is constant dependent:

julia> function foo(x)
           x ^ 2.1
           return x
       end
foo (generic function with 1 method)

julia> @code_typed foo(1.0)
CodeInfo(
1 ─     invoke Main.:^(x::Float64, 2.1::Float64)::Any
└──     return x
) => Float64

Because

julia> foo(-1.0)
ERROR: DomainError with -1.0:
Exponentiation yielding a complex result requires a complex argument.
Replace x^y with (x+0im)^y, Complex(x)^y, or similar.
Stacktrace:
 [1] throw_exp_domainerror(x::Float64)
   @ Base.Math ./math.jl:37
 [2] ^(x::Float64, y::Float64)
   @ Base.Math ./math.jl:983
 [3] foo(x::Float64)
   @ Main ./REPL[5]:2
 [4] top-level scope
   @ REPL[7]:1

@oscardssmith
Copy link
Member

Why is foo getting inferred as Any?

@Keno
Copy link
Member Author

Keno commented Jan 24, 2022

Why is foo getting inferred as Any?

Bit of a display artifact. The type isn't stored if the statement is unused.

Keno added 3 commits January 24, 2022 05:30
We have an inference loop fma_emulated -> ldexp -> ^(::Float64, ::Int) -> fma -> fma_emulated.
The arguments to `^` are constant, so constprop will figure it out, but it does require a bunch
of extra processing. There is a simpler way to write this using elementary bit operations.
Since resolving the inference loop requires constprop, this was breaking #43852. That is
fixable, but I think we should also make this change to avoid having an unnecessary inference
loop in our basic math functions, which will make future analyses easier.
The fact that the `exponent` call in `fma_emulated` requires reasoning
about the ranges of the floating point values in question, which the
compiler is not capable of doing (and is unlikely to ever do automatically).
Thus, in order for the compiler to know that `fma_emulated` (and
by extension `fma`) is :nothrow in a post-#43852 world, create a
separate version of the `exponent` function that assumes its precondition.
We could use `@assume_effects` instead, but this version is currently
slightly easier on the compiler.
The integer branch is nothrow, so if the caller does something like
`^(x::Float64, 2.0)`, we'd like to discover that.
Comment on lines +1003 to +1006
return pow_body(x, y)
end

@inline function pow_body(x::Float64, y::Float64)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps worth putting this inside the function, so it is clearer the purpose of it?

Suggested change
return pow_body(x, y)
end
@inline function pow_body(x::Float64, y::Float64)
return (@inline function pow_body(x::Float64, y::Float64) ... end)(x, y)

Copy link
Member

@vtjnash vtjnash left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM

@oscardssmith oscardssmith merged commit 06ad3f4 into master Jan 24, 2022
@oscardssmith oscardssmith deleted the kf/compilermath branch January 24, 2022 21:07
@oscardssmith oscardssmith added the performance Must go faster label Jan 24, 2022
Keno added a commit that referenced this pull request Jan 25, 2022
Should have been part of #43907, but I forgot.
Keno added a commit that referenced this pull request Jan 26, 2022
LilithHafner pushed a commit to LilithHafner/julia that referenced this pull request Feb 22, 2022
…out (JuliaLang#43907)

* ldexp: Break inference loop

We have an inference loop fma_emulated -> ldexp -> ^(::Float64, ::Int) -> fma -> fma_emulated.
The arguments to `^` are constant, so constprop will figure it out, but it does require a bunch
of extra processing. There is a simpler way to write this using elementary bit operations.
Since resolving the inference loop requires constprop, this was breaking JuliaLang#43852. That is
fixable, but I think we should also make this change to avoid having an unnecessary inference
loop in our basic math functions, which will make future analyses easier.

* Make fma_emulated easier for the compiler to reason about

The fact that the `exponent` call in `fma_emulated` requires reasoning
about the ranges of the floating point values in question, which the
compiler is not capable of doing (and is unlikely to ever do automatically).
Thus, in order for the compiler to know that `fma_emulated` (and
by extension `fma`) is :nothrow in a post-JuliaLang#43852 world, create a
separate version of the `exponent` function that assumes its precondition.
We could use `@assume_effects` instead, but this version is currently
slightly easier on the compiler.

* pow: Make integer vs float branch obvious to constprop

The integer branch is nothrow, so if the caller does something like
`^(x::Float64, 2.0)`, we'd like to discover that.
LilithHafner pushed a commit to LilithHafner/julia that referenced this pull request Feb 22, 2022
LilithHafner pushed a commit to LilithHafner/julia that referenced this pull request Mar 8, 2022
…out (JuliaLang#43907)

* ldexp: Break inference loop

We have an inference loop fma_emulated -> ldexp -> ^(::Float64, ::Int) -> fma -> fma_emulated.
The arguments to `^` are constant, so constprop will figure it out, but it does require a bunch
of extra processing. There is a simpler way to write this using elementary bit operations.
Since resolving the inference loop requires constprop, this was breaking JuliaLang#43852. That is
fixable, but I think we should also make this change to avoid having an unnecessary inference
loop in our basic math functions, which will make future analyses easier.

* Make fma_emulated easier for the compiler to reason about

The fact that the `exponent` call in `fma_emulated` requires reasoning
about the ranges of the floating point values in question, which the
compiler is not capable of doing (and is unlikely to ever do automatically).
Thus, in order for the compiler to know that `fma_emulated` (and
by extension `fma`) is :nothrow in a post-JuliaLang#43852 world, create a
separate version of the `exponent` function that assumes its precondition.
We could use `@assume_effects` instead, but this version is currently
slightly easier on the compiler.

* pow: Make integer vs float branch obvious to constprop

The integer branch is nothrow, so if the caller does something like
`^(x::Float64, 2.0)`, we'd like to discover that.
LilithHafner pushed a commit to LilithHafner/julia that referenced this pull request Mar 8, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
performance Must go faster
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants