SparseArrays.jl icon indicating copy to clipboard operation
SparseArrays.jl copied to clipboard

reverse(A,dims=:) creates new structural nonzeros

Open henselman-petrusek opened this issue 3 years ago • 16 comments

Calling reverse on a sparse array introduces structural nonzeros. Here's a minimal example

julia> a = SparseMatrixCSC(2, 2, [1,3,4], [1,2,1], [1,1,1])
2×2 SparseMatrixCSC{Int64, Int64} with 3 stored entries:
 1  1
 1  ⋅

julia> reverse(a,dims=:)
2×2 SparseMatrixCSC{Int64, Int64} with 4 stored entries:
 0  1
 1  1

julia> reverse(a,dims=1)
2×2 SparseMatrixCSC{Int64, Int64} with 4 stored entries:
 1  0
 1  1

julia> reverse(a,dims=2)
2×2 SparseMatrixCSC{Int64, Int64} with 4 stored entries:
 1  1
 0  1

henselman-petrusek avatar Oct 21 '22 00:10 henselman-petrusek

i guess we could overload _reverse! but is there a nice inplace way of doing it for dim=2?

SobhanMP avatar Oct 21 '22 23:10 SobhanMP

@Wimmerer for reverse(dim=2) can we do better than transpose!, reverse!(dim=1), transpose! ?

SobhanMP avatar Oct 21 '22 23:10 SobhanMP

I wasn't even aware of a reverse function in Julia for non Vectors. I'll have to think about it, I doubt there's anything better that's not more involved than it's worth

rayegun avatar Oct 21 '22 23:10 rayegun

Here's an alternative that is simple and more performant:

using SparseArrays


function reverse_sparse( A:: SparseMatrixCSC{Tv, Ti} ) where { Tv, Ti }

    m, n        =   size(A)
    K           =   length(A.colptr) # = n+1    
    rowval      =   m + 1 .- reverse( A.rowval )
    nzval       =   reverse( A.nzval )

    colptr      =   Array{Ti}(undef, K )
    colptr[1]   =   1

    for p in 2:K
        colptr[p]   =   colptr[p-1] + ( A.colptr[K-p+2] - A.colptr[K-p+1] )
    end

    return SparseMatrixCSC( m, n, colptr, rowval, nzval )
end


function benchmark()

    println()
    println("Large matrix")
    println("--------------")
    println()
    A           =   sprand(1000,1000,0.1)

    println("Current implementation:")
    @time Arev1  =   dropzeros(reverse(A))
    println()

    println("Custom implementation:")    
    @time Arev2  =   reverse_sparse(A)
    println()    

    println("Result is correct?")
    println(Arev1 == Arev2)
    println()    

    println("Small matrix")
    println("--------------")
    println()
    A           =   sparse([0 0 1; 1 0 0])

    println("Current implementation:")
    @time Arev1  =   dropzeros(reverse(A))
    println()

    println("Custom implementation:")    
    @time Arev2  =   reverse_sparse(A)
    println()    

    println("Result is correct?")
    println(Arev1 == Arev2)

end

And the result

Large matrix
--------------

Current implementation:
  1.513817 seconds (14 allocations: 7.960 MiB)

Custom implementation:
  0.000461 seconds (7 allocations: 2.291 MiB)

Result is correct?
true

Small matrix
--------------

Current implementation:
  0.000003 seconds (9 allocations: 656 bytes)

Custom implementation:
  0.000002 seconds (7 allocations: 480 bytes)

Result is correct?
true

The reverse_sparse function can easily be modified to reverse just rows or just columns.

henselman-petrusek avatar Oct 25 '22 00:10 henselman-petrusek

function reverse_sparse2(A::AbstractSparseMatrixCSC)
    rowval = reverse(A.rowval)
    rowval .= size(A, 1) + 1 .- rowval

    nzval = reverse(A.nzval)

    colptr = similar(getcolptr(A))
    colptr[firstindex(colptr)] = 1

    @inline for p in eachindex(colptr)[2:end]
        colptr[p] = colptr[p-1] + (getcolptr(A)[end-p+2] - A.colptr[end-p+1])
    end

    return @if_move_fixed A SparseMatrixCSC(size(A)..., colptr, rowval, nzval)
end

is a bit faster, allocates less and more up to code but you should open a pull request.

SobhanMP avatar Oct 25 '22 14:10 SobhanMP

Do note that we also need reverse! which would be better to implement. i suggest these but honestly i think given how much time that they take (10x time your code), i'm doing something wrong.

function reverse!_col(x)
    @inline for i in 1:size(x, 2)
        s = getcolptr(x)[i]
        t = getcolptr(x)[i + 1] - 1
        reverse!(view(nonzeros(x), s:t))
        v = view(rowvals(x), s:t)
        v .= (size(x, 1) + 1) .- v
        reverse!(v)
    end
    return x
end
function reverse!_row(x)
    y = transpose!(similar(x), x)
    reverse!_col(y)
    transpose!(x, y)
    return x
end

SobhanMP avatar Oct 25 '22 14:10 SobhanMP

The reverse col is slow? That's surprising, that's what I was going to propose and I don't see a better way immediately. Rows is harder.

In place*

rayegun avatar Oct 25 '22 16:10 rayegun

reverse_sparse2 takes like 10 micro second, _col takes 100 micro seconds

SobhanMP avatar Oct 25 '22 16:10 SobhanMP

The reverse col is slow? That's surprising, that's what I was going to propose and I don't see a better way immediately. Rows is harder.

Can we do temporaries in the row! one? That might be faster than the double transpose. Allocate similar(x) then just copyto! the vectors in reverse order.

E: not sure why this reposted

rayegun avatar Oct 25 '22 16:10 rayegun

a = sprandn(10_000, 10_000, 0.00001)
julia> @btime reverse!_col(copy($a));
  123.924 μs (4 allocations: 94.36 KiB)

julia> @btime reverse!_row(copy($a));
  154.990 μs (8 allocations: 188.72 KiB)
julia> @btime reverse_sparse($a);
  11.662 μs (5 allocations: 102.42 KiB)

julia> @btime reverse_sparse2($a);
  11.191 μs (4 allocations: 94.36 KiB)

julia> @btime reverse_sparse!(copy($a));
  269.779 μs (8 allocations: 188.72 KiB)

SobhanMP avatar Oct 25 '22 16:10 SobhanMP

Can we do temporaries in the row! one? That might be faster than the double transpose. Allocate similar(x) then just copyto! the vectors in reverse order.

not sure i understand.

SobhanMP avatar Oct 25 '22 16:10 SobhanMP

I'm a little unfamiliar with macros, and I'm getting an error for code that includes @if_move_fixed. Is this special to SparseArrays?

henselman-petrusek avatar Dec 08 '22 01:12 henselman-petrusek

Yes, it's specific to this package. It converts the last argument of the macro to a matrix with fixed pattern if any of the other arguments have fixed pattern

On Wed, Dec 7, 2022, 20:29 Gregory Henselman-Petrusek < @.***> wrote:

I'm a little unfamiliar with macros, and I'm getting an error for code that includes @if_move_fixed. Is this special to SparseArrays?

— Reply to this email directly, view it on GitHub https://github.com/JuliaSparse/SparseArrays.jl/issues/278#issuecomment-1341845352, or unsubscribe https://github.com/notifications/unsubscribe-auth/AARRZZZBICY3SFMDDLQ6LG3WME2YXANCNFSM6AAAAAARKVADYE . You are receiving this because you commented.Message ID: @.***>

SobhanMP avatar Dec 09 '22 00:12 SobhanMP

Also, are you using julia 1.9? because it's julia 1.9 thing.

SobhanMP avatar Dec 09 '22 14:12 SobhanMP

Oh that must be it -- I was running 1.8. I'm new to many aspects of development -- is there a way to PR two different versions of the code, one that'd work with the stable 1.6 release and one that works with 1.9?

henselman-petrusek avatar Dec 11 '22 21:12 henselman-petrusek

you have to make pull request to merge into the backport-1.6 branch and one for the main branch.

SobhanMP avatar Dec 12 '22 00:12 SobhanMP