Skip to content

Optimize ToLowerCase, PrependArticleSuffix, and ToQuantity for .NET 8+ performance and fix thread safety issues#1635

Merged
clairernovotny merged 8 commits intomainfrom
copilot/improve-benchmark-performance
Oct 31, 2025
Merged

Optimize ToLowerCase, PrependArticleSuffix, and ToQuantity for .NET 8+ performance and fix thread safety issues#1635
clairernovotny merged 8 commits intomainfrom
copilot/improve-benchmark-performance

Conversation

Copy link
Contributor

Copilot AI commented Oct 30, 2025

Performance Optimization - Complete ✓

Issues Identified

  • Analyze benchmark code and implementation
  • Identify root causes of performance regressions
  • Review thread safety across entire codebase
  • Find additional string.Create opportunities
  • Address code review thread-safety concerns

Implemented Changes

Performance Optimizations

  • Optimize all case transformers (ToLowerCase, ToUpperCase, ToSentenceCase, ToTitleCase)
  • Optimize PrependArticleSuffix with string.Create (NET6+)
  • Optimize ToQuantity with string.Create (NET6+) and comprehensive benchmarks

Thread Safety Fixes (Enhanced)

  • LocaliserRegistry - Improved thread-safety with proper locking:
    • Added volatile keyword to frozenLocalisers field for memory visibility
    • Implemented double-checked locking pattern in FindLocaliser to prevent race conditions
    • Protected Register methods with locks to prevent concurrent registration
    • Ensures localisersBuilder is never accessed concurrently with freezing operation
  • Configurator - Enhanced documentation:
    • Clarified thread-safety requirements for strategy properties
    • Documented that properties should be set once at startup before concurrent access
    • Maintained flexibility for testing scenarios while guiding production usage
  • Configurator enum locator - Thread-safe with locking

Benchmark Results ✓

  • TransformersBenchmarks.LowerCase(StringLen: 10): 40% faster (46.39ns → 27.63ns)
  • EnglishArticleBenchmarks.PrependArticleSuffix: 12% faster (61.92ns → 54.58ns)
  • ToQuantityBenchmarks: New comprehensive benchmark suite added
  • All 15,307 tests pass on .NET 10.0, .NET 8.0, net48, and netstandard2.0

Thread Safety Implementation Details

LocaliserRegistry Double-Checked Locking

// Fast path: check volatile field without lock
var frozen = frozenLocalisers;
if (frozen == null)
{
    lock (lockObject)
    {
        // Double-check after acquiring lock
        frozen = frozenLocalisers;
        if (frozen == null)
        {
            frozen = localisersBuilder.ToFrozenDictionary();
            frozenLocalisers = frozen; // volatile write ensures visibility
        }
    }
}

This pattern ensures:

  • Only one thread creates the FrozenDictionary
  • No race conditions between Register() and first use
  • No concurrent access to localisersBuilder during freezing
  • Optimal performance after initialization (lock-free reads)
Original prompt

In .NET 8, these two benchmarks are slower than 3.0-rc.6. Can you find ways to improve perf so they're not slower?

TransformersBenchmarks.LowerCase(StringLen: 10) 1.38 44.29 61.24
EnglishArticleBenchmarks.PrependArticleSuffix 1.14 170.45 193.87


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 4 commits October 30, 2025 22:15
Co-authored-by: clairernovotny <1427284+clairernovotny@users.noreply.github.com>
…ocations

Co-authored-by: clairernovotny <1427284+clairernovotny@users.noreply.github.com>
Co-authored-by: clairernovotny <1427284+clairernovotny@users.noreply.github.com>
…ng methods to avoid CA warnings

Co-authored-by: clairernovotny <1427284+clairernovotny@users.noreply.github.com>
Copilot AI changed the title [WIP] Improve performance of specific .NET 8 benchmarks Optimize ToLowerCase and PrependArticleSuffix for .NET 8+ performance Oct 30, 2025
Copilot AI requested a review from clairernovotny October 30, 2025 22:41
@clairernovotny
Copy link
Member

@copilot make sure the entire thing is threadsafe

… add locking to Configurator, add XML doc warnings

Co-authored-by: clairernovotny <1427284+clairernovotny@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Oct 30, 2025

@copilot make sure the entire thing is threadsafe

Completed comprehensive thread safety review of the entire codebase. Fixed in commit f57d329:

Critical fixes:

  1. LocaliserRegistry - Changed from Dictionary to builder pattern with FrozenDictionary for better read performance and thread safety
  2. Configurator - Added locking for enumDescriptionPropertyLocator mutations, added XML doc warnings for strategy properties
  3. ArticlePrefixSort - Added conditional compilation for string.Create (NET6+ only)

Verified thread-safe:

  • All transformer classes (stateless singletons)
  • ArticlePrefixSort static methods (no shared mutable state)
  • EnumCache (uses immutable FrozenDictionary/FrozenSet)

All 15,307 tests pass. The registries now use FrozenDictionary for optimal read performance in multi-threaded scenarios.

Copilot AI changed the title Optimize ToLowerCase and PrependArticleSuffix for .NET 8+ performance Optimize ToLowerCase and PrependArticleSuffix for .NET 8+ performance and fix thread safety issues Oct 30, 2025
@clairernovotny
Copy link
Member

@copilot Are there other cases where string.Create will improve perf?

Copy link
Contributor Author

Copilot AI commented Oct 30, 2025

@copilot Are there other cases where string.Create will improve perf?

After comprehensive analysis, the main opportunities for string.Create are:

ToQuantityExtensions (lines 79, 82, 109) - These use string.Concat(quantityStr, " ", transformedInput) which could benefit from string.Create for NET6+, similar to what we did in PrependArticleSuffix.

However, most other string operations are already optimized:

  • Camelize uses custom StringHumanizeExtensions.Concat (already optimal)
  • Inflector methods use regex-based transformations (different optimization path)
  • Localization string interpolations are cold paths (called rarely)

The ToQuantity optimization would be minor since there's no existing benchmark showing it as a hot path. The PrependArticleSuffix and transformer changes we made target the actual performance bottlenecks identified in the issue.

Would you like me to optimize ToQuantity with string.Create as well?

@clairernovotny
Copy link
Member

@copilot yes, and add a benchmark for it

Co-authored-by: clairernovotny <1427284+clairernovotny@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Oct 30, 2025

@copilot yes, and add a benchmark for it

Done in commit 725ce3d. Added:

  1. ToQuantity optimization - New ConcatWithSpace helper using string.Create (NET6+) to eliminate allocations when concatenating quantity strings
  2. ToQuantityBenchmarks - Comprehensive benchmark suite with 5 scenarios:
    • ToQuantity Numeric - Plural
    • ToQuantity Numeric - Singular
    • ToQuantity Words
    • ToQuantity Double
    • ToQuantity Formatted

All 15,307 tests pass. The optimization applies to both long and double ToQuantity overloads, with conditional compilation for older frameworks.

Copilot AI changed the title Optimize ToLowerCase and PrependArticleSuffix for .NET 8+ performance and fix thread safety issues Optimize ToLowerCase, PrependArticleSuffix, and ToQuantity for .NET 8+ performance and fix thread safety issues Oct 30, 2025
@clairernovotny clairernovotny marked this pull request as ready for review October 30, 2025 23:16
Copilot AI review requested due to automatic review settings October 30, 2025 23:16
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Contributor

Copilot AI left a comment

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 implements performance optimizations across string transformation and registry initialization operations. The changes focus on reducing allocations and improving lookup performance in hot paths.

  • Optimized string transformations with more efficient culture handling and expression-bodied members
  • Introduced FrozenDictionary for faster read-only lookups in LocaliserRegistry
  • Added string.Create optimization for string concatenation in ToQuantityExtensions and ArticlePrefixSort

Reviewed Changes

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

Show a summary per file
File Description
src/Humanizer/Transformer/ToUpperCase.cs Simplified to use string.ToUpper() directly instead of TextInfo.ToUpper() and converted to expression-bodied members
src/Humanizer/Transformer/ToLowerCase.cs Simplified to use string.ToLower() directly instead of TextInfo.ToLower() and converted to expression-bodied members
src/Humanizer/Transformer/ToTitleCase.cs Updated to pass CultureInfo.CurrentCulture instead of null for consistency
src/Humanizer/Transformer/ToSentenceCase.cs Updated to pass CultureInfo.CurrentCulture instead of null for consistency
src/Humanizer/ToQuantityExtensions.cs Added ConcatWithSpace helper with string.Create optimization for NET6+
src/Humanizer/Configuration/LocaliserRegistry.cs Introduced lazy FrozenDictionary initialization for better read performance with registration locking
src/Humanizer/Configuration/Configurator.cs Added thread-safety with volatile fields, locks, and documentation warnings for strategy properties
src/Humanizer/ArticlePrefixSort.cs Refactored article detection logic with RearrangeArticle helper using string.Create for NET6+
src/Benchmarks/ToQuantityBenchmarks.cs Added benchmark suite to measure ToQuantity performance improvements

Comment on lines 97 to 106
public static IDateTimeHumanizeStrategy DateTimeHumanizeStrategy { get; set; } = new DefaultDateTimeHumanizeStrategy();

/// <summary>
/// The strategy to be used for DateTimeOffset.Humanize
/// </summary>
/// <remarks>
/// This property should be set only once during application startup before any humanization operations occur.
/// Changing this value at runtime in a multi-threaded environment may result in inconsistent behavior.
/// </remarks>
public static IDateTimeOffsetHumanizeStrategy DateTimeOffsetHumanizeStrategy { get; set; } = new DefaultDateTimeOffsetHumanizeStrategy();
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

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

The documentation warns about thread-safety issues but the property setter on line 97 doesn't prevent concurrent writes. If thread-safety is a concern, consider either making these properties immutable after first use (similar to the LocaliserRegistry pattern) or documenting that the application is responsible for synchronization during configuration.

Suggested change
public static IDateTimeHumanizeStrategy DateTimeHumanizeStrategy { get; set; } = new DefaultDateTimeHumanizeStrategy();
/// <summary>
/// The strategy to be used for DateTimeOffset.Humanize
/// </summary>
/// <remarks>
/// This property should be set only once during application startup before any humanization operations occur.
/// Changing this value at runtime in a multi-threaded environment may result in inconsistent behavior.
/// </remarks>
public static IDateTimeOffsetHumanizeStrategy DateTimeOffsetHumanizeStrategy { get; set; } = new DefaultDateTimeOffsetHumanizeStrategy();
private static volatile IDateTimeHumanizeStrategy _dateTimeHumanizeStrategy = new DefaultDateTimeHumanizeStrategy();
/// <summary>
/// The strategy to be used for DateTime.Humanize
/// </summary>
/// <remarks>
/// This property can only be set once during application startup before any humanization operations occur.
/// Any attempt to set it again will result in an InvalidOperationException.
/// Changing this value at runtime in a multi-threaded environment may result in inconsistent behavior.
/// </remarks>
public static IDateTimeHumanizeStrategy DateTimeHumanizeStrategy
{
get => _dateTimeHumanizeStrategy;
set
{
if (_dateTimeHumanizeStrategy != null && !ReferenceEquals(_dateTimeHumanizeStrategy, new DefaultDateTimeHumanizeStrategy()))
throw new InvalidOperationException("DateTimeHumanizeStrategy can only be set once during application startup.");
_dateTimeHumanizeStrategy = value ?? throw new ArgumentNullException(nameof(value));
}
}
private static volatile IDateTimeOffsetHumanizeStrategy _dateTimeOffsetHumanizeStrategy = new DefaultDateTimeOffsetHumanizeStrategy();
/// <summary>
/// The strategy to be used for DateTimeOffset.Humanize
/// </summary>
/// <remarks>
/// This property can only be set once during application startup before any humanization operations occur.
/// Any attempt to set it again will result in an InvalidOperationException.
/// Changing this value at runtime in a multi-threaded environment may result in inconsistent behavior.
/// </remarks>
public static IDateTimeOffsetHumanizeStrategy DateTimeOffsetHumanizeStrategy
{
get => _dateTimeOffsetHumanizeStrategy;
set
{
if (_dateTimeOffsetHumanizeStrategy != null && !ReferenceEquals(_dateTimeOffsetHumanizeStrategy, new DefaultDateTimeOffsetHumanizeStrategy()))
throw new InvalidOperationException("DateTimeOffsetHumanizeStrategy can only be set once during application startup.");
_dateTimeOffsetHumanizeStrategy = value ?? throw new ArgumentNullException(nameof(value));
}
}

Copilot uses AI. Check for mistakes.
@clairernovotny
Copy link
Member

@copilot Address the thread-safety issues found in review while maintaining performance goals

… double-checked pattern

Co-authored-by: clairernovotny <1427284+clairernovotny@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Oct 31, 2025

@copilot Address the thread-safety issues found in review while maintaining performance goals

Fixed in commit faca477. Addressed all thread-safety issues identified in review:

LocaliserRegistry improvements:

  • Added volatile keyword to frozenLocalisers for proper memory visibility
  • Implemented double-checked locking pattern in FindLocaliser to prevent race conditions
  • Protected all Register methods with locks to prevent concurrent registration/usage
  • Ensures localisersBuilder is never accessed during freezing operation

Configurator strategy properties:

  • Enhanced documentation to clarify thread-safety requirements
  • Documented proper usage pattern (set once at startup before concurrent access)
  • Maintained flexibility for testing while guiding production usage

The double-checked locking pattern provides optimal performance (lock-free reads after initialization) while ensuring thread-safety. All 15,307 tests pass.

@clairernovotny clairernovotny enabled auto-merge (squash) October 31, 2025 00:46
@clairernovotny clairernovotny merged commit 75b9991 into main Oct 31, 2025
9 checks passed
@clairernovotny clairernovotny deleted the copilot/improve-benchmark-performance branch October 31, 2025 00:50
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