-
Notifications
You must be signed in to change notification settings - Fork 9
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
fix performance #39
fix performance #39
Conversation
Using the benchmark suite from #27, this is now far and away the fastest of all but one of the cases by 5-50x. The odd one out is
|
This fixes iterate, but interestingly it's still one step too complicated for julia> using InvertedIndices, Statistics, BenchmarkTools
julia> thing1(f, x) = map(i->f(view(x, Not(i))), eachindex(x))
thing1 (generic function with 1 method)
julia> thing2(f, x) = (is = eachindex(x); map(i->f(view(x, filter(!isequal(i), is))), is))
thing2 (generic function with 1 method)
julia> x = randn(2000);
julia> @btime thing1(mean, $x);
277.239 ms (14971031 allocations: 411.97 MiB)
julia> @btime thing2(mean, $x);
9.928 ms (4001 allocations: 62.03 MiB) all its time is being spent in collect, because:
|
OK, the remaining trouble with collect is that the iteration state is still marginally unstable — it's small-union-splittable unstable — but that's a union nonetheless. At first order, this works great. But when it's composed with other iterators (like the Generator in collect), that unstable state ends up getting stashed into a tuple where it's no longer splittable — just like the original problem. |
There we go, this now fixes #14, too. It's worth noting that I tried changing iteration to use
|
# This is a little silly, but splitting the tuple here allows inference to normalize | ||
# Tuple{Union{Nothing, Tuple}, Tuple} to Union{Tuple{Nothing, Tuple}, Tuple{Tuple, Tuple}} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Out of curiosity, have you noticed this across all supported versions or just in e.g. 1.11?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type stability unit tests fail without this manual union-split ternary, but pass with it. It's the difference between putting a type-unstable value in a tuple v.s. returning two different tuples... and those fundamentals hold true going back to 1.0:
▶ julia +1.0 -q
julia> function f()
x = rand([nothing, 1.0])
return (x,)
end
f (generic function with 1 method)
julia> @code_warntype f()
Body::Tuple{Union{Nothing, Float64}}
# ...
julia> function g()
x = rand([nothing, 1.0])
return x===nothing ? (nothing,) : (x,)
end
g (generic function with 1 method)
julia> @code_warntype g()
Body::Union{Tuple{Nothing}, Tuple{Float64}}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh interesting, I don't think I knew that. Thanks, I appreciate the explanation!
Want to rebase for a clean CI run now that #40 has been merged? |
Previously, we had been plopping the return values of both
iterate(I.skips)
anditerate(I.picks)
in a single tuple to use as the iteration state. This meant that we had a type instability of the flavor:Julia's inference does not like this. In particular, this isn't a small splittable union. This change refactors iteration with two dramatic simplifications:
pickitr
. Changing this to only worry about the state from the picks gets rid of one union split and simplifies things such that the iteration of picks is lock-step with the iteration itself; we're no longer one ahead.(nothing, pickstate)
or a((skipvalue, skipstate), pickstate)
, which is exactly the magic we need to get Julia to normalize theTuple{Union}
to aUnion{Tuple, Tuple}
.fixes #14, fixes #27