Skip to content

[performance] perf(SKRuntimeEffectUniforms): pre-size uniform lookup map to known count - #4512

Merged
mattleibow merged 1 commit into
mainfrom
dev/perf-runtimeeffect-uniforms-dict-sizing-d3d4670490889928
Jul 22, 2026
Merged

[performance] perf(SKRuntimeEffectUniforms): pre-size uniform lookup map to known count#4512
mattleibow merged 1 commit into
mainfrom
dev/perf-runtimeeffect-uniforms-dict-sizing-d3d4670490889928

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

🤖 Generated by the Fixer - Performance agentic workflow using the performance-fixer skill.

What changed

SKRuntimeEffectUniforms's constructor now builds its internal Dictionary<string, Variable> with
an explicit capacity equal to the known uniform count:

- uniforms = new Dictionary<string, Variable> ();
+ uniforms = new Dictionary<string, Variable> (names.Length);

The constructor already knows the final entry count (names.Length) before the fill loop, so the
default-sized (capacity 0) map needlessly rehashes several times as it is populated, allocating
throwaway bucket/entry arrays. This is on a per-uniform-set (per-frame for animated shaders) path.

Invariant that keeps it correct: Dictionary capacity is a pure sizing hint — it changes only
the initial bucket allocation, never lookup/insert semantics or iteration results. The map still
holds exactly names.Length entries.

Proof it is faster

BenchmarkDotNet, [MemoryDiagnoser], net10.0, AMD EPYC 9V74, 2 runs. Old = default-sized map,
New = pre-sized; both fill N entries:

dotnet run -c Release --project benchmarks/SkiaSharp.Benchmarks -- --filter '*SKRuntimeEffectUniformsDictionaryBenchmark*'
N (uniforms) Old Mean New Mean Ratio Old Alloc New Alloc Alloc Ratio
4 119.7 ns 79.5 ns 0.67 704 B 496 B 0.70
16 372.4 ns 242.8 ns 0.65 1640 B 1016 B 0.62
48 1205.8 ns 689.0 ns 0.57 8296 B 3200 B 0.39

Repeatable across both runs; no allocation regression (fewer allocations from removed rehashes).
Benchmark added at benchmarks/SkiaSharp.Benchmarks/Benchmarks/SKRuntimeEffectUniformsDictionaryBenchmark.cs.

Proof it is identical

New equivalence test SKRuntimeEffectTest.Shaders.UniformsMapIsCompleteForManyUniforms builds a
24-uniform effect (crossing several dictionary rehash boundaries) and asserts:

  • every declared uniform is listed, in declaration order, with the right Count;
  • the packed uniform buffer is byte-identical whether values are written in forward or reverse
    order (proving each name maps to the correct offset regardless of insertion path);
  • each packed float round-trips to its declared value.

Confirmed the test catches a deliberately-wrong result: truncating the constructor loop to
i < names.Length - 1 turns it RED; the shipped fix is GREEN. Full *Uniform* suite: 81 passed /
0 failed
against pre-built natives (dotnet cake --target=externals-download) on net10.0.

Scope note

  • Empirically measured, not merely statically reasoned.
  • Managed C# only (binding/SkiaSharp/SKRuntimeEffect.cs); no *.generated.cs, no externals/skia.
  • ABI impact: none — private field, public signatures unchanged.
  • Driver of the win: removed rehash allocations → perf/allocations.

Fixes #4511

Generated by Fixer - Performance · ● 39.1M ·

…ount

Build the internal name->Variable dictionary with an explicit capacity
(names.Length) instead of the default (zero) capacity. The constructor
already knows the exact final entry count up front, so the default-sized
map needlessly rehashes several times while it is filled, allocating
throwaway bucket/entry arrays on a per-uniform-set (potentially per-frame)
path.

Behaviour is identical: dictionary capacity is a pure sizing hint.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot added partner/agentic-workflows Issues and PRs created by SkiaSharp agentic workflows. perf/allocations Excessive managed allocations or per-frame GC/heap churn. Implies tenet/performance. tenet/performance Performance related issues labels Jul 22, 2026
@github-actions github-actions Bot added partner/agentic-workflows Issues and PRs created by SkiaSharp agentic workflows. perf/allocations Excessive managed allocations or per-frame GC/heap churn. Implies tenet/performance. labels Jul 22, 2026
@mattleibow
mattleibow marked this pull request as ready for review July 22, 2026 21:48
@mattleibow
mattleibow merged commit 2b370ba into main Jul 22, 2026
106 checks passed
@mattleibow
mattleibow deleted the dev/perf-runtimeeffect-uniforms-dict-sizing-d3d4670490889928 branch July 22, 2026 21:48
@github-actions

Copy link
Copy Markdown
Contributor Author

📖 Documentation Preview

The documentation for this PR has been deployed and is available at:

🔗 View Staging Site
🔗 View Staging Docs
🔗 View Staging Gallery (Blazor)
🔗 View Staging Gallery (Uno Platform)
🔗 View Staging SkiaFiddle

This preview will be updated automatically when you push new commits to this PR.


This comment is automatically updated by the documentation staging workflow.

@github-actions

Copy link
Copy Markdown
Contributor Author

📊 SkiaSharp benchmarks — PR #4512

this PR (full source build) vs 🌙 nightly · Linux · Windows · macOS

Informational only — this never blocks the PR. 🟢 faster / less allocation · 🔴 slower / more allocation; moves under 5% are hidden as noise.

⏱️ Times are raw BenchmarkDotNet means, and the ⭐ PR and baseline legs run on separate CI runners, so microbenchmarks can swing run-to-run — treat small time deltas as noise. Allocations are deterministic and the reliable signal. The interactive perf-dashboard (linked below) applies smoothing for the trend view.

Highlights

⏱️ Time — 🔴 16 slower · 🟢 42 faster

  • 🟢 LargeImageScaleBenchmark.UpscaleOpaque(Size: 1024) · macOS · 827.21 µs → 507.99 µs (-39%)
  • 🟢 BitmapDrawBenchmark.DrawScaledTiles(Tiles: 256) · macOS · 1.07 ms → 669.01 µs (-37%)
  • 🔴 RasterImageLifecycleBenchmark.CreateDataWithReleaseProc(Count: 256) · Windows · 95.00 µs → 129.66 µs (+36%)
  • 🟢 CanvasDrawBenchmark.Draw(Shapes: 64) · macOS · 2.12 ms → 1.35 ms (-36%)
  • 🟢 LargeImageScaleBenchmark.UpscaleCrossfade(Size: 1024) · macOS · 1.79 ms → 1.24 ms (-31%)
  • …and 53 more (see details below)
Full per-OS benchmark deltas

Linux

⏱️ Time (vs 🌙 nightly 4.151.0-nightly.83)

Benchmark baseline this PR Δ
RasterImageLifecycleBenchmark.CreateDataWithReleaseProc(Count: 256) 100.78 µs 120.98 µs 🔴 +20%
ColorMathBenchmark.ToColor(Colors: 4096) 16.21 µs 14.34 µs 🟢 -12%
MatrixMapPointsBenchmark.MapRect(Points: 256) 11.93 µs 11.12 µs 🟢 -7%
RasterImageLifecycleBenchmark.CreateRasterImage(Count: 256) 257.50 µs 273.19 µs 🔴 +6%
MatrixMapPointsBenchmark.MapPoints(Points: 256) 208.1 ns 220.2 ns 🔴 +6%

Windows

⏱️ Time (vs 🌙 nightly 4.151.0-nightly.83)

Benchmark baseline this PR Δ
RasterImageLifecycleBenchmark.CreateDataWithReleaseProc(Count: 256) 95.00 µs 129.66 µs 🔴 +36%
ColorMathBenchmark.ToColor(Colors: 4096) 13.48 µs 17.06 µs 🔴 +27%
PathBoundsBenchmark.TightBounds(Points: 64) 1.53 µs 1.86 µs 🔴 +21%
PathBoundsBenchmark.TightBounds(Points: 1024) 16.41 µs 19.87 µs 🔴 +21%
RuntimeEffectShaderBenchmark.DrawFrame 1.22 ms 1.45 ms 🔴 +18%
RasterImageLifecycleBenchmark.CreateRasterImage(Count: 256) 253.92 µs 284.61 µs 🔴 +12%
MatrixMapPointsBenchmark.MapPoint(Points: 256) 5.51 µs 4.90 µs 🟢 -11%
MatrixMapPointsBenchmark.MapPoints(Points: 4096) 2.02 µs 2.24 µs 🔴 +11%
CanvasDrawBenchmark.Draw(Shapes: 64) 2.58 ms 2.86 ms 🔴 +11%
CanvasDrawBenchmark.Draw(Shapes: 512) 20.68 ms 22.89 ms 🔴 +11%
MatrixOpsBenchmark.Invert(Count: 4096) 149.85 µs 134.08 µs 🟢 -11%
LargeImageScaleBenchmark.UpscaleOpaque(Size: 2048) 3.31 ms 2.97 ms 🟢 -10%
LargeImageScaleBenchmark.UpscaleCrossfade(Size: 2048) 11.35 ms 10.25 ms 🟢 -10%
ColorMathBenchmark.UnPreMultiply(Colors: 4096) 12.80 µs 11.61 µs 🟢 -9%
ColorMathBenchmark.PreMultiply(Colors: 4096) 14.70 µs 13.37 µs 🟢 -9%
BitmapDrawBenchmark.DrawScaledTiles(Tiles: 256) 1.32 ms 1.42 ms 🔴 +8%
ColorParseBenchmark.Parse(Iterations: 1000) 78.09 µs 83.90 µs 🔴 +7%
LargeImageScaleBenchmark.UpscaleOpaque(Size: 1024) 819.07 µs 760.08 µs 🟢 -7%
MatrixMapPointsBenchmark.MapRadius(Points: 4096) 155.35 µs 165.65 µs 🔴 +7%
LargeImageScaleBenchmark.UpscaleCrossfade(Size: 1024) 2.82 ms 2.63 ms 🟢 -7%
MatrixMapPointsBenchmark.MapPoint(Points: 4096) 85.91 µs 80.45 µs 🟢 -6%
MatrixMapPointsBenchmark.MapRect(Points: 256) 8.12 µs 8.54 µs 🔴 +5%

macOS

⏱️ Time (vs 🌙 nightly 4.151.0-nightly.83)

Benchmark baseline this PR Δ
LargeImageScaleBenchmark.UpscaleOpaque(Size: 1024) 827.21 µs 507.99 µs 🟢 -39%
BitmapDrawBenchmark.DrawScaledTiles(Tiles: 256) 1.07 ms 669.01 µs 🟢 -37%
CanvasDrawBenchmark.Draw(Shapes: 64) 2.12 ms 1.35 ms 🟢 -36%
LargeImageScaleBenchmark.UpscaleCrossfade(Size: 1024) 1.79 ms 1.24 ms 🟢 -31%
ColorMathBenchmark.ToColor(Colors: 4096) 23.03 µs 16.07 µs 🟢 -30%
BitmapDrawBenchmark.DrawUnscaledTiles(Tiles: 64) 221.47 µs 157.78 µs 🟢 -29%
BitmapDrawBenchmark.DrawScaledTiles(Tiles: 64) 288.50 µs 207.07 µs 🟢 -28%
BitmapDrawBenchmark.DrawUnscaledTiles(Tiles: 256) 738.94 µs 541.26 µs 🟢 -27%
LargeImageScaleBenchmark.UpscaleOpaque(Size: 2048) 3.27 ms 2.46 ms 🟢 -25%
SceneRenderBenchmark.RenderFrame(Complexity: 1) 1.52 ms 1.18 ms 🟢 -23%
MatrixOpsBenchmark.Invert(Count: 4096) 79.15 µs 62.09 µs 🟢 -22%
LargeImageScaleBenchmark.UpscaleCrossfade(Size: 2048) 6.97 ms 5.53 ms 🟢 -21%
ColorMathBenchmark.ToColorF(Colors: 4096) 6.16 µs 4.90 µs 🟢 -21%
RuntimeEffectShaderBenchmark.DrawFrame 660.79 µs 529.40 µs 🟢 -20%
CanvasDrawBenchmark.Draw(Shapes: 512) 13.49 ms 10.90 ms 🟢 -19%
SceneRenderBenchmark.RenderFrame(Complexity: 4) 6.76 ms 5.58 ms 🟢 -18%
ColorMathBenchmark.UnPreMultiply(Colors: 4096) 8.91 µs 7.43 µs 🟢 -17%
ColorMathBenchmark.PreMultiply(Colors: 4096) 9.83 µs 8.22 µs 🟢 -16%
RasterImageLifecycleBenchmark.CreateDataWithReleaseProc(Count: 256) 90.56 µs 76.69 µs 🟢 -15%
RasterImageLifecycleBenchmark.CreateRasterImage(Count: 256) 184.84 µs 157.40 µs 🟢 -15%
PathBoundsBenchmark.TightBounds(Points: 1024) 10.21 µs 8.80 µs 🟢 -14%
MatrixOpsBenchmark.Concat(Count: 4096) 141.36 µs 122.08 µs 🟢 -14%
MatrixMapPointsBenchmark.MapRect(Points: 4096) 58.15 µs 51.37 µs 🟢 -12%
MatrixMapPointsBenchmark.MapRadius(Points: 256) 7.39 µs 6.54 µs 🟢 -11%
ColorParseBenchmark.Parse(Iterations: 1000) 74.27 µs 66.03 µs 🟢 -11%
…and 6 more

📈 Full interactive perf-dashboard & run details →

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

partner/agentic-workflows Issues and PRs created by SkiaSharp agentic workflows. perf/allocations Excessive managed allocations or per-frame GC/heap churn. Implies tenet/performance. tenet/performance Performance related issues

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[performance] Pre-size SKRuntimeEffectUniforms uniform lookup map to known count

1 participant