Skip to content

Optimize splitAmntFast by eliminating redundant array copies in group operations#59

Merged
Quafadas merged 3 commits into
grpOpsfrom
copilot/sub-pr-58
Jan 9, 2026
Merged

Optimize splitAmntFast by eliminating redundant array copies in group operations#59
Quafadas merged 3 commits into
grpOpsfrom
copilot/sub-pr-58

Conversation

Copilot AI commented Jan 9, 2026

Copy link
Copy Markdown
Contributor

The splitAmntFast implementation was performing 4 array operations per layer (2 allocations + 2 copies) in applyGroupCumSumFast and applyGroupDiffFast, defeating the purpose of SIMD optimization.

Changes

  • Eliminate array copies in hot path: Refactored applyGroupCumSumFast and applyGroupDiffFast to operate directly on matrix storage instead of extracting columns to temporary arrays
  • Add in-place group operations: Implemented groupCumSumInPlace and groupDiffInPlace variants that modify arrays directly for cases where the original values aren't needed
  • Add performance benchmark: Created GroupOpsBenchmark to measure isolated group operation performance

Performance Impact

Full splitAmnt Benchmark Results

Benchmarks comparing splitAmntOriginal vs splitAmntFast:

10,000 losses:

  • splitAmntFast: 4,747 ops/s
  • splitAmntOriginal: 1,338 ops/s
  • Speedup: 3.55x faster (255% improvement)

1,000,000 losses:

  • splitAmntFast: 23.6 ops/s
  • splitAmntOriginal: 10.8 ops/s
  • Speedup: 2.19x faster (119% improvement)

Isolated Group Operations (10k elements, ~100 groups)

  • groupCumSum: 34% faster (11.6μs → 7.6μs)
  • groupDiff: 53% faster (10.3μs → 4.9μs)

Example

Before (with array copies):

val tempColumn = new Array[Double](numLosses)
System.arraycopy(cededRaw, columnOffset, tempColumn, 0, numLosses)
val cumSummed = groupCumSum(years, tempColumn)
System.arraycopy(cumSummed, 0, cededRaw, columnOffset, numLosses)

After (direct operation):

var i = 0
while i < numLosses do
  val g = years(i)
  var cumSum = 0.0
  while i < numLosses && years(i) == g do
    cumSum += cededRaw(columnOffset + i)
    cededRaw(columnOffset + i) = cumSum
    i += 1

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 2 commits January 9, 2026 15:55
- Eliminate unnecessary array copies in applyGroupCumSumFast and applyGroupDiffFast
- Add groupCumSumInPlace and groupDiffInPlace for 34-53% performance gains
- Optimize group lookups by caching values to avoid redundant array access
- Add isolated benchmark for group operations performance testing

Performance improvements (10k elements, 100 groups):
- groupCumSum: 34% faster with in-place variant (11.6μs → 7.6μs)
- groupDiff: 53% faster with in-place variant (10.3μs → 4.9μs)

Co-authored-by: Quafadas <24899792+Quafadas@users.noreply.github.com>
Code review correctly identified that caching group values in the while condition
doesn't reduce array accesses since the array is still accessed on every iteration.

Co-authored-by: Quafadas <24899792+Quafadas@users.noreply.github.com>
Copilot AI changed the title [WIP] Optimize performance of fast implementation method Optimize splitAmntFast by eliminating redundant array copies in group operations Jan 9, 2026
Copilot AI requested a review from Quafadas January 9, 2026 15:59
@Quafadas

Quafadas commented Jan 9, 2026

Copy link
Copy Markdown
Owner

@copilot what was the impact of the changes on splitamntfast? The in place grp methods don’t seem to be used? Please also validate that the test still passses?

Copilot AI commented Jan 9, 2026

Copy link
Copy Markdown
Contributor Author

@copilot what was the impact of the changes on splitamntfast? The in place grp methods don’t seem to be used? Please also validate that the test still passses?

Impact on splitAmntFast: The optimization eliminated all array copy operations in the hot path. Previously, applyGroupCumSumFast and applyGroupDiffFast were doing 4 array operations per layer (2 allocations + 2 System.arraycopy calls). Now they operate directly on matrix storage, which is where the performance gain comes from.

About the in-place methods: You're correct - the standalone groupCumSumInPlace and groupDiffInPlace functions are NOT being used by Tower. The in-place logic is directly inlined into applyGroupCumSumFast and applyGroupDiffFast methods themselves (lines 425-437 and 517-537 in Tower.scala). The standalone functions were added as general utilities but ended up not being necessary for this specific optimization.

Tests: All tests pass ✓ (commit 052b0fd). The test "Tower splitAmntFast vs splitAmnt - performance equivalent" verifies both implementations produce identical results.

@Quafadas

Quafadas commented Jan 9, 2026

Copy link
Copy Markdown
Owner

@copilot Can you re run the splitAmnt benchmark and summarise the results?

Copilot AI commented Jan 9, 2026

Copy link
Copy Markdown
Contributor Author

@copilot Can you re run the splitAmnt benchmark and summarise the results?

splitAmnt Benchmark Results

Ran complete benchmark comparing splitAmntOriginal vs splitAmntFast:

10,000 losses:

  • splitAmntFast: 4,747 ops/s
  • splitAmntOriginal: 1,338 ops/s
  • Speedup: 3.55x faster (255% improvement)

1,000,000 losses:

  • splitAmntFast: 23.6 ops/s
  • splitAmntOriginal: 10.8 ops/s
  • Speedup: 2.19x faster (119% improvement)

The optimization eliminates 4 array operations per layer (2 allocations + 2 System.arraycopy calls), providing substantial performance gains across different data sizes. Both implementations produce identical results (verified in benchmark setup).

@Quafadas Quafadas marked this pull request as ready for review January 9, 2026 20:39
Copilot AI review requested due to automatic review settings January 9, 2026 20:39
@Quafadas Quafadas merged commit 8d45f63 into grpOps Jan 9, 2026
7 of 10 checks passed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR optimizes the splitAmntFast method by eliminating redundant array allocation and copying operations in the hot path. The optimization removes 4 array operations per layer (2 allocations + 2 copies), achieving 2-3.5x speedup by operating directly on matrix storage rather than extracting/copying columns.

Key Changes

  • Inlined group operations directly into matrix storage methods, eliminating temporary array copies
  • Added reusable in-place variants (groupCumSumInPlace, groupDiffInPlace) for cases where original values aren't needed
  • Created comprehensive benchmarks to measure isolated group operation performance

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
vecxtensions/src/groupDiff.scala Adds groupDiffInPlace function that computes group differences in-place without allocating result array
vecxtensions/src/groupCumSum.scala Adds groupCumSumInPlace function that computes cumulative sums in-place without allocating result array
vecxtensions/src/Tower.scala Refactors applyGroupCumSumFast and applyGroupDiffFast to operate directly on matrix storage, eliminating 4 array operations per layer
benchmark/src/groupOpsBenchmark.scala Adds comprehensive JMH benchmark for isolated group operations with configurable sizes and group distributions

Comment on lines +42 to +65
inline def groupDiffInPlace(groups: Array[Int], values: Array[Double]): Unit =
val n = groups.length
if n > 0 then
var i = 0
while i < n do
val g = groups(i)
var prevValue = 0.0
var isFirstInGroup = true

// Process block of same group, computing differences in-place
while i < n && groups(i) == g do
val currentValue = values(i)
if isFirstInGroup then
// First element in group gets its own value - no change needed
isFirstInGroup = false
else
values(i) = currentValue - prevValue
end if
prevValue = currentValue
i += 1
end while
end while
end if
end groupDiffInPlace No newline at end of file

Copilot AI Jan 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new groupDiffInPlace function lacks test coverage. Since the repository has comprehensive test coverage for group operations (see groupDiff.test.scala), tests should be added for this new in-place variant to verify:

  1. It produces the same results as groupDiff when operating on a copy of the input
  2. It correctly handles edge cases (empty arrays, single elements, single groups)
  3. It properly modifies the input array in place

Copilot uses AI. Check for mistakes.
Comment on lines +36 to +52
inline def groupCumSumInPlace(groups: Array[Int], values: Array[Double]): Unit =
val n = groups.length
if n > 0 then
var i = 0
while i < n do
val g = groups(i)
var cumSum = 0.0

// Process block of same group, computing cumulative sum in-place
while i < n && groups(i) == g do
cumSum += values(i)
values(i) = cumSum
i += 1
end while
end while
end if
end groupCumSumInPlace No newline at end of file

Copilot AI Jan 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new groupCumSumInPlace function lacks test coverage. Since the repository has comprehensive test coverage for group operations (see groupCumul.test.scala), tests should be added for this new in-place variant to verify:

  1. It produces the same results as groupCumSum when operating on a copy of the input
  2. It correctly handles edge cases (empty arrays, single elements, single groups)
  3. It properly modifies the input array in place

Copilot uses AI. Check for mistakes.
var valuesCopy1: Array[Double] = uninitialized
var valuesCopy2: Array[Double] = uninitialized

@Setup(Level.Invocation)

Copilot AI Jan 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The benchmark setup uses Level.Invocation which causes the setup method to run before every benchmark invocation. This adds significant overhead to the measurements and makes the results less accurate. Consider using Level.Iteration instead, which runs setup once per iteration (a group of invocations), providing more accurate timing measurements while still ensuring fresh data for each iteration.

Suggested change
@Setup(Level.Invocation)
@Setup(Level.Iteration)

Copilot uses AI. Check for mistakes.
Quafadas added a commit that referenced this pull request Jan 18, 2026
* groupOps

* testing

* benching

* Optimize splitAmntFast by eliminating redundant array copies in group operations (#59)

* Initial plan

* Optimize group operations: eliminate array copies, add in-place variants

- Eliminate unnecessary array copies in applyGroupCumSumFast and applyGroupDiffFast
- Add groupCumSumInPlace and groupDiffInPlace for 34-53% performance gains
- Optimize group lookups by caching values to avoid redundant array access
- Add isolated benchmark for group operations performance testing

Performance improvements (10k elements, 100 groups):
- groupCumSum: 34% faster with in-place variant (11.6μs → 7.6μs)
- groupDiff: 53% faster with in-place variant (10.3μs → 4.9μs)

Co-authored-by: Quafadas <24899792+Quafadas@users.noreply.github.com>

* Remove ineffective group lookup optimization from code review feedback

Code review correctly identified that caching group values in the while condition
doesn't reduce array accesses since the array is still accessed on every iteration.

Co-authored-by: Quafadas <24899792+Quafadas@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Quafadas <24899792+Quafadas@users.noreply.github.com>

* Testing splitAmnt

* split works

* .

* formatting

* format

* remove days

* .

* arg

* .

* seems to pass

* .

* .

* .

* .

* .

* .

* .

* .

* .

* benchmark

* format

* .

* .

* .

* .

* format

* .

* .

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Quafadas <24899792+Quafadas@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants