Skip to content

[performance] perf(harfbuzz): avoid defensive Feature[] copy in Font.Shape - #4690

Draft
github-actions[bot] wants to merge 1 commit into
mainfrom
dev/perf-harfbuzz-shape-features-b7aebc11cffeab7d
Draft

[performance] perf(harfbuzz): avoid defensive Feature[] copy in Font.Shape#4690
github-actions[bot] wants to merge 1 commit into
mainfrom
dev/perf-harfbuzz-shape-features-b7aebc11cffeab7d

Conversation

@github-actions

@github-actions github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Description

HarfBuzzSharp.Font.Shape(Buffer, IReadOnlyList<Feature>, IReadOnlyList<string>) called features?.ToArray() unconditionally, allocating a fresh Feature[] copy on every shape call — even on the common Shape(Buffer, params Feature[]) path where the caller already owns a real array. hb_shape_full only reads the features (const hb_feature_t*), so the copy is pure managed waste.

This change pins the caller's array directly when it already is a Feature[], falling back to ToArray() only for non-array lists:

var featuresArray = features as Feature[] ?? features?.ToArray ();

The pinned pointer and count handed to native are identical either way, so shaping is unchanged.

Changes

  • binding/HarfBuzzSharp/Font.cs — pin caller-owned Feature[] instead of copying.
  • benchmarks/SkiaSharp.Benchmarks/Benchmarks/HarfBuzzShapeFeaturesBenchmark.cs — New-vs-Old proof.
  • tests/Tests/HarfBuzzSharp/HBFontShapeFeaturesTest.cs — equivalence + sensitivity coverage.

Required skia PR

None.

Areas Affected

  • HarfBuzzSharp (text shaping)

Testing

Proof 1 — BenchmarkDotNet (New vs Old, InProcessEmit, net10.0, x64, Linux):

FeatureCount New (fix) Old (ToArray) Allocated New Allocated Old
1 575.8 ns 610.3 ns ~0 B 72 B

~5.7% faster; per-call Feature[] allocation eliminated (72 B → 0 B). No allocation regression. Empirically measured on the run's hardware.

Proof 2 — equivalence test HBFontShapeFeaturesTest (5 tests, all pass): array vs non-array feature lists produce bit-identical GlyphInfos/GlyphPositions across empty / 1 / 3 features and the null-list case; a guard confirms the comparison catches a genuinely different result.

Built binding/HarfBuzzSharp (net10.0) and ran the equivalence tests via tests/SkiaSharp.Tests.Console — all green.

ABI

Body-only change to an existing method — ABI-safe. Renders identically before and after.


Produced by the performance-fixer agentic workflow + performance-fixer skill (managed-C# hot-path scan, text-and-fonts focus area). Measured numbers are empirical (net10.0/x64/Linux); ns figures vary by runner, the 72 B → 0 B allocation delta is the stable primary signal.

Fixes #4689

Generated by Fixer - Performance · opus48 · 693.3 AIC · ⌖ 37 AIC · ⊞ 13K ·

Font.Shape called features?.ToArray() unconditionally, allocating a fresh
Feature[] copy on every shape call even on the common params Feature[] path
where the caller already owns an array. hb_shape_full only reads the features
(const hb_feature_t*), so pinning the caller's array directly is safe and
produces identical shaping.

Benchmark (net10.0, x64, Linux, FeatureCount=1): 610.3 ns/72 B -> 575.8 ns/0 B
(~5.7% faster, per-call allocation eliminated). Equivalence test proves glyph
infos/positions are bit-identical for array vs non-array feature lists.

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 Aug 6, 2026
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: No status

Development

Successfully merging this pull request may close these issues.

[performance] Font.Shape allocates a defensive Feature[] copy on every shape call

0 participants