Add fast sort!/sort for SparseMatrixCSC - #784
Open
ViralBShah wants to merge 2 commits into
Open
Conversation
The generic `Base.sort!(A; dims)` fallback sorts a sparse matrix through `getindex`/`setindex!` on slices, which densifies the structure and is orders of magnitude slower than sorting the equivalent dense matrix. Sorting a column only permutes its stored entries: the values sorting before `zero(eltype(A))` end up at the top of the column, the rest at the bottom, and the structural zeros stay contiguous in between. So each column can be sorted in place by sorting its stored values and rewriting its row indices, exactly as `sort!` already does for sparse vectors (#303). Rows are sorted the same way, on a transposed copy that is transposed back afterwards. `nnz` is now preserved instead of growing, and `sort` returns a `SparseMatrixCSC` for `dims = 1` rather than a `ReshapedArray` wrapping a `SparseVector`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PddHRZV2UXjpTPRkoQoM8j
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #784 +/- ##
==========================================
- Coverage 92.32% 92.09% -0.24%
==========================================
Files 12 12
Lines 8377 8410 +33
==========================================
+ Hits 7734 7745 +11
- Misses 643 665 +22 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
`Base.sort` on a zero-size dense matrix throws `ArgumentError: step cannot be zero`, so the size-parametrised testset compares those cases against the input rather than against a dense reference. Pin down what the sparse methods actually do there instead of leaving it implicit: 0xn, mx0 and 0x0 round-trip unchanged with the right size, element/index types and `nnz == 0`, and structurally empty matrices of non-zero size still match dense and leave the column pointers alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PddHRZV2UXjpTPRkoQoM8j
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #302 (the matrix half; #303 already covered vectors).
The problem
There is no
sort!method forSparseMatrixCSC, sosort!(A; dims)falls back to the genericAbstractArraypath, which reads and writes slices throughgetindex/setindex!. That densifies the structure and is much slower than sorting the equivalent dense matrix — the benchmark from the issue:The fix
Sorting a column only permutes its stored entries. Under any ordering, all the structural zeros compare equal, so they stay contiguous: the stored values that sort before
zero(eltype(A))end up at the top of the column, the rest at the bottom, and the zeros fill the gap. A column can therefore be sorted in place by sorting its stored values and rewriting its row indices — exactly the tricksort!already uses for sparse vectors in #303.dims = 1does that directly.dims = 2sorts the columns of a transposed copy and transposes the result back, so it staysO(nnz + m + n)and reuses the existinghalfperm!machinery.Same benchmark after:
That is 294x / 612x for
dims = 1and 51x / 91x fordims = 2, and sparse is now well ahead of dense rather than behind it.Two behaviour changes worth calling out
nnzis preserved instead of growing. The old path stored the zeros it wrote into the slices, sosort!on a sparse matrix used to (partially) densify it, the same way the vector case did before Optimize and improve dispatch to sort* #303.sort(A; dims = 1)now returns aSparseMatrixCSC. The generic fallback went throughA[:]/reshapeand returned aBase.ReshapedArray{Float64, 2, SparseVector{Float64, Int64}, Tuple{}}.dims = 2already returned aSparseMatrixCSCviapermutedims.searchsortedfirst_discard_keywordsmoved fromsparsevector.jltosparsematrix.jl(which is included first) since both sorting implementations use it; it is unchanged otherwise.Tests
Added a
sort/sort!testset intest/sparsematrix_ops.jlcovering both dims against dense references across sizes (including empty and single row/column), densities, and keyword combinations (rev,by,lt,alg,scratch), plus index-type preservation,nnzpreservation, explicitly stored zeros, and the error paths for a bad or missingdims.Aquareports the same ambiguity/piracy/unbound counts asmain.🤖 Generated with Claude Code
https://claude.ai/code/session_01PddHRZV2UXjpTPRkoQoM8j