-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
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
parse x^literal as call to ^(x, Val{literal})? #20527
Comments
The main issue, as always with special syntactic treatment of literals, is that |
@StefanKarpinski, I don't see that as being a problem in practice. The main practical effect is that the performance and the roundoff errors could be different in the two cases, but we can document this and I would think that most practitioners would be willing to accept that in order to get the benefits of hard-coded small powers. Of course, someone could define a screwy method like |
Just submitted a PR to make this concrete. The parser (actually lowering) changes are quite easy. |
I guess the next natural question is if we should allow small negative literal powers since they could be done in a type-stable way with this change. This would probably eliminate 99% of complaints about things like |
I.e. on this PR we can do the following: julia> 5^-2
ERROR: DomainError:
Cannot raise an integer x to a negative power -n.
Make x a float by adding a zero decimal (e.g. 2.0^-n instead of 2^-n), or write 1/x^n, float(x)^-n, or (x//1)^-n.
Stacktrace:
[1] power_by_squaring(::Int64, ::Int64) at ./intfuncs.jl:170
[2] ^(::Int64, ::Type{Val{-2}}) at ./intfuncs.jl:201
julia> Base.:^(x::Integer, ::Type{Val{p}}) where {p} =
p < 0 ? 1/Base.power_by_squaring(x,-p) : Base.power_by_squaring(x,p)
julia> 5^-2
0.04
julia> 5^-31
1.2448081382545486e-19
julia> 5^-32
ERROR: DomainError:
Cannot raise an integer x to a negative power -n.
Make x a float by adding a zero decimal (e.g. 2.0^-n instead of 2^-n), or write 1/x^n, float(x)^-n, or (x//1)^-n.
Stacktrace:
[1] power_by_squaring(::Int64, ::Int64) at ./intfuncs.jl:170
[2] ^(::Int64, ::Int64) at ./intfuncs.jl:194
julia> p = -2
-2
julia> 5^p
ERROR: DomainError:
Cannot raise an integer x to a negative power -n.
Make x a float by adding a zero decimal (e.g. 2.0^-n instead of 2^-n), or write 1/x^n, float(x)^-n, or (x//1)^-n.
Stacktrace:
[1] power_by_squaring(::Int64, ::Int64) at ./intfuncs.jl:170
[2] ^(::Int64, ::Int64) at ./intfuncs.jl:194
julia> 5^-p
25 The power of #265 always makes me smile :) There is something somehow quite different about |
Now that I think about it, there's no reason we should restrict this to small integers. We might as well do it for all literal integers, since code generation only occurs for the integers that someone actually uses. |
That strikes me as somewhat dangerous. It seems almost inevitable that someone will generate code that uses a lot of literal powers and ends up generating absurd amounts of code. OTOH, arbitrary cutoff like this are pretty annoying. |
Since the amount of code generated for |
I worry about code the generates code that calls |
@StefanKarpinski, I still don't see your worry. Say that I generate a long function that calls |
I generally think that implementing compiler optimizations via parser rules is a bit iffy. We already have the optimization that |
It already dispatches to that method internal to MPFR. Given the cost of computing x*x, I would be surprised if the extra runtime test was a measurable cost. |
What about all the cases that currently call |
I really do think there's merit to the idea that |
TLDR - it's probably better to skip this post and the next (the point I wanted to make here is 4 posts below). There's more than this case whereby one parameter is typically static and controls the action of the function (and possibly it's output type), and I feel it would be nice to solve this generically rather that adding parser tricks. If inference, constant propagation and inlining occurred together (or iterated a few times, or whatever), then we could follow this pattern:
where In practice, you also have to consider the case where In the case like In the case like This approach opens up many new possible API options and optimizations both within EDIT: Sorry, I got completely muddled in the above. Not that it is wrong, but (Apologies - I really shouldn't post when I'm tired!) |
A couple of observations:
|
I don't really see how the I normally 100% agree with the sentiment about "parser tricks" but you're missing my core point: the difference between |
Just a couple of data points: for julia> @btime f(3); @btime g(3);
5.382 ns (0 allocations: 0 bytes)
1.377 ns (0 allocations: 0 bytes)
julia> @btime f(3.0); @btime g(3.0);
1.648 ns (0 allocations: 0 bytes)
1.377 ns (0 allocations: 0 bytes)
julia> x = 3.0+4im; @btime f($x); @btime g($x);
10.057 ns (0 allocations: 0 bytes)
4.469 ns (0 allocations: 0 bytes) Not surprisingly, the biggest benefits of inlining small powers are precisely in the cases where we currently use But I think the non-performance implications for dimensionful code are more important, and I agree with @StefanKarpinski that literal integer exponents are conceptually a somewhat distinct operation. |
You're completely right @StefanKarpinski about the The simple (and correct) version of the point I was making was this: with constants injected into inlined functions, then e.g. I understand what you are saying about (Finally, I'm wondering if at least part of the perception of a conceptual difference for when the base is dimensionful is simply because it breaks type inference? I don't think that is insurmountable - e.g. maybe we could use "lispy" recursion to append extra |
@stevengj they are some nice speedups! |
@andyferris, "pureness" is not the right word here. This is not a violation of function purity. It is classic syntactic sugar: the meaning is determined purely by the "spelling" of the code and not by anything else.
|
Right, that makes sense. The thing I was aiming to achieve here was expanding "literal" to "compile-time constant" (I note you use the two interchangeably in your post, but strictly speaking, literal is a subset of constant). I do support the |
When working on #20484, it occurred to me that it might be nice if
x^literal
were parsed as a call tox^Val{literal}
, at least for small integer literals (say, between-32
and32
). There would still be a fallback^{p}(x, ::Type{Val{p}}) = x^p
that just called the regular^
function, of course.This way:
Dimensionful calculations (e.g. Unitful) could be type-stable even if they involve
x^2
and similar common small exponents.We could make
x^-n
faster for small literaln
(^(-n) is slow #8939).We could implement the optimal addition-chain exponentiation algorithm for small integer exponents, completely unrolled. (For the hardware numeric types IIRC we call
pow
which lowers LLVM'spowi
intrinsic, which probably does something like this. But this does not happen for other types. See also ^ is slow #2741)Types which support certain cases especially efficiently can implement their own methods. e.g. MPFR has an optimized
mpfr_sqr
forx^2
that could be used byBigFloat
.We could make
x^-n
work in a type-stable way for integerx
in the common case of literaln
(Exponentiation by a negative power throws a DomainError #3024).cc @ajkeller34, @timholy
The text was updated successfully, but these errors were encountered: