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

constant-folding rational constants #32024

Closed
stevengj opened this issue May 14, 2019 · 8 comments · Fixed by #41258
Closed

constant-folding rational constants #32024

stevengj opened this issue May 14, 2019 · 8 comments · Fixed by #41258
Labels
performance Must go faster

Comments

@stevengj
Copy link
Member

stevengj commented May 14, 2019

It is nice to use rationals for constants like 2//3 that are supposed to be precision-independent, in a context like f(x) = x * (2//3), but they still impose a performance cost because converting them to Float32 or Float64 involves a runtime division. Can this be made to happen at compile-time?

@pure (::Type{T})(x::Rational{<:BitInteger}) where T<:Union{Float16,Float32,Float64} = T(x.num/x.den)

apparently doesn't work — @code_native f(2.3) still shows a runtime division.

This feature was requested in #14324, which was "fixed" in #24362 by @vtjnash — does that mean that there is supposed to be an annotation that enables constant-folding here? (If not, why was #14324 closed?)

In contrast, g(x) = x * (oftype(x,2) / oftype(x,3)) works (@code_native g(3.7) shows that the constant 2/3 ≈ 0.6666666666666666 is constructed at compile time) but is obviously more cumbersome to write.

@stevengj
Copy link
Member Author

stevengj commented Jun 17, 2021

Is there any hope for this? The generated code for f(x) = x * (2//3) with f(2.3) is still abysmal.

@KristofferC
Copy link
Sponsor Member

KristofferC commented Jun 17, 2021

I experimented with the new @aggressive_constprop macro for this a while ago but I didn't manage to get anything useful.

@oscardssmith
Copy link
Member

Does f(x::T) = x * T(2//3) do and better?

@stevengj
Copy link
Member Author

Does f(x::T) = x * T(2//3) do any better?

Not as far as I can tell. (In x * (2//3), the compiler can already statically determine the required type conversion.)

@MasonProtter
Copy link
Contributor

MasonProtter commented Jun 17, 2021

I believe the problem is the construction of the Rational, not the conversion of an existing Rational to floats. Unfortunately, x // y can throw, so I believe that's what's blocking constant prop:

julia> 1//typemin(Int)
ERROR: ArgumentError: invalid rational: denominator can't be typemin(Int64)
Stacktrace:
 [1] __throw_rational_argerror_typemin(T::Type)
   @ Base ./rational.jl:20
 [2] checked_den
   @ ./rational.jl:24 [inlined]
 [3] Rational{Int64}(num::Int64, den::Int64)
   @ Base ./rational.jl:35
 [4] Rational
   @ ./rational.jl:38 [inlined]
 [5] //(n::Int64, d::Int64)
   @ Base ./rational.jl:61
 [6] top-level scope
   @ REPL[43]:1

The problem in the original post could be simplified to this:

julia> h() = 2//3
h (generic function with 2 methods)

julia> @code_typed h()
CodeInfo(
1%1  = invoke Base.divgcd(2::Int64, 3::Int64)::Tuple{Int64, Int64}%2  = Base.getfield(%1, 1)::Int64%3  = Base.getfield(%1, 2)::Int64%4  = Base.slt_int(%3, 0)::Bool
└──       goto #5 if not %4
2%6  = Base.neg_int(%3)::Int64%7  = Base.slt_int(%6, 0)::Bool
└──       goto #4 if not %7
3 ─       invoke Base.__throw_rational_argerror_typemin(Int64::Type)::Union{}
└──       unreachable
4%11 = Base.neg_int(%2)::Int64
5%12 = φ (#4 => %11, #1 => %2)::Int64%13 = φ (#4 => %6, #1 => %3)::Int64%14 = %new(Rational{Int64}, %12, %13)::Rational{Int64}
└──       goto #6
6 ─       goto #7
7 ─       goto #8
8 ─       goto #9
9return %14
) => Rational{Int64}

julia> @btime h()
  15.165 ns (0 allocations: 0 bytes)
2//3

@MasonProtter
Copy link
Contributor

MasonProtter commented Jun 17, 2021

Here is one sloppy solution: just bypass the constructor and don't check for invalid rationals:

julia> (//)(x::T, y::T) where {T <: Integer} = Base.unsafe_rational(T, x, y)
// (generic function with 1 method)

julia> f(x) = x * (2//3)
f (generic function with 1 method)

julia> @code_llvm f(1.1)
;  @ REPL[3]:1 within `f'
define double @julia_f_291(double %0) {
top:
; ┌ @ promotion.jl:322 within `*' @ float.jl:332
   %1 = fmul double %0, 0x3FE5555555555555
; └
  ret double %1
}

@KristofferC
Copy link
Sponsor Member

Unfortunately, x // y can throw, so I believe that's what's blocking constant prop:

Hm, why would that block constant propagation?

@simeonschaub
Copy link
Member

simeonschaub commented Jun 17, 2021

A quick workaround for this is to use

f(x) = x * Base.unsafe_rational(2, 3)

instead. (Edit: Sorry, overread Mason's answer right above)
The problem seems to be indeed that there are multiple checks which may throw errors not only in the Rational constructor itself, but also inside gcd. I am not sure gcd can ever actually throw with the argument checks we already do in the constructor, so maybe we can have a version of gcd that will never throw, so we could perhaps even mark it as @pure if that change alone doesn't fix this.

simeonschaub added a commit that referenced this issue Jun 17, 2021
With this PR:

```julia
julia> f(x) = x * (2//3)
f (generic function with 1 method)

julia> @code_typed f(2.3)
CodeInfo(
1 ─ %1 = Base.mul_float(x, 0.6666666666666666)::Float64
└──      return %1
) => Float64
```

It is a bit unfortunate to have to resort to `@pure` here, but I could
not get it to constant fold any other way. I don't think this usage
should be problematic since the method only accepts `BitInteger`s and
only ever calls methods that really shouldn't be redefined.

fixes #32024
simeonschaub added a commit that referenced this issue Jun 17, 2021
With this PR:

```julia
julia> f(x) = x * (2//3)
f (generic function with 1 method)

julia> @code_typed f(2.3)
CodeInfo(
1 ─ %1 = Base.mul_float(x, 0.6666666666666666)::Float64
└──      return %1
) => Float64
```

It is a bit unfortunate to have to resort to `@pure` here, but I could
not get it to constant fold any other way. I don't think this usage
should be problematic since the method only accepts `BitInteger`s and
only ever calls methods that really shouldn't be redefined.

fixes #32024
simeonschaub added a commit that referenced this issue Jul 24, 2021
With this PR:

```julia
julia> f(x) = x * (2//3)
f (generic function with 1 method)

julia> @code_typed f(2.3)
CodeInfo(
1 ─ %1 = Base.mul_float(x, 0.6666666666666666)::Float64
└──      return %1
) => Float64
```

It is a bit unfortunate to have to resort to `@pure` here, but I could
not get it to constant fold any other way. I don't think this usage
should be problematic since the method only accepts `BitInteger`s and
only ever calls methods that really shouldn't be redefined.

fixes #32024
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 a pull request may close this issue.

5 participants