Replaced unsafe ptr logic with chained split_at_mut in DenseMatrix an…#371
Replaced unsafe ptr logic with chained split_at_mut in DenseMatrix an…#371jackpots28 wants to merge 5 commits into
Conversation
…d DenseMatrixMutView
|
thank you! please run |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## development #371 +/- ##
===============================================
- Coverage 45.59% 44.34% -1.26%
===============================================
Files 93 94 +1
Lines 8034 8105 +71
===============================================
- Hits 3663 3594 -69
- Misses 4371 4511 +140 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
I updated the formatting and caught the issue with sort_by that clippy complained about. Test coverage is still slightly lower "src/linalg/basic/matrix.rs: 225/364" | "59.72% coverage, 5238/8771 lines covered", should I add some tests for more coverage? |
adding more tests on the methods affected by changes is always welcome 🐳 thanks. at your convenience. |
…tView - src/linalg/basic/matrix.rs: 251/364 +7.14% tarpaulin check
Rolled through and updated matrix.rs to include 33 new cases |
genefold-ai
left a comment
There was a problem hiding this comment.
The security goal is right and the overall direction is solid — removing unsafe pointer arithmetic is a clear win for long-term maintainability. Two runtime bugs need to be fixed before merging (see inline comments marked 🔴), and there are three medium-priority cleanup items (🟡) that would meaningfully improve performance and reduce duplication. Happy to discuss any of these — great contribution.
| // Collect all mutable references up-front using split_at_mut so | ||
| // that the resulting iterator owns no borrow of "self.values" | ||
|
|
||
| match (column_major, axis) { |
There was a problem hiding this comment.
🔴 [HIGH] Potential panic: col_slice[..nrows] when stride < nrows
In DenseMatrixMutView, when the view has a stride smaller than nrows (e.g. a sub-view into a larger matrix where padding or offset applies), split_at_mut(stride) will produce a col_slice shorter than nrows, causing col_slice[..nrows] to panic at runtime.
The original unsafe implementation handled this correctly via direct offset arithmetic; this safe version does not inherit that safety.
Please add an explicit guard before slicing:
assert!(
col_slice.len() >= nrows,
"iter_mut: stride ({stride}) < nrows ({nrows}): view layout is inconsistent"
);
for elem in col_slice[..nrows].iter_mut() {Or alternatively, use .get_mut(..nrows) and propagate/handle the None case rather than panicking silently later.
| let nrows = self.nrows; | ||
| let ncols = self.ncols; | ||
| let ptr = self.values.as_mut_ptr(); | ||
|
|
There was a problem hiding this comment.
🔴 [HIGH] Subtraction underflow: ncols - 1 panics (debug) or wraps (release) when ncols == 0
The expression if _c == ncols - 1 is evaluated without first checking whether ncols is zero. When ncols == 0:
- In debug mode: panics with a subtraction overflow.
- In release mode: wraps around to
usize::MAX, causing the branch condition to never match and likely producing an incorrect or out-of-bounds slice.
This can be triggered by a zero-column DenseMatrixMutView (e.g. a view into an empty region). The same issue exists symmetrically for nrows - 1 in the row-major cases.
Please add an early guard at the top of the method:
if ncols == 0 || nrows == 0 {
return Box::new(std::iter::empty());
}This also simplifies all subsequent arithmetic by making zero-size cases unreachable.
| let mut indexed: Vec<(usize, &'b mut T)> = by_col | ||
| .into_iter() | ||
| .enumerate() | ||
| .map(|(flat_col_idx, r)| { |
There was a problem hiding this comment.
🟡 [MEDIUM] Triple Vec allocation in Case A is redundant — refs can be eliminated
In Case A (column-major, row-by-row) the code allocates by_col, then indexed, then a final refs Vec that is immediately consumed by into_iter():
let mut refs: Vec<&'b mut T> = Vec::with_capacity(total);
refs.extend(indexed.into_iter().map(|(_, r)| r));
Box::new(refs.into_iter())The final refs is a pure copy of the indexed values with keys stripped. It can be removed entirely:
Box::new(indexed.into_iter().map(|(_, r)| r))This saves one Vec allocation and one full iteration pass per iter_mut call. The same pattern appears in Case D of this method and in both Cases A and D of DenseMatrix::iterator_mut — four sites in total should be cleaned up.
| @@ -143,81 +143,135 @@ impl<'a, T: Debug + Display + Copy + Sized> DenseMatrixMutView<'a, T> { | |||
| } | |||
There was a problem hiding this comment.
🟡 [MEDIUM] Duplicated split_at_mut chunking logic across cases — extract a private helper
Cases B and C in DenseMatrixMutView::iter_mut, and their analogues in DenseMatrix::iterator_mut, share nearly identical code for iterating over a strided slice with split_at_mut:
let mut remaining: &'b mut [T] = self.values;
for _c in 0..ncols {
let col_end = if _c == ncols - 1 { remaining.len() } else { stride };
let (col_slice, tail) = remaining.split_at_mut(col_end);
for elem in col_slice[..nrows].iter_mut() { refs.push(elem); }
remaining = tail;
}This 9-line block is repeated (with minor parameter changes) four times across the two impls. Consider extracting a private helper:
/// Collects mutable references to the first `take` elements of each
/// chunk of size `chunk` from `slice`, advancing through the tail.
fn collect_strided_mut<'a, T>(
slice: &'a mut [T],
chunks: usize,
chunk_size: usize,
take: usize,
) -> Vec<&'a mut T> {
let mut refs = Vec::with_capacity(chunks * take);
let mut remaining = slice;
for i in 0..chunks {
let end = if i == chunks - 1 { remaining.len() } else { chunk_size };
let (head, tail) = remaining.split_at_mut(end);
refs.extend(head[..take].iter_mut());
remaining = tail;
}
refs
}This reduces duplication, makes the zero-size guard easier to add in one place, and makes the bounds check (head.len() >= take) easier to enforce uniformly.
|
|
||
| // Case D: row-major, col-by-col | ||
| (false, _) => { | ||
| let total = nrows * ncols; |
There was a problem hiding this comment.
🟡 [MEDIUM] O(n log n) sort for cross-axis traversal — consider direct index computation
Cases A and D both use this pattern to reorder elements for cross-axis iteration:
let mut indexed: Vec<(usize, &'b mut T)> = by_col
.into_iter()
.enumerate()
.map(|(flat_col_idx, elem)| {
let c = flat_col_idx / nrows;
let r = flat_col_idx % nrows;
(r * ncols + c, elem)
})
.collect();
indexed.sort_unstable_by_key(|(idx, _)| *idx);This performs an O(n log n) sort on n = nrows * ncols elements just to reorder them into row-major (or col-major) access order. For large matrices this is a meaningful overhead compared to the original O(n) pointer arithmetic.
For DenseMatrix (contiguous, non-strided allocation), the same reordering can be done directly using chunks_mut with a transposition index formula, which is O(n):
// Case A: column-major, iterate row-by-row (no sort needed)
// Conceptually: output[r * ncols + c] = col_major_data[c * nrows + r]
// This is just a transpose read-order, achievable with two nested iterators.If a sort is kept for correctness in the strided DenseMatrixMutView case, please add a comment explaining why direct index computation is not viable there.
|
@jackpots28 please see the automated code-review by @genefold-ai and feel free to decide which patches to apply |
|
This one more highlighted by another automated code-review (may overlap the previous ones): 🔴 [HIGH] Bug 1: Subtraction underflow when ncols == 0 or nrows == 0 File: src/linalg/basic/matrix.rs — both DenseMatrixMutView::iter_mut and DenseMatrix::iterator_mut When ncols == 0: This was correctly pointed out by the genefold-ai review. Add an early guard: About the tests: Also I am trying https://docs.rs/cargo-crap/latest/cargo_crap/ to analyse complexity/risk patterns. |
|
Not too bad |
|
this is a problem, can't merge with this as it breaks lazy evaluation: 1. Significant Performance Regression — Eager CollectionFile: src/linalg/basic/matrix.rs Problems: Recommendation: Use flat_map with split_at_mut to maintain lazy evaluation: also code duplication: 3. Code Duplication Across 4 CasesFile: src/linalg/basic/matrix.rs This results in ~200 lines of duplicated logic. |
Yeah, I'm working on the deduplication via some single collector logic to use across the four instances instead. Also got curious and started into some bigger matrices testing, found what you stated with degradation since it does the full collect. I'll work through it! |
…d DenseMatrixMutView
Fixes #368
Checklist
Current behaviour
Changing DenseMatrix and DenseMatrixMutView that uses unsafe ptr arithmetic for generating mutable iterators
New expected behavior
DenseMatrix and DenseMatrixMutView continue to produce mutable iterators, but uses a "head" / "tail" method + split_at_mut function for iterating
Change logs
unsafepointer arithmetic inDenseMatrix/DenseMatrixMutViewmutable iterators with a safe, chainedsplit_at_mutimplementation to ensure memory safety without performance loss.