You could always loop over the low-level rowvals and nonzeros of the sparse matrix.
Ideally, @view on a sparse-CSC matrix would create a sparse vector with views of the data. I tried defining this:
using SparseArrays
using SparseArrays: AbstractSparseMatrixCSC, getcolptr
function Base.view(x::AbstractSparseMatrixCSC, ::Colon, j::Integer)
checkbounds(x, :, j)
r1 = convert(Int, getcolptr(x)[j])
r2 = convert(Int, getcolptr(x)[j+1]) - 1
return @views SparseVector(size(x, 1), rowvals(x)[r1:r2], nonzeros(x)[r1:r2])
end
but it fails because SparseVector requires its arguments to be Vector and doesn’t accept views. Would be nice to generalize this type to allow other AbstractVector types.
You could always loop over the low-level rowvals and nonzeros of the sparse matrix.
That improves the performance:
function foo3(x,A)
for j in axes(A,2)
r1 = SparseArrays.getcolptr(A)[j]
r2 = SparseArrays.getcolptr(A)[j+1]-1
if isequal(rowvals(A)[r1:r2],rowvals(x)) && isequal(nonzeros(A)[r1:r2],nonzeros(x))
return true
end
end
return false
end
Try putting @views on this line (or on a surrounding block) so that you don’t make a copy with slices. Also, isequal is probably not the function you want here:
Just put @views on the whole block of code, and it will change every [...] slice into a view, including expressions like rowvals(A)[r1:r2].
(@views is one of those syntaxes that seems to be under-used in Julia code … it’s a lot cleaner than putting @view or view(...) on every slicing expression.)
Thanks! I fixed it using dropzeros!(x) in my first line, which doesn’t add any allocations.
Unfortunately, that changes x, which works for me, but it is obviously not a feasible replacement for x in eachcol(A). I don’t have any non-structural zeros in A, but for a general solution, that would also have to be taken into account.
The zeros might have to be dropped in A too (as non-structural zeros on either side of equality may mess it up)… I see you have already taken note of this.
Another point is the difficulty of benchmarking this operation, as the test will vary widely in timing depending on the specific instance of x and A (and the correct value of x in eachcol(A)).
An efficient implementation of in should stop as soon as it finds the first match. There is no point in comparing the remaining columns since the result won’t change. For benchmarking, one should probably consider the worst case, i.e., no match and the full search. In my example above, I considered one of the last columns to have a long search, but I confess that the choice was ad hoc.
The correct value of x in eachcol(A) is indeed a design question. One has to decide if x shall be considered a column of A if it differs by non-structural zeros. I would suggest considering that true because it is in line with x == A[:,i] and the dense transformations from my initial post y in eachcol(B).
Here is a little shortcut optimization (assuming no non-structural zeros):
function foo4(x,A)
for j in axes(A,2)
r1 = SparseArrays.getcolptr(A)[j]
r2 = SparseArrays.getcolptr(A)[j+1]-1
r2-r1+1 == nnz(x) || continue
@views if rowvals(A)[r1:r2] == rowvals(x)
if nonzeros(A)[r1:r2] == nonzeros(x)
return true
end
end
end
return false
end
It exploits the necessity of column length to equal vector length (non-zeros-wise).