-
-
Notifications
You must be signed in to change notification settings - Fork 5.6k
fix #37312 fast extrema computation on sparse arrays #37429
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
Conversation
fad44f3
to
3cf63de
Compare
Maybe merge the two methods? |
3cf63de
to
ae6f998
Compare
I agree. I merged them in higherorderfns.jl which is made for these kind of functions if i understand it right. |
Actually the performance issue remains with Arrays so this is extrema in Base which is slow. This is out of the scope of this pull request. |
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.
LGTM!
I wonder whether we should do the same for julia> using SparseArrays, BenchmarkTools
julia> x = sprandn(1000, 1, 0.2);
julia> @btime minimum($x);
617.713 ns (5 allocations: 144 bytes)
julia> x = sparse(vec(x));
julia> @btime minimum($x);
505.575 ns (0 allocations: 0 bytes) |
|
||
# (13) extrema methods optimized for sparse vectors/matrices. | ||
function extrema(f, A::SparseVecOrMat) | ||
if iszero(length(A)) |
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.
In the spirit of
julia/stdlib/SparseArrays/src/sparsevector.jl
Lines 1360 to 1367 in dbd3d4c
function maximum(x::AbstractSparseVector{T}) where T<:Real | |
n = length(x) | |
n > 0 || throw(ArgumentError("maximum over empty array is not allowed.")) | |
m = nnz(x) | |
(m == 0 ? zero(T) : | |
m == n ? maximum(nonzeros(x)) : | |
max(zero(T), maximum(nonzeros(x))))::T | |
end |
you may want to introduce a M = length(A)
and reuse it below. As I said, I wonder whether we should move those here and extend them to SparseVecOrMat
. Hopefully, this doesn't introduce method ambiguities, but can be done in a separate PR.
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.
Done.
Concerning minimum
and maximum
, they definitely need to be moved and applied on SparseVecOrMat
. There are no optimizations for sparse matrices on either of these functions for now.
@@ -1154,4 +1155,23 @@ map(f::Tf, A::SparseOrStructuredMatrix, Bs::Vararg{SparseOrStructuredMatrix,N}) | |||
map!(f::Tf, C::AbstractSparseMatrixCSC, A::SparseOrStructuredMatrix, Bs::Vararg{SparseOrStructuredMatrix,N}) where {Tf,N} = | |||
(_checksameshape(C, A, Bs...); _noshapecheck_map!(f, C, _sparsifystructured(A), map(_sparsifystructured, Bs)...)) | |||
|
|||
|
|||
# (13) extrema methods optimized for sparse vectors/matrices. | |||
function extrema(f, A::SparseVecOrMat) |
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.
This doesn't handle dims
or dims=:
, so I believe those will still call the slower generic code. We might want to at least handle dims=:
, and use invoke
to call the AbstractArray method for general dims.
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.
I think it's worse because dims
is a keyword argument, so this will error now if you pass a dims
argument. This will probably need to be renamed to a hidden function _extrema
and extrema
needs to check for a dims
argument and fall back to the slow code if dims
is not :
. An alternative of course would be to handle the dims
argument here directly.
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.
It doesn't throw errors. It only use the slow generic code.
There are only thew cases.
extrema(f, A::SparseVector, dims=1)
, extrema(f, A::AbstractSparseMatrix, dims=[1,2])
and extrema(f, SparseVecOrMat, ::Colon)
which are equivalent to extrema(f, A::SparseVecOrMat)
.
We still need to implement extrema(f, A::SparseVecOrMat, dims=1)
and extrema(f, A::SparseVecOrMat, dims=2)
.
In my opinion there should be two separate functions because the optimized algorithms are quite different.
I believe it covers every cases.
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 problem is that dims
is not a positional argument and keyword args don't participate in dispatch. The correct function signature should probably be Base._extrema_dims(f, A::SparseVecOrMat, ::Colon)
, so this only gets called for the dims=:
case (which is also the default).
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.
I implemented the @JeffBezanson's idea of using invoke
. Actually reimplementing the functions that handles dims would greatly improve speed but it is a bit tricky to get them consistent with the Base implementation.
Codecov Report
@@ Coverage Diff @@
## master #37429 +/- ##
==========================================
+ Coverage 87.05% 87.55% +0.49%
==========================================
Files 357 351 -6
Lines 72041 71010 -1031
==========================================
- Hits 62718 62174 -544
+ Misses 9323 8836 -487
Continue to review full report at Codecov.
|
vmin, vmax | ||
end | ||
|
||
extrema(A::SparseVecOrMat) = extrema(identity, A) |
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.
I believe this fallback is already defined in Base.
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 more specific definition is
extrema(A::AbstractArray; dims = :) = _extrema_dims(identity, A, dims)
in base/multidimensional.jl line 1599.
So it seems this is needed.
I benchmarked extrema
with:
julia> S = sprand(10000, 10000, 0.00001);
julia> @btime extrema(S)
3.328 μs (1 allocation: 32 bytes)
(0.0, 0.9993973695872556)
and without this line:
julia> @btime extrema(S)
643.939 ms (1 allocation: 32 bytes)
(0.0, 0.9993973695872556)
ae6f998
to
fe000fd
Compare
Why do you define separate methods for |
n = 10 | ||
A = sprand(n, n, 0.2) | ||
B = Array(A) | ||
x = sprand(n, 0.2) | ||
y = Array(x) | ||
f(x) = x^3 | ||
@test extrema(A) == extrema(B) | ||
@test extrema(x) == extrema(y) | ||
@test extrema(f, A) == extrema(f, B) | ||
@test extrema(f, x) == extrema(f, y) | ||
@test extrema(spzeros(n, n)) == (0.0, 0.0) | ||
@test extrema(spzeros(n)) == (0.0, 0.0) | ||
@test_throws ArgumentError extrema(spzeros(0, 0)) | ||
@test_throws ArgumentError extrema(spzeros(0)) | ||
@test extrema(sparse(ones(n, n))) == (1.0, 1.0) | ||
@test extrema(sparse(ones(n))) == (1.0, 1.0) |
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.
Can you add tests with dims=:
and dims=2
to test that the fallback still works correctly?
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.
I added some tests. It should cover most cases.
I think I agree with @simeonschaub. If the generic
I wonder if it's acceptable for sparse matrices(!) and |
fe000fd
to
63bd1fa
Compare
It is indeed quite cleaner to extend '_extrema_dims'. In the same idea, I rename 'extrema' so it extends '_extrema_itr'. The new tests are passing and it works well in manual experimentations. However some other tests are failing. It seems to be an ambiguity with '_extrema_dims'. I haven't figure out why yet. |
invoke(_extrema_dims, Tuple{Any, AbstractArray, Any}, f, A, dims) | ||
end | ||
|
||
_extrema_dims(f, A::SparseVecOrMat, ::Colon) = _extrema_itr(f, A) |
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.
I'm not sure, but maybe splitting this into two helps with the ambiguity?
_extrema_dims(f, A::SparseVecOrMat, ::Colon) = _extrema_itr(f, A) | |
_extrema_dims(f, A::AbstractSparseVector, ::Colon) = _extrema_itr(f, A) | |
_extrema_dims(f, A::AbstractSparseMatrix, ::Colon) = _extrema_itr(f, A) |
I believe you could generalize the method on line 1174 towards AbstractSparseVector
... well, or restrict the matrix methods to SparseMatrixCSC
. I'm not sure what the API for the abstract ones is.
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.
Unfortunately it doesn't work.
The method on line 1174 can't be generalized because nnz is not defined for AbstractSparseVector
. Almost every function in Sparsearrays
is defined for SparseVector
anf AbstractSparseMatric
, actually.
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.
No problem, that's fine. But does splitting the methods into SparseVector
and AbstractSparseMatrix
help? I think there might be an ambiguity because the method here is more abstract in the second argument but more concrete in the third (but I forgot compared to which other method).
63bd1fa
to
d2a6822
Compare
This is certainly "ready for review", if not for merging. 😉 |
It seems that the freebsd test is failing in test/stress.jl. I can't see how it is related to this pull request. |
Thank you @Clement-F for this great work. |
Fix #37312
Exploit sparsity to speed up extrema computation. It iterates over non-zeros values and then account for the zero value if necessary.
It is consistently faster than the generic algorithm.