Reuse appender row instance to eliminate per-row allocation#336
Open
skuirrels wants to merge 1 commit into
Open
Reuse appender row instance to eliminate per-row allocation#336skuirrels wants to merge 1 commit into
skuirrels wants to merge 1 commit into
Conversation
DuckDBAppender.CreateRow() allocated a new DuckDBAppenderRow for every appended row. Since a row is always filled and EndRow()-ed before the next CreateRow() call, and the table name, vector writers, data chunk and native appender are stable for the appender's lifetime, a single row instance can be cached and re-targeted at the next row index via a new Reset() method. Benchmark (1,000,000 rows x 4 columns, in-memory table, .NET 10, M4 Pro): Before: 35.37 ms, 61.13 MB allocated, 7000 Gen0/1000 ops After: 25.96 ms, 0.09 MB allocated, 0 Gen0/1000 ops ~99.85% fewer managed allocations and ~27% faster from the removed GC pressure. Adds a DuckDB.NET.Benchmarks project (BenchmarkDotNet, in-process toolchain) covering the appender and mapped-appender paths.
This was referenced Jul 10, 2026
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #336 +/- ##
===========================================
+ Coverage 87.58% 87.61% +0.02%
===========================================
Files 77 77
Lines 3134 3140 +6
Branches 463 464 +1
===========================================
+ Hits 2745 2751 +6
Misses 275 275
Partials 114 114 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Contributor
Author
|
Just flagging that the macOS red X isn't from this change — it's the Coveralls upload step, which dies trying to Happy to send a small CI fix for it separately if that's helpful. |
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.
What's Changed
DuckDBAppender.CreateRow()allocated a newDuckDBAppenderRowfor everyappended row. In bulk-insert workloads that is one short-lived heap object per
row, adding sustained Gen0 GC pressure to the hottest loop in the library.
A row is always filled and
EndRow()-ed before the nextCreateRow()call, andthe table name, vector writers, data chunk and native appender are stable for
the lifetime of the appender. So a single
DuckDBAppenderRowcan be cached andre-targeted at the next row via a new internal
Reset(rowIndex)method insteadof being reallocated.
Benchmark
Appending 1,000,000 rows × 4 columns (
INTEGER, BIGINT, DOUBLE, BOOLEAN) into anin-memory table. BenchmarkDotNet, .NET 10, Apple M4 Pro:
The 61 MB was almost entirely the per-row row object. Removing it also removes
the GC pressure (7000 → 0 Gen0 collections), which is what drives the wall-clock
speedup on top of the allocation win.
Behavior note
Because the row instance is now reused, a caller that holds onto a row reference
across a
CreateRow()call will see it mutate. This matches the documentedcreate → append →
EndRow()usage pattern (only one row is active at a time),but it is a subtle change worth calling out.
Benchmarks project
Adds a small
DuckDB.NET.Benchmarksproject (BenchmarkDotNet) with the appenderbenchmark above, so the improvement is reproducible and future regressions are
catchable. A couple of things I'd like your steer on:
projects (multi-targeted + signed). Happy to align it if you'd prefer.
Directory.Build.propsrenames the output assembly (DuckDB.NET.Benchmarks)while the project file stays
Benchmarks.csproj, which the default toolchaincan't locate.
If you'd rather this PR not carry a new project at all, I can drop it and keep
just the
DuckDB.NET.Datachange.Tests
The full existing suite (6,895 tests) still passes locally; the 66 appender
tests were run specifically against this change.