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

faster inv and div for Complex{Union{Float16, Float32}} #44111

Merged
merged 14 commits into from
Feb 13, 2022

Conversation

oscardssmith
Copy link
Member

As discovered in https://discourse.julialang.org/t/efficient-calculation-of-many-complex-lorentzians-on-a-large-array/76150/21, Complex division for Float16 and Float32 are currently doing a lot of extra work. This still does the math in Float64, but ignores all the extra checks.

@oscardssmith oscardssmith added performance Must go faster complex Complex numbers labels Feb 10, 2022
@oscardssmith
Copy link
Member Author

oscardssmith commented Feb 10, 2022

Benchmarks look pretty good to me

#generates random numbers with widely varying values
myrand() = ComplexF32(reinterpret(Float32, rand(Int32)), reinterpret(Float32, rand(Int32)))
@btime myrand()
  3.528 ns (0 allocations: 0 bytes)

#old
@btime inv(myrand())  
  10.503 ns (0 allocations: 0 bytes)

@btime myrand()/myrand()
  17.030 ns (0 allocations: 0 bytes)
  
#new
@btime inv(myrand())  
  3.983 ns (0 allocations: 0 bytes)

@btime myrand()/myrand()
  6.893 ns (0 allocations: 0 bytes)

@LaurentPlagne
Copy link

LaurentPlagne commented Feb 10, 2022

Maybe I have missed something but isn't it a problem that CFloat32/16 semantic differs from CFloat64 ? (not the same checks).

@oscardssmith
Copy link
Member Author

This isn't quite right yet, but a lot of the checks have to do with numeric issues that don't exist when you do the math in wider precision.

@giordano
Copy link
Contributor

Can we please have correct functions rather than fast ones?

julia> function Base.inv(z::Complex{<:Union{Float16,Float32}})
           c, d = reim(widen(z))
           (isinf(c) | isinf(d)) && return complex(copysign(zero(z), c), flipsign(-zero(z), d))
           mag = inv(muladd(c, c, d^2))
           return oftype(z, Complex(c*mag, -d*mag))
       end

julia> inv(complex(Inf32))
ERROR: MethodError: no method matching copysign(::ComplexF32, ::Float64)
Closest candidates are:
  copysign(::Signed, ::Float64) at /usr/share/julia/base/int.jl:149
  copysign(::Signed, ::Real) at /usr/share/julia/base/int.jl:150
  copysign(::Rational, ::Real) at /usr/share/julia/base/rational.jl:256
  ...
Stacktrace:
 [1] inv(z::ComplexF32)
   @ Main ./REPL[1]:3
 [2] top-level scope
   @ REPL[3]:1

@oscardssmith
Copy link
Member Author

spelling is hard.

@giordano
Copy link
Contributor

Relatedly, division of complex numbers has been plagued by several issues, some of them have been fixed over time, but for example #32992 still stands. Might be a good occasion to fix it?

@oscardssmith
Copy link
Member Author

Can you write me a test script for what the behavior should be? I think I want to merge this without fixing the Float64 edge cases (1.8 feature freeze is coming up), but I'd be happy to fix these in a follow-up PR.

@giordano
Copy link
Contributor

@testset "Complex division by infinity - $T" for T in (Float16, Float32, Float64)
       @test isequal(complex(one(T)) / complex(T(Inf), T(Inf)), complex(zero(T)))
end

Note that with latest version of this PR inv of infinity is still broken:

julia> function Base.:/(z::Complex{T}, w::Complex{T}) where {T<:Union{Float16,Float32}}
           c, d = reim(widen(w))
           a, b = reim(widen(z))
           (isinf(c) | isinf(d)) && return complex(copysign(zero(T), c), flipsign(zero(T), d)) * z
           mag = inv(muladd(c, c, d^2))
           re_part = muladd(a, c, b*d)
           im_part = muladd(b, c, -a*d)
           return oftype(z, Complex(re_part*mag, im_part*mag))
       end

julia> function Base.inv(z::T) where T<:Complex{<:Union{Float16,Float32}}
           c, d = reim(widen(z))
           (isinf(c) | isinf(d)) && return complex(copysign(zero(T), c), flipsign(-zero(T), d))
           mag = inv(muladd(c, c, d^2))
           return oftype(z, Complex(c*mag, -d*mag))
       end

julia> inv(complex(Inf32))
ERROR: MethodError: no method matching copysign(::ComplexF32, ::Float64)
Closest candidates are:
  copysign(::Signed, ::Float64) at /usr/share/julia/base/int.jl:149
  copysign(::Signed, ::Real) at /usr/share/julia/base/int.jl:150
  copysign(::Rational, ::Real) at /usr/share/julia/base/rational.jl:256
  ...
Stacktrace:
 [1] inv(z::ComplexF32)
   @ Main ./REPL[5]:3
 [2] top-level scope
   @ REPL[6]:1

Same error as before

base/complex.jl Outdated Show resolved Hide resolved
@oscardssmith
Copy link
Member Author

@giordano how would you feel about changing the sign of the zeros for a/b to match those of a*inv(b). Currently, big(1.0 + 0.0im)/big(-Inf + Inf*im) != inv(big(-Inf + Inf*im)) which seems wrong.

@giordano
Copy link
Contributor

What you suggest makes sense, but in v1.7.2 I get:

julia> big(1.0 + 0.0im)/big(-Inf + Inf*im) == inv(big(-Inf + Inf*im))
true

so that's already the case? If in master you get something different that looks like a regression

@oscardssmith
Copy link
Member Author

huh, I clearly need to experiment more...

test/complex.jl Outdated
@test isequal(complex(one(T)) / complex(T(Inf), T(-Inf)), complex(zero(T), zero(T))) broken=(T==Float64)
@test isequal(complex(one(T)) / complex(T(-Inf), T(Inf)), complex(-zero(T), -zero(T))) broken=(T in (Float32, Float64))
@test isequal(complex(one(T)) / complex(T(Inf), T(-Inf)), complex(zero(T), zero(T)))
@test isequal(complex(one(T)) / complex(T(-Inf), T(Inf)), complex(zero(T), -zero(T)))
Copy link
Contributor

Choose a reason for hiding this comment

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

Why the sign change?

Copy link
Member Author

Choose a reason for hiding this comment

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

Because I was having trouble with math. Complex numbers in IEEE don't have a multiplicative identity. Complex(1.,0.)*Complex(-0.,-0.)==Complex(0.,-0.) and Complex(1.,-0.)*Complex(-0.,-0.)==Complex(-0.,0.), so my idea for getting the signs wasn't working...

Copy link
Member Author

Choose a reason for hiding this comment

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

do you know a good algorithm for computing the sign of the zeros when we know that the divisor is some type of infinity? I've looked for a bit, but can't come up with anything nice.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it should be the following: in (a + b * im) / (c + d * im), assuming a and b finite and non-zero, and c and d infinite:

  • real part: sign(a) * sign(c)
  • imaginary part: sign(b) * -sign(d)

Copy link
Contributor

@giordano giordano Feb 11, 2022

Choose a reason for hiding this comment

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

For completeness, you should likely refer to the ISO/IEC 10967-3:2006 standard, section "5.2
Imaginary and complex floating point datatypes and operations"

Copy link
Member

Choose a reason for hiding this comment

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

It's a bit worrying to me how e.g. signs in tests are being changed with an off-hand "fix tests" commit. It's to me a bit of a Chesterton's fence situation. A significant amount of work has gone in to making sure that these things are working the way they are and one needs to be quite careful when changing these pretty low-level operations.

Copy link
Member Author

Choose a reason for hiding this comment

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

@giordano thanks for finding the thing that actually works.

base/complex.jl Outdated Show resolved Hide resolved
@oscardssmith
Copy link
Member Author

Is this ready to merge (assuming CI finally works?)

@giordano
Copy link
Contributor

Failures like

  return type ComplexF16 does not match inferred return type Union{ComplexF16, ComplexF32}

and

  Test threw exception
  Expression: (D \ S)::Tridiagonal{elty} == Tridiagonal(Matrix(D) \ Matrix(S))
  TypeError: in typeassert, expected LinearAlgebra.Tridiagonal{ComplexF32, V} where V<:AbstractVector{ComplexF32}, got a value of type LinearAlgebra.Tridiagonal{Union{ComplexF32, ComplexF64}, Vector{Union{ComplexF32, ComplexF64}}}

look relevant

@giordano
Copy link
Contributor

Ok, performance improvements look good and we're reconciling with math for division by infinity across all types. Failure on Linux looks unrelated as far as I could tell, but FreeBSD and macOS tests are stalling, I don't know what's their status nowadays.

@oscardssmith
Copy link
Member Author

Ok. I think this is finally ready.

@oscardssmith oscardssmith merged commit 1e86463 into JuliaLang:master Feb 13, 2022
@oscardssmith oscardssmith deleted the faster-complex-float32-div branch February 13, 2022 23:52
antoine-levitt pushed a commit to antoine-levitt/julia that referenced this pull request Feb 17, 2022
)

* faster inv and div for Complex{Union{Float16, Float32}}
* fix float64 division bug
LilithHafner pushed a commit to LilithHafner/julia that referenced this pull request Feb 22, 2022
)

* faster inv and div for Complex{Union{Float16, Float32}}
* fix float64 division bug
LilithHafner pushed a commit to LilithHafner/julia that referenced this pull request Mar 8, 2022
)

* faster inv and div for Complex{Union{Float16, Float32}}
* fix float64 division bug
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
complex Complex numbers performance Must go faster
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Division of complex finite values by infinite complex values should always return zero
4 participants