Skip to content

Create FixedRational type for faster dimensions #21

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

Merged
merged 9 commits into from
Jun 10, 2023
Merged

Conversation

MilesCranmer
Copy link
Member

Rational{Int16} is about 8x slower than simply using Int32/C for a constant C. This PR defines this type as FixedRational{T,denominator}. The default type is now FixedRational{Int32, 2^4 * 3^2 * 5^2 * 7}, which seems like a reasonable choice.

The downside is that one can not represent, e.g., 1/11 in a dimension.

Users can of course use their own type for the dimension type parameter, including the faster FixedRational{Int8,6}. (Though I chose a safer default.)

@oscardssmith could you review this?

@github-actions
Copy link
Contributor

github-actions bot commented Jun 9, 2023

Benchmark Results

main 1b8602e... t[main]/t[1b8602e...]
creation/Quantity(x) 3 ± 0.3 ns 2.7 ± 0.1 ns 1.11
creation/Quantity(x, length=y) 3.2 ± 0.3 ns 3.3 ± 0.3 ns 0.97
time_to_load 0.0836 ± 0.0043 s 0.0894 ± 0.0024 s 0.936
with_numbers/*real 3.5 ± 0.3 ns 3.9 ± 0.3 ns 0.897
with_numbers/^int 0.217 ± 0.049 μs 30.2 ± 4.8 ns 7.2
with_numbers/^int * real 0.205 ± 0.051 μs 26.9 ± 3.8 ns 7.6
with_quantity/+y 5.7 ± 0.5 ns 4.1 ± 0.3 ns 1.39
with_quantity//y 0.287 ± 0.057 μs 1.6 ± 0.2 ns 179
with_self/dimension 1.7 ± 0.1 ns 1.5 ± 0.2 ns 1.13
with_self/inv 8.4 ± 0.3 ns 1.4 ± 0.1 ns 6
with_self/ustrip 1.6 ± 0.1 ns 1.5 ± 0.2 ns 1.07

Benchmark Plots

A plot of the benchmark results have been uploaded as an artifact to the workflow run for this PR.
Go to "Actions"->"Benchmark a pull request"->[the most recent run]->"Artifacts" (at the bottom).

Base.convert(::Type{FixedRational{T,den}}, x::Integer) where {T,den} = unsafe_fixed_rational(x * den, T, Val(den))
Base.convert(::Type{FixedRational{T,den}}, x::Rational) where {T,den} = FixedRational{T,den}(x)
Base.convert(::Type{Rational}, x::FixedRational{T,den}) where {T,den} = Rational{T}(x.num, den)
Base.convert(::Type{AF}, x::FixedRational{T,den}) where {AF<:AbstractFloat,T,den} = convert(AF, x.num / den)
Copy link
Collaborator

Choose a reason for hiding this comment

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

x.num//den

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 converts to an AbstractFloat, so x.num//den would be significantly slower as it would have to compute divgcd first

@oscardssmith
Copy link
Collaborator

I still think this should probably use FixedRational{Int8,6} as the default. I feel like 90% of users would be served fine by Int8, and FixedRational{Int8,6} probably catches at least half of the remainder. Using an 8 bit type will make this a lot faster since it will allow the whole Dimmensions struct to fit in 64 bits which can allow really fast conversions. Other than that, looks good to me.

@MilesCranmer
Copy link
Member Author

Thanks for the review!

I still think this should probably use FixedRational{Int8,6} as the default. I feel like 90% of users would be served fine by Int8, and FixedRational{Int8,6} probably catches at least half of the remainder. Using an 8 bit type will make this a lot faster since it will allow the whole Dimmensions struct to fit in 64 bits which can allow really fast conversions. Other than that, looks good to me.

I agree but I am just worried about introducing bugs for that 10% of users who don't expect such significant rounding. Maybe a compromise would be to recommend FixedRational{Int8,6} as the faster option near the top of the README?

@oscardssmith
Copy link
Collaborator

I think it will be faster even if you make it throw errors for rounding or overflow.

@MilesCranmer
Copy link
Member Author

Tricky to throw errors for rounding, since it happens quite frequently (e.g., x^0.5 technically uses rounding).

I'll just merge for now so we can at least improve speeds over Rational{Int16} (since its quite slow) and we can keep the question open about a faster default. I'll mention the Int8/6 type on the README too.

@oscardssmith
Copy link
Collaborator

That's easy. That turns into a division by 2 which just a check to see if the input is odd (if so, error) is a bitshift by 1. cbrts will be a tiny bit slower since then you have to divrem(x,3) and check the remainder, but it's not too bad.

@MilesCranmer
Copy link
Member Author

Hm, I see. Okay I will think more about this. In principle I am on board with your suggestion; just want to be safe!

@MilesCranmer
Copy link
Member Author

Oof, this is tricky. den should be of type T... But there's no way to indicate that in the type signature:

struct FixedRational{T<:Integer,den::T} <: Real
    num::T
end

is illegal syntax. But otherwise, just writing FixedRational{Int8,6} would not be the same type as FixedRational{Int8,Int8(6)}. But I have no idea how to enforce den be the same type... Unless you always access it with, e.g.,

denom(::FixedRational{T,den}) = convert(T, den)

and hope the compiler can propagate it

@oscardssmith
Copy link
Collaborator

I don't think the type of den actually matters.

@MilesCranmer
Copy link
Member Author

Not sure which one you mean, but I'll answer both:

  1. "It doesn't matter because it will get converted to T": I looked into the LLVM code and this was not the case; it seems to convert to T at the very end, but the calculations are done in Int64. But I think we want Int8 calculations to be done in Int8
  2. "It doesn't matter because den isn't used": It is used in a few functions: in Base.:* (used by power laws), Base.inv, Base.isinteger, and Base.round.

@MilesCranmer
Copy link
Member Author

Okay fixed with this:

denom(::Type{F}) where {T,den,F<:FixedRational{T,den}} = convert(T, den)

which should be used to access the denominator of FixedRational{T,den}, rather than from den itself in the type signature. The compiler seems to be content and still compiles the converted den into the various functions.

@oscardssmith
Copy link
Collaborator

I meant 2, but it's good to know that this fixes it. The other possible fix would be to define unsafe_fixed_rational to have the convert so it will always be correct by construction.

@MilesCranmer MilesCranmer merged commit dd9c83d into main Jun 10, 2023
@MilesCranmer MilesCranmer deleted the fast-fractions branch June 10, 2023 03:56
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

Successfully merging this pull request may close these issues.

2 participants