Skip to content

Performance improvements and more benchmarks#347

Merged
twcclegg merged 7 commits into
mainfrom
performance
Jun 3, 2026
Merged

Performance improvements and more benchmarks#347
twcclegg merged 7 commits into
mainfrom
performance

Conversation

@twcclegg

Copy link
Copy Markdown
Owner

Changes

@codecov

codecov Bot commented May 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.66667% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.94%. Comparing base (c5fb218) to head (f4f483f).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
csharp/PhoneNumbers/PhoneNumberMatcher.cs 91.30% 1 Missing and 1 partial ⚠️
csharp/PhoneNumbers/PrefixFileReader.cs 50.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #347      +/-   ##
==========================================
- Coverage   97.94%   97.94%   -0.01%     
==========================================
  Files          40       40              
  Lines       52924    52949      +25     
  Branches     1115     1124       +9     
==========================================
+ Hits        51839    51862      +23     
- Misses        851      852       +1     
- Partials      234      235       +1     

☔ View full report in Codecov by Sentry.
📢 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.

@github-actions

github-actions Bot commented May 28, 2026

Copy link
Copy Markdown

📊 Benchmark Results

Commit: f4f483f · Full run · Windows windows-latest

PR branch

BenchmarkDotNet v0.15.8, Windows 11 (10.0.26100.32860/24H2/2024Update/HudsonValley) (Hyper-V)
AMD EPYC 7763 2.44GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.300
  [Host]             : .NET 9.0.16 (9.0.16, 9.0.1626.22923), X64 RyuJIT x86-64-v3
  .NET 8.0           : .NET 8.0.27 (8.0.27, 8.0.2726.22922), X64 RyuJIT x86-64-v3
  .NET 9.0           : .NET 9.0.16 (9.0.16, 9.0.1626.22923), X64 RyuJIT x86-64-v3
  .NET Framework 4.8 : .NET Framework 4.8.1 (4.8.9325.0), X64 RyuJIT VectorSize=256


Method Job Runtime PhoneNumberCount Mean Error StdDev Gen0 Gen1 Allocated
InputDigitPerKeystroke .NET 8.0 .NET 8.0 1000 4.717 ms 0.0480 ms 0.0449 ms 242.1875 7.8125 3.9 MB
InputDigitPerKeystroke .NET 9.0 .NET 9.0 1000 4.627 ms 0.0311 ms 0.0291 ms 242.1875 7.8125 3.9 MB
InputDigitPerKeystroke .NET Framework 4.8 .NET Framework 4.8 1000 10.238 ms 0.1296 ms 0.1149 ms 843.7500 62.5000 5.07 MB
InputDigitPerKeystroke .NET 8.0 .NET 8.0 10000 47.583 ms 0.6588 ms 0.6162 ms 2363.6364 90.9091 38.96 MB
InputDigitPerKeystroke .NET 9.0 .NET 9.0 10000 45.219 ms 0.3867 ms 0.3428 ms 2363.6364 90.9091 38.96 MB
InputDigitPerKeystroke .NET Framework 4.8 .NET Framework 4.8 10000 106.976 ms 2.0874 ms 1.8505 ms 8400.0000 800.0000 50.68 MB

BenchmarkDotNet v0.15.8, Windows 11 (10.0.26100.32860/24H2/2024Update/HudsonValley) (Hyper-V)
AMD EPYC 7763 2.44GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.300
  [Host]     : .NET 9.0.16 (9.0.16, 9.0.1626.22923), X64 RyuJIT x86-64-v3
  Job-XMNSVG : .NET 8.0.27 (8.0.27, 8.0.2726.22922), X64 RyuJIT x86-64-v3
  Job-VISNLR : .NET 9.0.16 (9.0.16, 9.0.1626.22923), X64 RyuJIT x86-64-v3
  Job-UAVPVK : .NET Framework 4.8.1 (4.8.9325.0), X64 RyuJIT VectorSize=256

InvocationCount=1  IterationCount=20  LaunchCount=1  
RunStrategy=ColdStart  UnrollFactor=1  WarmupCount=1  

Method Runtime Mean Error StdDev Allocated
CreateInstance .NET 8.0 316.0 μs 86.67 μs 99.81 μs 79.55 KB
CreateInstanceAndLoadAllRegions .NET 8.0 8,547.8 μs 335.40 μs 386.25 μs 1685.46 KB
FirstRegionLookup .NET 8.0 367.2 μs 107.37 μs 123.64 μs 85.15 KB
CreateInstance .NET 9.0 312.1 μs 83.85 μs 96.56 μs 75.04 KB
CreateInstanceAndLoadAllRegions .NET 9.0 8,623.7 μs 626.29 μs 721.23 μs 1576.76 KB
FirstRegionLookup .NET 9.0 359.7 μs 103.19 μs 118.83 μs 80.09 KB
CreateInstance .NET Framework 4.8 256.0 μs 106.91 μs 123.12 μs 176.42 KB
CreateInstanceAndLoadAllRegions .NET Framework 4.8 8,643.2 μs 538.62 μs 620.28 μs 4355.91 KB
FirstRegionLookup .NET Framework 4.8 299.9 μs 134.43 μs 154.81 μs 192.47 KB

BenchmarkDotNet v0.15.8, Windows 11 (10.0.26100.32860/24H2/2024Update/HudsonValley) (Hyper-V)
AMD EPYC 7763 2.44GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.300
  [Host]             : .NET 9.0.16 (9.0.16, 9.0.1626.22923), X64 RyuJIT x86-64-v3
  .NET 8.0           : .NET 8.0.27 (8.0.27, 8.0.2726.22922), X64 RyuJIT x86-64-v3
  .NET 9.0           : .NET 9.0.16 (9.0.16, 9.0.1626.22923), X64 RyuJIT x86-64-v3
  .NET Framework 4.8 : .NET Framework 4.8.1 (4.8.9325.0), X64 RyuJIT VectorSize=256


Method Job Runtime PhoneNumberCount Mean Error StdDev Gen0 Allocated
ExtractPossibleNumber_CleanInput .NET 8.0 .NET 8.0 1000 24.47 μs 0.206 μs 0.193 μs - -
ExtractPossibleNumber_WithLeadingJunk .NET 8.0 .NET 8.0 1000 37.97 μs 0.305 μs 0.270 μs 2.8687 48360 B
ExtractPossibleNumber_CleanInput .NET 9.0 .NET 9.0 1000 24.66 μs 0.051 μs 0.043 μs - -
ExtractPossibleNumber_WithLeadingJunk .NET 9.0 .NET 9.0 1000 36.28 μs 0.140 μs 0.124 μs 2.8687 48360 B
ExtractPossibleNumber_CleanInput .NET Framework 4.8 .NET Framework 4.8 1000 52.50 μs 0.112 μs 0.105 μs - -
ExtractPossibleNumber_WithLeadingJunk .NET Framework 4.8 .NET Framework 4.8 1000 65.04 μs 0.378 μs 0.335 μs 8.5449 54488 B
ExtractPossibleNumber_CleanInput .NET 8.0 .NET 8.0 10000 246.84 μs 0.788 μs 0.698 μs - -
ExtractPossibleNumber_WithLeadingJunk .NET 8.0 .NET 8.0 10000 378.08 μs 2.508 μs 2.346 μs 28.8086 482984 B
ExtractPossibleNumber_CleanInput .NET 9.0 .NET 9.0 10000 250.77 μs 1.042 μs 0.924 μs - -
ExtractPossibleNumber_WithLeadingJunk .NET 9.0 .NET 9.0 10000 355.63 μs 0.990 μs 0.926 μs 28.8086 482984 B
ExtractPossibleNumber_CleanInput .NET Framework 4.8 .NET Framework 4.8 10000 524.53 μs 0.463 μs 0.411 μs - -
ExtractPossibleNumber_WithLeadingJunk .NET Framework 4.8 .NET Framework 4.8 10000 650.55 μs 0.759 μs 0.634 μs 85.9375 544887 B

BenchmarkDotNet v0.15.8, Windows 11 (10.0.26100.32860/24H2/2024Update/HudsonValley) (Hyper-V)
AMD EPYC 7763 2.44GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.300
  [Host]             : .NET 9.0.16 (9.0.16, 9.0.1626.22923), X64 RyuJIT x86-64-v3
  .NET 8.0           : .NET 8.0.27 (8.0.27, 8.0.2726.22922), X64 RyuJIT x86-64-v3
  .NET 9.0           : .NET 9.0.16 (9.0.16, 9.0.1626.22923), X64 RyuJIT x86-64-v3
  .NET Framework 4.8 : .NET Framework 4.8.1 (4.8.9325.0), X64 RyuJIT VectorSize=256


Method Job Runtime PhoneNumberCount Mean Error StdDev Gen0 Gen1 Allocated
FindNumbers_Valid .NET 8.0 .NET 8.0 100 176.3 μs 0.56 μs 0.52 μs 4.1504 - 69.95 KB
FindNumbers_StrictGrouping .NET 8.0 .NET 8.0 100 362.6 μs 0.52 μs 0.46 μs 7.3242 - 123.32 KB
FindNumbers_Valid .NET 9.0 .NET 9.0 100 164.2 μs 1.31 μs 1.16 μs 4.1504 - 69.95 KB
FindNumbers_StrictGrouping .NET 9.0 .NET 9.0 100 337.7 μs 1.14 μs 0.96 μs 7.3242 - 123.32 KB
FindNumbers_Valid .NET Framework 4.8 .NET Framework 4.8 100 1,278.8 μs 5.97 μs 5.29 μs 11.7188 - 76.94 KB
FindNumbers_StrictGrouping .NET Framework 4.8 .NET Framework 4.8 100 1,619.6 μs 10.50 μs 9.31 μs 39.0625 - 248.66 KB
FindNumbers_Valid .NET 8.0 .NET 8.0 1000 1,990.2 μs 8.59 μs 7.18 μs 39.0625 - 681.67 KB
FindNumbers_StrictGrouping .NET 8.0 .NET 8.0 1000 4,212.8 μs 32.14 μs 28.49 μs 70.3125 - 1252.85 KB
FindNumbers_Valid .NET 9.0 .NET 9.0 1000 1,898.0 μs 8.45 μs 7.91 μs 41.0156 - 681.67 KB
FindNumbers_StrictGrouping .NET 9.0 .NET 9.0 1000 3,996.0 μs 11.93 μs 10.57 μs 70.3125 - 1252.85 KB
FindNumbers_Valid .NET Framework 4.8 .NET Framework 4.8 1000 12,829.9 μs 121.36 μs 113.52 μs 109.3750 - 753.13 KB
FindNumbers_StrictGrouping .NET Framework 4.8 .NET Framework 4.8 1000 18,272.5 μs 353.82 μs 363.34 μs 406.2500 31.2500 2654.28 KB

BenchmarkDotNet v0.15.8, Windows 11 (10.0.26100.32860/24H2/2024Update/HudsonValley) (Hyper-V)
AMD EPYC 7763 2.44GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.300
  [Host]             : .NET 9.0.16 (9.0.16, 9.0.1626.22923), X64 RyuJIT x86-64-v3
  .NET 8.0           : .NET 8.0.27 (8.0.27, 8.0.2726.22922), X64 RyuJIT x86-64-v3
  .NET 9.0           : .NET 9.0.16 (9.0.16, 9.0.1626.22923), X64 RyuJIT x86-64-v3
  .NET Framework 4.8 : .NET Framework 4.8.1 (4.8.9325.0), X64 RyuJIT VectorSize=256


Method Job Runtime PhoneNumberCount Mean Error StdDev Gen0 Gen1 Allocated
ParseValidateAndFormatPhoneNumbers .NET 8.0 .NET 8.0 1000 2.713 ms 0.0061 ms 0.0054 ms 35.1563 - 582.48 KB
ParseValidateAndFormatPhoneNumbers .NET 9.0 .NET 9.0 1000 2.565 ms 0.0093 ms 0.0083 ms 35.1563 - 582.48 KB
ParseValidateAndFormatPhoneNumbers .NET Framework 4.8 .NET Framework 4.8 1000 7.441 ms 0.0762 ms 0.0713 ms 226.5625 23.4375 1397.58 KB
ParseValidateAndFormatPhoneNumbers .NET 8.0 .NET 8.0 10000 27.273 ms 0.1811 ms 0.1605 ms 343.7500 - 5818.97 KB
ParseValidateAndFormatPhoneNumbers .NET 9.0 .NET 9.0 10000 25.355 ms 0.0660 ms 0.0585 ms 343.7500 - 5818.97 KB
ParseValidateAndFormatPhoneNumbers .NET Framework 4.8 .NET Framework 4.8 10000 73.640 ms 0.3488 ms 0.3092 ms 2142.8571 142.8571 13952.16 KB
ParseValidateAndFormatPhoneNumbers .NET 8.0 .NET 8.0 100000 271.212 ms 0.9295 ms 0.7761 ms 3500.0000 - 58205.32 KB
ParseValidateAndFormatPhoneNumbers .NET 9.0 .NET 9.0 100000 257.989 ms 2.9871 ms 2.7942 ms 3500.0000 - 58205.32 KB
ParseValidateAndFormatPhoneNumbers .NET Framework 4.8 .NET Framework 4.8 100000 754.965 ms 13.9973 ms 13.0930 ms 22000.0000 2000.0000 139529.64 KB
main branch

BenchmarkDotNet v0.15.8, Windows 11 (10.0.26100.32860/24H2/2024Update/HudsonValley) (Hyper-V)
AMD EPYC 7763 2.44GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.300
  [Host]             : .NET 9.0.16 (9.0.16, 9.0.1626.22923), X64 RyuJIT x86-64-v3
  .NET 8.0           : .NET 8.0.27 (8.0.27, 8.0.2726.22922), X64 RyuJIT x86-64-v3
  .NET 9.0           : .NET 9.0.16 (9.0.16, 9.0.1626.22923), X64 RyuJIT x86-64-v3
  .NET Framework 4.8 : .NET Framework 4.8.1 (4.8.9325.0), X64 RyuJIT VectorSize=256


Method Job Runtime PhoneNumberCount Mean Error StdDev Gen0 Gen1 Allocated
ParseValidateAndFormatPhoneNumbers .NET 8.0 .NET 8.0 1000 2.719 ms 0.0165 ms 0.0154 ms 35.1563 - 582.48 KB
ParseValidateAndFormatPhoneNumbers .NET 9.0 .NET 9.0 1000 2.515 ms 0.0066 ms 0.0052 ms 35.1563 - 582.48 KB
ParseValidateAndFormatPhoneNumbers .NET Framework 4.8 .NET Framework 4.8 1000 7.511 ms 0.0268 ms 0.0251 ms 226.5625 23.4375 1397.64 KB
ParseValidateAndFormatPhoneNumbers .NET 8.0 .NET 8.0 10000 26.832 ms 0.0589 ms 0.0522 ms 343.7500 - 5818.97 KB
ParseValidateAndFormatPhoneNumbers .NET 9.0 .NET 9.0 10000 25.445 ms 0.0696 ms 0.0617 ms 343.7500 - 5818.97 KB
ParseValidateAndFormatPhoneNumbers .NET Framework 4.8 .NET Framework 4.8 10000 75.861 ms 1.0163 ms 0.9010 ms 2142.8571 142.8571 13952.15 KB
ParseValidateAndFormatPhoneNumbers .NET 8.0 .NET 8.0 100000 274.642 ms 2.1780 ms 1.9308 ms 3500.0000 - 58205.32 KB
ParseValidateAndFormatPhoneNumbers .NET 9.0 .NET 9.0 100000 256.281 ms 0.6291 ms 0.5254 ms 3500.0000 - 58205.32 KB
ParseValidateAndFormatPhoneNumbers .NET Framework 4.8 .NET Framework 4.8 100000 756.903 ms 7.1415 ms 5.9635 ms 22000.0000 2000.0000 139529.64 KB

@twcclegg twcclegg marked this pull request as ready for review May 29, 2026 14:57
@twcclegg twcclegg requested a review from wmundev May 29, 2026 14:57
@wmundev wmundev requested a review from Copilot May 30, 2026 09:10

Copilot AI left a comment

Copy link
Copy Markdown

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 focuses on reducing runtime allocations in hot paths, compressing embedded metadata/mapping resources, and expanding the BenchmarkDotNet performance suite for the C# port of libphonenumber.

Changes:

  • Compress generated binary metadata/geocoding/timezone resources with gzip and update runtime loaders/readers to transparently decompress.
  • Reduce allocations in frequently executed code paths (matcher grouping validation, RFC3966 group splitting, AsYouTypeFormatter substring avoidance, etc.).
  • Add multiple new BenchmarkDotNet benchmarks and document how to run them; introduce Codecov coverage gates.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
csharp/PhoneNumbers/Util.cs Exposes internals to the performance test assembly.
csharp/PhoneNumbers/PrefixFileReader.cs Decompresses gzipped prefix-map resources before deserialization.
csharp/PhoneNumbers/PhoneNumberUtil.cs Uses generic Enum.GetValues where supported to reduce overhead.
csharp/PhoneNumbers/PhoneNumberToTimeZonesMapper.cs Decompresses gzipped timezone map resource before deserialization.
csharp/PhoneNumbers/PhoneNumbers.csproj Enables nullable on modern TFMs and opts into AOT/trim analysis (see review comments).
csharp/PhoneNumbers/PhoneNumberMatcher.cs Reduces allocations in grouping validation and avoids intermediate substring+split.
csharp/PhoneNumbers/MetadataLoader.cs Changes embedded metadata loading to return decompressed streams (gzip).
csharp/PhoneNumbers/AsYouTypeFormatter.cs Avoids intermediate substring allocations when extracting national number.
csharp/PhoneNumbers.PerformanceTest/README.md Documents additional available benchmarks.
csharp/PhoneNumbers.PerformanceTest/Benchmarks/AsYouTypeFormatterBenchmark.cs New benchmark measuring per-keystroke formatting cost.
csharp/PhoneNumbers.PerformanceTest/Benchmarks/PhoneNumberMatcherBenchmark.cs New benchmark for FindNumbers under different leniencies.
csharp/PhoneNumbers.PerformanceTest/Benchmarks/ParsingHelpersBenchmark.cs New benchmark isolating ExtractPossibleNumber paths.
csharp/PhoneNumbers.PerformanceTest/Benchmarks/ColdStartBenchmark.cs New cold-start benchmarks for first-use and lazy-load costs.
csharp/PhoneNumbers.MetadataBuilder/Program.cs Writes gzip-compressed outputs for metadata/geocoding/timezones generation.
codecov.yml Adds Codecov project/patch coverage thresholds.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +30 to +35
<!--
Opt in to trim/AOT analysis on modern .NET TFMs. IsAotCompatible turns on the trim, single-file,
and AOT analyzers, and stamps the assembly with the metadata that lets consumers PublishAot
without warnings about untrimmable references. netstandard2.0 consumers are unaffected.
-->
<IsAotCompatible>true</IsAotCompatible>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

i think this might be legit to address

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

  1. RegexOptions.Compiled is not annotated [RequiresDynamicCode]. The .NET regex engine detects AOT mode at runtime and silently falls back to the interpreted engine for any new Regex(pattern, RegexOptions.Compiled) call. Just a perf cost on whichever code paths build regexes dynamically. (See dotnet/runtime for Regex.Compile's AOT handling; the IL-emit path is guarded by an IsDynamicCodeSupported check.) That's why IsAotCompatible=true + TreatWarningsAsErrors=true builds clean.

  2. All static patterns already use [GeneratedRegex] on net7+. Source-generated regexes are fully ahead-of-time-compiled and indistinguishable from RegexOptions.Compiled performance under AOT. The runtime new Regex(pattern, Compiled) paths in the codebase are the netstandard2.0/net48 fallbacks (those TFMs don't set IsAotCompatible) plus the genuinely dynamic patterns in PhoneRegex / RegexCache whose patterns come from per-region metadata strings — these can't be source-generated regardless.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

i see, all good in that case

Comment on lines 126 to +133
public Stream? LoadMetadata(string fileName)
=> assembly.GetManifestResourceStream(resourcePrefix + fileName);
{
// The build pipeline gzips every bin before embedding it (see GZipStream wrapping in
// PhoneNumbers.MetadataBuilder). Decompress on the way out so callers see the plain
// bin format they already expect.
var raw = assembly.GetManifestResourceStream(resourcePrefix + fileName);
return raw == null ? null : new GZipStream(raw, CompressionMode.Decompress);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

i think this is fine since we use nuget packages

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

It's technically breaking in that what public Stream? LoadMetadata(string fileName) expects is changing but this was only added a month ago and I think it's safe to say no one is consuming it.

Comment thread csharp/PhoneNumbers.MetadataBuilder/Program.cs
Comment thread csharp/PhoneNumbers/PhoneNumbers.csproj
Comment thread csharp/PhoneNumbers/MetadataLoader.cs
Comment thread csharp/PhoneNumbers.MetadataBuilder/Program.cs
Comment thread csharp/PhoneNumbers/PhoneNumbers.csproj
Comment thread csharp/PhoneNumbers/MetadataLoader.cs
Comment thread csharp/PhoneNumbers.MetadataBuilder/Program.cs

@wmundev wmundev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

looks good, nice one

@twcclegg twcclegg merged commit 3e551ee into main Jun 3, 2026
10 checks passed
@twcclegg twcclegg deleted the performance branch June 3, 2026 02:22
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