Skip to content

Avoid LINQ and boxed-enumerator allocations in parameter binding#337

Merged
Giorgi merged 1 commit into
Giorgi:developfrom
skuirrels:perf/parameter-binding-allocations
Jul 11, 2026
Merged

Avoid LINQ and boxed-enumerator allocations in parameter binding#337
Giorgi merged 1 commit into
Giorgi:developfrom
skuirrels:perf/parameter-binding-allocations

Conversation

@skuirrels

Copy link
Copy Markdown
Contributor

What's Changed

PreparedStatement.BindParameters runs on every command execution. It did
two things that allocate each time:

  1. parameterCollection.OfType<DuckDBParameter>().Any(p => …) — allocates a
    LINQ iterator.
  2. foreach (DuckDBParameter param in parameterCollection) over the non-generic
    DbParameterCollection — boxes the List<T> struct enumerator.

Both are replaced with index-based loops over the existing typed indexer, so the
detection of named parameters and the bind loop no longer allocate.

Benchmark

5,000 executions of a 3-parameter scalar query (SELECT $a + $b + $c), reusing
one command. BenchmarkDotNet, .NET 10, Apple M4 Pro:

Metric Before After
Allocated 6.52 MB 5.91 MB

That is ~128 bytes and 2–3 allocations removed per execution. It won't move
the wall clock — native prepare/execute dominates each call — but it cuts steady
GC pressure in tight parameterized-query loops (the rest of the 5.91 MB is
unavoidable per-query allocation from result handling).

Tests

Full existing suite passes locally (6,895 tests); the parameter and command
suites were run specifically against this change.


Companion to #336. The reproducible benchmark lives in the DuckDB.NET.Benchmarks
project introduced there — I kept this PR to the single source change to avoid
overlapping that project. Happy to add a ParameterBindingBenchmark to it as a
follow-up once #336 lands, if useful.

PreparedStatement.BindParameters ran a LINQ OfType<>().Any(...) and iterated
the parameters with foreach over the non-generic collection (which boxed the
List<T> struct enumerator) on every command execution. Replace both with
index-based loops over the typed indexer.

This removes ~128 bytes and 2-3 allocations per command execution, which
matters in tight parameterized-query loops. Execution time is unchanged, as the
native prepare/execute dominates.
@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.58%. Comparing base (6f5a68d) to head (1024599).

Additional details and impacted files
@@             Coverage Diff             @@
##           develop     #337      +/-   ##
===========================================
- Coverage    87.58%   87.58%   -0.01%     
===========================================
  Files           77       77              
  Lines         3134     3141       +7     
  Branches       463      465       +2     
===========================================
+ Hits          2745     2751       +6     
- Misses         275      276       +1     
  Partials       114      114              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@skuirrels

Copy link
Copy Markdown
Contributor Author

Just flagging that the macOS red X isn't from this change — it's the Coveralls upload step, which dies trying to brew install its reporter ("Refusing to load formula ... from untrusted tap"). Homebrew changed how it trusts taps and the coveralls action hasn't caught up, so it's failing on macOS for any PR right now, not just this one. The build and tests themselves pass fine on macOS, and Ubuntu/Windows are both green.

Happy to send a small CI fix for it separately if that's helpful.

@Giorgi Giorgi merged commit fe2abc5 into Giorgi:develop Jul 11, 2026
10 of 13 checks passed
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.

2 participants