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

base/twiceprecision.jl needs work #49589

Open
nsajko opened this issue May 1, 2023 · 13 comments
Open

base/twiceprecision.jl needs work #49589

nsajko opened this issue May 1, 2023 · 13 comments
Labels
maths Mathematical functions ranges Everything AbstractRange

Comments

@nsajko
Copy link
Contributor

nsajko commented May 1, 2023

Algorithms for arithmetic (+, -, *, /)

The algorithms for real (as opposed to complex) double-word addition, multiplication and division were seemingly (there are no relevant explanatory comments or references in the code) all created ad-hoc and need to be updated to the standard algos, something I'll try to do in #49569.

Regarding the algorithms for complex numbers, they seem to be missing entirely, causing Base.TwicePrecision{<:Complex} to fall back to the algorithms for real numbers. This is OK for addition because Complex is a Cartesian (orthogonal) representation of complex numbers, but multiplication and division could be quite broken. This could be fixed by adding methods for Base.TwicePrecision{<:Complex} to base/complex.jl. A paper exists for double-word complex multiplication, and I guess I could work something out for division.

Design of the Base.TwicePrecision type

The Base.TwicePrecision type has an awkward and unsafe design. It accepts elements of any type, not just floating-point-based types. I propose this design instead as a replacement:

In base/twiceprecision.jl:

abstract type AbstractMultiWordFloatingPoint{n,T<:Number} end

# some methods for `AbstractMultiWordFloatingPoint` ...

struct MultiWordFloatingPoint{n,T<:AbstractFloat} <:
    AbstractMultiWordFloatingPoint{n,T}

    words::NTuple{n,T}
end

# some methods for `MultiWordFloatingPoint` ...

const DoubleWordFloatingPoint =
    MultiWordFloatingPoint{2,T} where {T<:AbstractFloat}

# some methods for `DoubleWordFloatingPoint`, including arithmetic ...

In base/complex.jl:

struct MultiWordFloatingPointComplex{n,T<:Complex{<:AbstractFloat}} <:
    AbstractMultiWordFloatingPoint{n,T}

    words::NTuple{n,T}
end

# some methods for `MultiWordFloatingPointComplex` ...

const DoubleWordFloatingPointComplex =
    MultiWordFloatingPointComplex{2,T} where {T<:AbstractFloat}

# some methods for `DoubleWordFloatingPointComplex`, including arithmetic ...

Note that MultiWordFloatingPointComplex only makes sense because Complex is an orthogonal coordinate representation of the complex numbers. A polar representation, for example, would be nonsensical in this context because a multi-word representation represents an unevaluated sum.

The n type parameter would be restricted to two in practice for the time being, however I think that having it as a parameter right away gives a more elegant design.

Is supporting complex numbers even necessary?

The point of Base.TwicePrecision is supporting ranges, and while ranges with complex elements do exist, I think the step is always real? This seems like it would mean that double-word algorithms for complex numbers are an unnecessary evil.

EDIT: the answer seems to be yes, supporting complex numbers is necessary:

julia> (1.0:5.0) .* im
0.0 + 1.0im:0.0 + 1.0im:0.0 + 5.0im

julia> (1.0:5.0) .* im .* im
-1.0 + 0.0im:-1.0 + 0.0im:-5.0 + 0.0im

splitprec

This is more of a question than an issue, but I find this dubious so I thought it'd be good to raise it here with the other stuff.

Why does TwicePrecision{<:AbstractFloat}(::Integer) use that splitprec function:

https://github.com/JuliaLang/julia/blob/master/base/twiceprecision.jl#L213-L214

The fallback conversion method TwicePrecision{<:Any}(::Any) is sufficient for converting integers to double-word floating-point:

https://github.com/JuliaLang/julia/blob/master/base/twiceprecision.jl#L203-L207

So it seems that the intention is to give up accuracy on purpose while converting integers to TwicePrecision? It's likely that I'm missing something, but I find this quite strange and I can't imagine why would one desire such behavior. Also, it seems that something similar is happening when the truncbits function is used.

@timholy
Copy link
Member

timholy commented May 1, 2023

As the author of twiceprecision, I would love it if someone who knew this stuff fixed it. The only reason I worked on this at all was because I wanted to make ranges generic (#18777) and I couldn't do that while breaking a bunch of floating-point precision tests. So the current implementation is me thrashing my way towards not breaking any tests, but a more principled effort would be appreciated. I have no useful input to provide about the design, I'm sure you'll come up with something better.

@Seelengrab
Copy link
Contributor

Seelengrab commented May 2, 2023

So it seems that the intention is to give up accuracy on purpose while converting integers to TwicePrecision?

That'd be contradicting its docstring:

In particular, while convert(Float64, i) can be lossy since Float64 has only 53 bits of precision, splitprec(Float64, i) is exact for any Int64/UInt64.

Also, regarding more words:

The Base.TwicePrecision type has an awkward and unsafe design. It accepts elements of any type, not just floating-point-based types. I propose this design instead as a replacement:

abstract type AbstractMultiWordFloatingPoint{n,T<:Number} end
...
struct MultiWordFloatingPointComplex{n,T<:Complex{<:AbstractFloat}} <:

Does Base have the ambition to implement that, or should this be in a package? TwicePrecision is only an internal detail for (mostly accurate) ranges after all, not necessarily a starting point for hypercomplex numbers.

nsajko added a commit to nsajko/julia that referenced this issue May 3, 2023
Safety is now improved in that it's not possible to construct
nonsensical `TwicePrecision` types any more.

Correctness is improved for constructing `TwicePrecision{T}` from
`TwicePrecision{S}`. Previously a call like
`TwicePrecision{Float64}(::TwicePrecision{Float32})` would leave `hi`
and `lo` unnormalized; while a call like
`TwicePrecision{Float32}(::TwicePrecision{Float64})` would additionally
introduce unnecessary truncation.

Accuracy is improved for constructing `TwicePrecision` from types like
`BigFloat` or `Rational`. Previously a call like
`TwicePrecision{Float64}(::BigFloat)` would unconditionally leave `lo`
as zero.

Example code that tests the improvements (all Boolean values should
evaluate to `true`):
```julia
const TwicePrecision = Base.TwicePrecision
const canonicalize2 = Base.canonicalize2

function twiceprecision_roundtrip_is_not_lossy(
    ::Type{S},
    x::T,
) where {S<:Number, T<:Union{Number,TwicePrecision}}
    tw = TwicePrecision{S}(x)
    x == T(tw)
end

function twiceprecision_is_normalized(tw::Tw) where {Tw<:TwicePrecision}
    (hi, lo) = (tw.hi, tw.lo)
    normalized = Tw(canonicalize2(hi, lo)...)
    (abs(lo) ≤ abs(hi)) & (tw == normalized)
end

rand_twiceprecision(::Type{T}) where {T<:Number} = TwicePrecision{T}(rand(widen(T)))

rand_twiceprecision_is_ok(::Type{T}) where {T<:Number} = !iszero(rand_twiceprecision(T).lo)

setprecision(BigFloat, 70)  # The mantissa needs to be just a bit larger than for `Float64`

rand_twiceprecision_is_ok(Float32)
rand_twiceprecision_is_ok(Float32)
rand_twiceprecision_is_ok(Float32)

rand_twiceprecision_is_ok(Float64)
rand_twiceprecision_is_ok(Float64)
rand_twiceprecision_is_ok(Float64)

twiceprecision_roundtrip_is_not_lossy(Float64, rand(BigFloat))
twiceprecision_roundtrip_is_not_lossy(Float64, rand(BigFloat))
twiceprecision_roundtrip_is_not_lossy(Float64, rand(BigFloat))

twiceprecision_roundtrip_is_not_lossy(Float64, rand_twiceprecision(Float32))
twiceprecision_roundtrip_is_not_lossy(Float64, rand_twiceprecision(Float32))
twiceprecision_roundtrip_is_not_lossy(Float64, rand_twiceprecision(Float32))

twiceprecision_is_normalized(TwicePrecision{Float32}(rand_twiceprecision(Float64)))
twiceprecision_is_normalized(TwicePrecision{Float32}(rand_twiceprecision(Float64)))
twiceprecision_is_normalized(TwicePrecision{Float32}(rand_twiceprecision(Float64)))

twiceprecision_is_normalized(TwicePrecision{Float64}(rand_twiceprecision(Float32)))
twiceprecision_is_normalized(TwicePrecision{Float64}(rand_twiceprecision(Float32)))
twiceprecision_is_normalized(TwicePrecision{Float64}(rand_twiceprecision(Float32)))
```

Updates JuliaLang#49589
@nsajko
Copy link
Contributor Author

nsajko commented May 3, 2023

Does Base have the ambition to implement that, or should this be in a package? TwicePrecision is only an internal detail for (mostly accurate) ranges after all, not necessarily a starting point for hypercomplex numbers.

Not sure whether you're aware that TwicePrecision{<:Complex} is already a thing, although the current implementation is obviously lacking. It's used in the code snippets I added above in an edit, or in (1.0:5.0) .+ im for example.

nsajko added a commit to nsajko/julia that referenced this issue May 3, 2023
Safety is now improved in that it's not possible to construct
nonsensical `TwicePrecision` types any more.

Correctness is improved for constructing `TwicePrecision{T}` from
`TwicePrecision{S}`. Previously a call like
`TwicePrecision{Float64}(::TwicePrecision{Float32})` would leave `hi`
and `lo` unnormalized; while a call like
`TwicePrecision{Float32}(::TwicePrecision{Float64})` would additionally
introduce unnecessary truncation.

Accuracy is improved for constructing `TwicePrecision` from types like
`BigFloat` or `Rational`. Previously a call like
`TwicePrecision{Float64}(::BigFloat)` would unconditionally leave `lo`
as zero.

Example code that tests the improvements (all Boolean values should
evaluate to `true`):
```julia
const TwicePrecision = Base.TwicePrecision
const canonicalize2 = Base.canonicalize2

function twiceprecision_roundtrip_is_not_lossy(
    ::Type{S},
    x::T,
) where {S<:Number, T<:Union{Number,TwicePrecision}}
    tw = TwicePrecision{S}(x)
    x == T(tw)
end

function twiceprecision_is_normalized(tw::Tw) where {Tw<:TwicePrecision}
    (hi, lo) = (tw.hi, tw.lo)
    normalized = Tw(canonicalize2(hi, lo)...)
    (abs(lo) ≤ abs(hi)) & (tw == normalized)
end

rand_twiceprecision(::Type{T}) where {T<:Number} = TwicePrecision{T}(rand(widen(T)))

rand_twiceprecision_is_ok(::Type{T}) where {T<:Number} = !iszero(rand_twiceprecision(T).lo)

setprecision(BigFloat, 70)  # The mantissa needs to be just a bit larger than for `Float64`

rand_twiceprecision_is_ok(Float32)
rand_twiceprecision_is_ok(Float32)
rand_twiceprecision_is_ok(Float32)

rand_twiceprecision_is_ok(Float64)
rand_twiceprecision_is_ok(Float64)
rand_twiceprecision_is_ok(Float64)

twiceprecision_roundtrip_is_not_lossy(Float64, rand(BigFloat))
twiceprecision_roundtrip_is_not_lossy(Float64, rand(BigFloat))
twiceprecision_roundtrip_is_not_lossy(Float64, rand(BigFloat))

twiceprecision_roundtrip_is_not_lossy(Float64, rand_twiceprecision(Float32))
twiceprecision_roundtrip_is_not_lossy(Float64, rand_twiceprecision(Float32))
twiceprecision_roundtrip_is_not_lossy(Float64, rand_twiceprecision(Float32))

twiceprecision_is_normalized(TwicePrecision{Float32}(rand_twiceprecision(Float64)))
twiceprecision_is_normalized(TwicePrecision{Float32}(rand_twiceprecision(Float64)))
twiceprecision_is_normalized(TwicePrecision{Float32}(rand_twiceprecision(Float64)))

twiceprecision_is_normalized(TwicePrecision{Float64}(rand_twiceprecision(Float32)))
twiceprecision_is_normalized(TwicePrecision{Float64}(rand_twiceprecision(Float32)))
twiceprecision_is_normalized(TwicePrecision{Float64}(rand_twiceprecision(Float32)))
```

Updates JuliaLang#49589
nsajko added a commit to nsajko/julia that referenced this issue May 3, 2023
Safety is now improved in that constructing nonsensical
`TwicePrecision` types is now less likely.

Correctness is improved for constructing `TwicePrecision{T}` from
`TwicePrecision{S}`. Previously a call like
`TwicePrecision{Float64}(::TwicePrecision{Float32})` would leave `hi`
and `lo` unnormalized; while a call like
`TwicePrecision{Float32}(::TwicePrecision{Float64})` would additionally
introduce unnecessary truncation.

Accuracy is improved for constructing `TwicePrecision` from types like
`BigFloat` or `Rational`. Previously a call like
`TwicePrecision{Float64}(::BigFloat)` would unconditionally leave `lo`
as zero.

Example code that tests the improvements (all Boolean values should
evaluate to `true`):
```julia
const TwicePrecision = Base.TwicePrecision
const canonicalize2 = Base.canonicalize2

function twiceprecision_roundtrip_is_not_lossy(
    ::Type{S},
    x::T,
) where {S<:Number, T<:Union{Number,TwicePrecision}}
    tw = TwicePrecision{S}(x)
    x == T(tw)
end

function twiceprecision_is_normalized(tw::Tw) where {Tw<:TwicePrecision}
    (hi, lo) = (tw.hi, tw.lo)
    normalized = Tw(canonicalize2(hi, lo)...)
    (abs(lo) ≤ abs(hi)) & (tw == normalized)
end

rand_twiceprecision(::Type{T}) where {T<:Number} = TwicePrecision{T}(rand(widen(T)))

rand_twiceprecision_is_ok(::Type{T}) where {T<:Number} = !iszero(rand_twiceprecision(T).lo)

setprecision(BigFloat, 70)  # The mantissa needs to be just a bit larger than for `Float64`

rand_twiceprecision_is_ok(Float32)
rand_twiceprecision_is_ok(Float32)
rand_twiceprecision_is_ok(Float32)

rand_twiceprecision_is_ok(Float64)
rand_twiceprecision_is_ok(Float64)
rand_twiceprecision_is_ok(Float64)

twiceprecision_roundtrip_is_not_lossy(Float64, rand(BigFloat))
twiceprecision_roundtrip_is_not_lossy(Float64, rand(BigFloat))
twiceprecision_roundtrip_is_not_lossy(Float64, rand(BigFloat))

twiceprecision_roundtrip_is_not_lossy(Float64, rand_twiceprecision(Float32))
twiceprecision_roundtrip_is_not_lossy(Float64, rand_twiceprecision(Float32))
twiceprecision_roundtrip_is_not_lossy(Float64, rand_twiceprecision(Float32))

twiceprecision_is_normalized(TwicePrecision{Float32}(rand_twiceprecision(Float64)))
twiceprecision_is_normalized(TwicePrecision{Float32}(rand_twiceprecision(Float64)))
twiceprecision_is_normalized(TwicePrecision{Float32}(rand_twiceprecision(Float64)))

twiceprecision_is_normalized(TwicePrecision{Float64}(rand_twiceprecision(Float32)))
twiceprecision_is_normalized(TwicePrecision{Float64}(rand_twiceprecision(Float32)))
twiceprecision_is_normalized(TwicePrecision{Float64}(rand_twiceprecision(Float32)))
```

Updates JuliaLang#49589
nsajko added a commit to nsajko/julia that referenced this issue May 3, 2023
Safety is now improved in that constructing nonsensical
`TwicePrecision` types is now less likely.

Correctness is improved for constructing `TwicePrecision{T}` from
`TwicePrecision{S}`. Previously a call like
`TwicePrecision{Float64}(::TwicePrecision{Float32})` would leave `hi`
and `lo` unnormalized; while a call like
`TwicePrecision{Float32}(::TwicePrecision{Float64})` would additionally
introduce unnecessary truncation.

Accuracy is improved for constructing `TwicePrecision` from types like
`BigFloat` or `Rational`. Previously a call like
`TwicePrecision{Float64}(::BigFloat)` would unconditionally leave `lo`
as zero.

Example code that tests the improvements (all Boolean values should
evaluate to `true`):
```julia
const TwicePrecision = Base.TwicePrecision
const canonicalize2 = Base.canonicalize2

function twiceprecision_roundtrip_is_not_lossy(
    ::Type{S},
    x::T,
) where {S<:Number, T<:Union{Number,TwicePrecision}}
    tw = TwicePrecision{S}(x)
    x == T(tw)
end

function twiceprecision_is_normalized(tw::Tw) where {Tw<:TwicePrecision}
    (hi, lo) = (tw.hi, tw.lo)
    normalized = Tw(canonicalize2(hi, lo)...)
    (abs(lo) ≤ abs(hi)) & (tw == normalized)
end

rand_twiceprecision(::Type{T}) where {T<:Number} = TwicePrecision{T}(rand(widen(T)))

rand_twiceprecision_is_ok(::Type{T}) where {T<:Number} = !iszero(rand_twiceprecision(T).lo)

setprecision(BigFloat, 70)  # The mantissa needs to be just a bit larger than for `Float64`

rand_twiceprecision_is_ok(Float32)
rand_twiceprecision_is_ok(Float32)
rand_twiceprecision_is_ok(Float32)

rand_twiceprecision_is_ok(Float64)
rand_twiceprecision_is_ok(Float64)
rand_twiceprecision_is_ok(Float64)

twiceprecision_roundtrip_is_not_lossy(Float64, rand(BigFloat))
twiceprecision_roundtrip_is_not_lossy(Float64, rand(BigFloat))
twiceprecision_roundtrip_is_not_lossy(Float64, rand(BigFloat))

twiceprecision_roundtrip_is_not_lossy(Float64, rand_twiceprecision(Float32))
twiceprecision_roundtrip_is_not_lossy(Float64, rand_twiceprecision(Float32))
twiceprecision_roundtrip_is_not_lossy(Float64, rand_twiceprecision(Float32))

twiceprecision_is_normalized(TwicePrecision{Float32}(rand_twiceprecision(Float64)))
twiceprecision_is_normalized(TwicePrecision{Float32}(rand_twiceprecision(Float64)))
twiceprecision_is_normalized(TwicePrecision{Float32}(rand_twiceprecision(Float64)))

twiceprecision_is_normalized(TwicePrecision{Float64}(rand_twiceprecision(Float32)))
twiceprecision_is_normalized(TwicePrecision{Float64}(rand_twiceprecision(Float32)))
twiceprecision_is_normalized(TwicePrecision{Float64}(rand_twiceprecision(Float32)))
```

Updates JuliaLang#49589
@Seelengrab
Copy link
Contributor

I'm specifically referring to the n-dimensionality of AbstractMultiWordFloatingPoint{n,T<:Number}. Complex currently is decidedly two dimensional after all - making this more generic over N dimensions to me seems more suited for a package than Base.

The non-Number element type is, I think, intentional, to support usecases like Unitful.jl, which aren't <: Number.

@timholy
Copy link
Member

timholy commented May 3, 2023

Adding to @Seelengrab's points, note that the limited functionality is largely intentional. Were this functionality to become public, one then must ask how you create ranges of TwicePrecision objects? You might need TwicePrecision{TwicePrecision{T}} which seems potentially difficult. If TwicePrecision remains hidden away then it can continue (soon, be better at!) generating accurate ranges without having to worry about recursion.

@Seelengrab
Copy link
Contributor

Seelengrab commented May 3, 2023

Yes - TwicePrecision is a tool for getting more accurate ranges of floating point numbers, not a tool for actually implementing generic DoubleWord arithmetic (though of course that's what it emulates under the hood..). The existence of the PhysQuantities tests involving them makes me a bit nervous in terms of what we can actually change about TwicePrecision, but my gut says that improvements purely related to TwicePrecision{<:IEEEFloat} and TwicePrecision{<:Complex{<:IEEEFloat}} and interactions among these two ought to be in reach (and is what I think @nsajko is trying to achieve).

@nsajko
Copy link
Contributor Author

nsajko commented May 3, 2023

Just disregard MultiWordFloatingPoint then, it's really not important.

nsajko added a commit to nsajko/julia that referenced this issue May 3, 2023
Correctness is improved for constructing `TwicePrecision{T}` from
`TwicePrecision{S}`. Previously a call like
`TwicePrecision{Float64}(::TwicePrecision{Float32})` would leave `hi`
and `lo` unnormalized; while a call like
`TwicePrecision{Float32}(::TwicePrecision{Float64})` would additionally
introduce unnecessary truncation.

Accuracy is improved for constructing `TwicePrecision` from types like
`BigFloat` or `Rational`. Previously a call like
`TwicePrecision{Float64}(::BigFloat)` would unconditionally leave `lo`
as zero.

Example code that tests the improvements (all Boolean values should
evaluate to `true`):
```julia
const TwicePrecision = Base.TwicePrecision
const canonicalize2 = Base.canonicalize2

function twiceprecision_roundtrip_is_not_lossy(
    ::Type{S},
    x::T,
) where {S<:Number, T<:Union{Number,TwicePrecision}}
    tw = TwicePrecision{S}(x)
    x == T(tw)
end

function twiceprecision_is_normalized(tw::Tw) where {Tw<:TwicePrecision}
    (hi, lo) = (tw.hi, tw.lo)
    normalized = Tw(canonicalize2(hi, lo)...)
    (abs(lo) ≤ abs(hi)) & (tw == normalized)
end

rand_twiceprecision(::Type{T}) where {T<:Number} = TwicePrecision{T}(rand(widen(T)))

rand_twiceprecision_is_ok(::Type{T}) where {T<:Number} = !iszero(rand_twiceprecision(T).lo)

setprecision(BigFloat, 70)  # The mantissa needs to be just a bit larger than for `Float64`

rand_twiceprecision_is_ok(Float32)
rand_twiceprecision_is_ok(Float32)
rand_twiceprecision_is_ok(Float32)

rand_twiceprecision_is_ok(Float64)
rand_twiceprecision_is_ok(Float64)
rand_twiceprecision_is_ok(Float64)

twiceprecision_roundtrip_is_not_lossy(Float64, rand(BigFloat))
twiceprecision_roundtrip_is_not_lossy(Float64, rand(BigFloat))
twiceprecision_roundtrip_is_not_lossy(Float64, rand(BigFloat))

twiceprecision_roundtrip_is_not_lossy(Float64, rand_twiceprecision(Float32))
twiceprecision_roundtrip_is_not_lossy(Float64, rand_twiceprecision(Float32))
twiceprecision_roundtrip_is_not_lossy(Float64, rand_twiceprecision(Float32))

twiceprecision_is_normalized(TwicePrecision{Float32}(rand_twiceprecision(Float64)))
twiceprecision_is_normalized(TwicePrecision{Float32}(rand_twiceprecision(Float64)))
twiceprecision_is_normalized(TwicePrecision{Float32}(rand_twiceprecision(Float64)))

twiceprecision_is_normalized(TwicePrecision{Float64}(rand_twiceprecision(Float32)))
twiceprecision_is_normalized(TwicePrecision{Float64}(rand_twiceprecision(Float32)))
twiceprecision_is_normalized(TwicePrecision{Float64}(rand_twiceprecision(Float32)))
```

Updates JuliaLang#49589
nsajko added a commit to nsajko/julia that referenced this issue May 4, 2023
Update the arithmetic code to use algorithms published since
twiceprecision.jl got written.

Handle complex numbers.

Prevent harmful promotions.

Some of the introduced extended precision arithmetic code will also be
used in `base/div.jl` to tackle issue JuliaLang#49450 with PR JuliaLang#49561.

TODO: evaluate the accuracy improvements and write tests after JuliaLang#49616
gets merged

Results:

The example in JuliaLang#33677 still gives the same result.

I wasn't able to evaluate JuliaLang#23497 because of how much Julia changed in
the meantime.

TODO: check whether there are improvements for JuliaLang#26553

Updates JuliaLang#49589
nsajko added a commit to nsajko/julia that referenced this issue May 4, 2023
Update the arithmetic code to use algorithms published since
twiceprecision.jl got written.

Handle complex numbers.

Prevent harmful promotions.

Some of the introduced extended precision arithmetic code will also be
used in `base/div.jl` to tackle issue JuliaLang#49450 with PR JuliaLang#49561.

TODO: evaluate the accuracy improvements and write tests after JuliaLang#49616
gets merged

Results:

The example in JuliaLang#33677 still gives the same result.

I wasn't able to evaluate JuliaLang#23497 because of how much Julia changed in
the meantime.

TODO: check whether there are improvements for JuliaLang#26553

Updates JuliaLang#49589
nsajko added a commit to nsajko/julia that referenced this issue May 4, 2023
Update the arithmetic code to use algorithms published since
twiceprecision.jl got written.

Handle complex numbers.

Prevent harmful promotions.

Some of the introduced extended precision arithmetic code will also be
used in `base/div.jl` to tackle issue JuliaLang#49450 with PR JuliaLang#49561.

TODO: evaluate the accuracy improvements and write tests after JuliaLang#49616
gets merged

Results:

The example in JuliaLang#33677 still gives the same result.

I wasn't able to evaluate JuliaLang#23497 because of how much Julia changed in
the meantime.

TODO: check whether there are improvements for JuliaLang#26553

Updates JuliaLang#49589
KristofferC pushed a commit that referenced this issue May 4, 2023
Correctness is improved for constructing `TwicePrecision{T}` from
`TwicePrecision{S}`. Previously a call like
`TwicePrecision{Float64}(::TwicePrecision{Float32})` would leave `hi`
and `lo` unnormalized; while a call like
`TwicePrecision{Float32}(::TwicePrecision{Float64})` would additionally
introduce unnecessary truncation.

Accuracy is improved for constructing `TwicePrecision` from types like
`BigFloat` or `Rational`. Previously a call like
`TwicePrecision{Float64}(::BigFloat)` would unconditionally leave `lo`
as zero.

Example code that tests the improvements (all Boolean values should
evaluate to `true`):
```julia
const TwicePrecision = Base.TwicePrecision
const canonicalize2 = Base.canonicalize2

function twiceprecision_roundtrip_is_not_lossy(
    ::Type{S},
    x::T,
) where {S<:Number, T<:Union{Number,TwicePrecision}}
    tw = TwicePrecision{S}(x)
    x == T(tw)
end

function twiceprecision_is_normalized(tw::Tw) where {Tw<:TwicePrecision}
    (hi, lo) = (tw.hi, tw.lo)
    normalized = Tw(canonicalize2(hi, lo)...)
    (abs(lo) ≤ abs(hi)) & (tw == normalized)
end

rand_twiceprecision(::Type{T}) where {T<:Number} = TwicePrecision{T}(rand(widen(T)))

rand_twiceprecision_is_ok(::Type{T}) where {T<:Number} = !iszero(rand_twiceprecision(T).lo)

setprecision(BigFloat, 70)  # The mantissa needs to be just a bit larger than for `Float64`

rand_twiceprecision_is_ok(Float32)
rand_twiceprecision_is_ok(Float32)
rand_twiceprecision_is_ok(Float32)

rand_twiceprecision_is_ok(Float64)
rand_twiceprecision_is_ok(Float64)
rand_twiceprecision_is_ok(Float64)

twiceprecision_roundtrip_is_not_lossy(Float64, rand(BigFloat))
twiceprecision_roundtrip_is_not_lossy(Float64, rand(BigFloat))
twiceprecision_roundtrip_is_not_lossy(Float64, rand(BigFloat))

twiceprecision_roundtrip_is_not_lossy(Float64, rand_twiceprecision(Float32))
twiceprecision_roundtrip_is_not_lossy(Float64, rand_twiceprecision(Float32))
twiceprecision_roundtrip_is_not_lossy(Float64, rand_twiceprecision(Float32))

twiceprecision_is_normalized(TwicePrecision{Float32}(rand_twiceprecision(Float64)))
twiceprecision_is_normalized(TwicePrecision{Float32}(rand_twiceprecision(Float64)))
twiceprecision_is_normalized(TwicePrecision{Float32}(rand_twiceprecision(Float64)))

twiceprecision_is_normalized(TwicePrecision{Float64}(rand_twiceprecision(Float32)))
twiceprecision_is_normalized(TwicePrecision{Float64}(rand_twiceprecision(Float32)))
twiceprecision_is_normalized(TwicePrecision{Float64}(rand_twiceprecision(Float32)))
```

Updates #49589
nsajko added a commit to nsajko/julia that referenced this issue May 4, 2023
Update the arithmetic code to use algorithms published since
twiceprecision.jl got written.

Handle complex numbers.

Prevent some harmful promotions.

Some of the introduced extended precision arithmetic code will also be
used in `base/div.jl` to tackle issue JuliaLang#49450 with PR JuliaLang#49561.

TODO: write tests

TODO: some tests fail sometimes:
```
Test Failed at /home/nsajko/tmp/julia/test/ranges.jl:223
  Expression: cmp_sn2(Tw(xw / yw), astuple(x / y)..., slopbits)
```

Results:

The example in JuliaLang#33677 still gives the same result.

I wasn't able to evaluate JuliaLang#23497 because of how much Julia changed in
the meantime.

Proof that JuliaLang#26553 is fixed:
```julia-repl
julia> const TwicePrecision = Base.TwicePrecision
Base.TwicePrecision

julia> a = TwicePrecision{Float64}(0.42857142857142855, 2.3790493384824782e-17)
Base.TwicePrecision{Float64}(0.42857142857142855, 2.3790493384824782e-17)

julia> b = TwicePrecision{Float64}(3.4, 8.881784197001253e-17)
Base.TwicePrecision{Float64}(3.4, 8.881784197001253e-17)

julia> a_minus_b_inexact = Tuple(a - b)
(-2.9714285714285715, 1.0150610510858574e-16)

julia> BigFloat(a) == sum(map(BigFloat, Tuple(a)))
true

julia> BigFloat(b) == sum(map(BigFloat, Tuple(b)))
true

julia> a_minus_b = BigFloat(a) - BigFloat(b)
-2.971428571428571428571428571428577679589762353999797347402693647078382455095635

julia> Float64(a_minus_b) == first(a_minus_b_inexact)
true

julia> Float64(a_minus_b - Float64(a_minus_b)) == a_minus_b_inexact[begin + 1]
true
```

Fixes JuliaLang#26553

Updates JuliaLang#49589
@simonbyrne
Copy link
Contributor

simonbyrne commented May 4, 2023

Thanks for the enthusiasm!

As noted above, TwicePrecision code is for floating point range computation:

!!! warning
`TwicePrecision` is an internal type used to increase the
precision of floating-point ranges, and not intended for external use.
If you encounter them in real code, the most likely explanation is
that you are directly accessing the fields of a range. Use
the function interface instead, `step(r)` rather than `r.step`

Many of the original design decisions were based on that (in particular, the original design didn't rely on FMAs, as the hardware operation wasn't available on all architectures, but that has since become less of a concern). It isn't really intended to be used as a generic number type (and so doesn't implement all the necessary operations one would want). For a fully-featured type, there is the DoubleFloats.jl package.

The primary downside to making it a full-featured type in Base is that it is extremely difficult to upgrade user code: if you make improvements or fixes to DoubleFloats.jl, then any user can Pkg.update() to get these. Changes in Base can take more than 6 months to wind their way into the release version of Julia.

nsajko added a commit to nsajko/julia that referenced this issue May 4, 2023
Update the arithmetic code to use algorithms published since
twiceprecision.jl got written.

Handle complex numbers.

Prevent some harmful promotions.

Some of the introduced extended precision arithmetic code will also be
used in `base/div.jl` to tackle issue JuliaLang#49450 with PR JuliaLang#49561.

Results:

The example in JuliaLang#33677 still gives the same result.

I wasn't able to evaluate JuliaLang#23497 because of how much Julia changed in
the meantime.

Proof that JuliaLang#26553 is fixed:
```julia-repl
julia> const TwicePrecision = Base.TwicePrecision
Base.TwicePrecision

julia> a = TwicePrecision{Float64}(0.42857142857142855, 2.3790493384824782e-17)
Base.TwicePrecision{Float64}(0.42857142857142855, 2.3790493384824782e-17)

julia> b = TwicePrecision{Float64}(3.4, 8.881784197001253e-17)
Base.TwicePrecision{Float64}(3.4, 8.881784197001253e-17)

julia> a_minus_b_inexact = Tuple(a - b)
(-2.9714285714285715, 1.0150610510858574e-16)

julia> BigFloat(a) == sum(map(BigFloat, Tuple(a)))
true

julia> BigFloat(b) == sum(map(BigFloat, Tuple(b)))
true

julia> a_minus_b = BigFloat(a) - BigFloat(b)
-2.971428571428571428571428571428577679589762353999797347402693647078382455095635

julia> Float64(a_minus_b) == first(a_minus_b_inexact)
true

julia> Float64(a_minus_b - Float64(a_minus_b)) == a_minus_b_inexact[begin + 1]
true
```

Fixes JuliaLang#26553

Updates JuliaLang#49589
@nsajko
Copy link
Contributor Author

nsajko commented May 4, 2023

I never said I want TwicePrecision made public or anything. I just want to fix it as much as possible.

@KristofferC
Copy link
Member

I just want to fix it as much as possible.

I think a good way of going about that is to point to areas where the float range code have bugs and how improving TwicePrecision fixes those bugs

nsajko added a commit to nsajko/julia that referenced this issue May 6, 2023
Disables some constructors to help in catching errors earlier.

Disables converting constructors like, for example,
`TwicePrecision{Float64}(::BigFloat, ::BigFloat)`. This
constructor was actually used in some cases before commit "Base:
TwicePrecision: improve constructors (JuliaLang#49616)" (ce2c3ae),
so disabling the constructor would have brought the possible accuracy
losses and construction of unnormalized double-word numbers to
attention sooner.

Also effectively *blacklists* some types, to prevent them from being
the type of a word in a `TwicePrecision` type. For example,
`TwicePrecision{<:Integer}` objects are now disallowed, which is good
as such a type would be nonsensical. Blacklisting is required, as
opposed to whitelisting, because of the need to support various
number-like types that are not `<:Number`.

In the same way it is now forbidden to construct `TwicePrecision{T}`
objects where `T` is not a concrete type. This should prevent the
construction of `TwicePrecision` objects with the high and low words
of differing types.

Updates JuliaLang#49589
nsajko added a commit to nsajko/julia that referenced this issue May 6, 2023
Disables some constructors to help in catching errors earlier.

Disables converting constructors like, for example,
`TwicePrecision{Float64}(::BigFloat, ::BigFloat)`. This
constructor was actually used in some cases before commit "Base:
TwicePrecision: improve constructors (JuliaLang#49616)" (ce2c3ae),
so disabling the constructor would have brought the possible accuracy
losses and construction of unnormalized double-word numbers to
attention sooner.

Also effectively *blacklists* some types, to prevent them from being
the type of a word in a `TwicePrecision` type. For example,
`TwicePrecision{<:Integer}` objects are now disallowed, which is good
as such a type would be nonsensical. Blacklisting is required, as
opposed to whitelisting via type constraints, because of the need to
support various floating-point-like types that are not
`<:AbstractFloat`, and, more generally, various number-like types that
are not `<:Number` (such as types representing physical units).

In the same way it is now forbidden to construct `TwicePrecision{T}`
objects where `T` is not a concrete type. This should prevent the
construction of `TwicePrecision` objects with the high and low words
of differing types.

Updates JuliaLang#49589
@nsajko
Copy link
Contributor Author

nsajko commented May 6, 2023

When I said

So it seems that the intention is to give up accuracy on purpose while converting integers to TwicePrecision? It's likely that I'm missing something, but I find this quite strange and I can't imagine why would one desire such behavior.

I was referring to this requirement stated in splitprecs documentation:

- `hi` can be exactly multiplied by the `hi` component of another call to `splitprec`.

The requirement seems to be satisfied by ensuring that the least significant bits of the most significant word (hi) are zero, basically reserving low bits of the mantissa for future operations, so I guess that it's also related to these tests of splitprec:

@test endswith(bitstring(hi), repeat('0', Base.Math.significand_bits(T) ÷ 2))

The other requirements satisfied by splitprec seem natural for double-word numbers, while this, last, requirement actually causes loss of precision (because of the mandatory zero bits), making it really stand out.

Can someone who remembers please explain the rationale behind this requirement?

Interestingly, the only place where splitprec is used (apart from in tests), is for constructing TwicePrecision objects from integers:

TwicePrecision{T}(canonicalize2(splitprec(T, i)...)...)

It would, however, seem much more natural to just use the more general constructor immediately above, the effect would be the same except that splitprec reserves precision for later to satisfy the requirement in question.

@simonbyrne
Copy link
Contributor

I'd have to dig through the diffs, but the original motivation was for what is known as Veltkamp splitting, as a way to get higher-precision multiplication. Now that we have hardware fma ops on most platforms, I think most of the code has switched to using that (but I haven't looked for sure).

@JeffreySarnoff
Copy link
Contributor

JeffreySarnoff commented Aug 4, 2023

I'd have to dig through the diffs, but the original motivation was for what is known as Veltkamp splitting, as a way to get higher-precision multiplication. Now that we have hardware fma ops on most platforms, I think most of the code has switched to using that (but I haven't looked for sure).

Most code has switched to using fma, either directly or through llvm allowed muladd->fma or via MuladdMacro.jl afaik

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
maths Mathematical functions ranges Everything AbstractRange
Projects
None yet
Development

No branches or pull requests

7 participants