Skip to content

Conversation

@kzrnm
Copy link
Contributor

@kzrnm kzrnm commented Sep 11, 2025

Extracted the logic into EnsureTerminated()

This allowed removing the overloads of AsSpan(terminate) and GetPinnableReference(terminate).

Unified the implementation of AppendFormat with that of StringBuilder .

Copied from StringBuilder, can't be done via generic extension as ValueStringBuilder is a ref struct and cannot be used in a generic.

In .NET 9, generics can use ref structs.

This is reverted due to observed performance regression.

Remove ValueStringBuilder.TryCopyTo.

Even though TryCopyTo has the unintuitive behavior of also calling Dispose, it’s only used in a single place—Number.BigInteger.cs.

@github-actions github-actions bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Sep 11, 2025
@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label Sep 11, 2025
@jkotas
Copy link
Member

jkotas commented Sep 12, 2025

How does this affect StringBuilder.AppendFormat performance?

Despite the unintuitive behavior of TryCopyTo also calling Dispose, it’s only being used in a single place.
@kzrnm
Copy link
Contributor Author

kzrnm commented Sep 12, 2025

@jkotas

The benchmark results for string.Format (which internally calls ValueStringBuilder.AppendFormat) and StringBuilder.AppendFormat are as follows. With StringBuilder there is almost no impact, while with ValueStringBuilder performance has improved.


BenchmarkDotNet v0.15.2, Windows 11 (10.0.26100.4770/24H2/2024Update/HudsonValley)
13th Gen Intel Core i5-13500 2.50GHz, 1 CPU, 20 logical and 14 physical cores
.NET SDK 10.0.100-rc.1.25420.111
  [Host]     : .NET 10.0.0 (10.0.25.42111), X64 RyuJIT AVX2
  Job-ZICBXN : .NET 10.0.0 (42.42.42.42424), X64 RyuJIT AVX2
  Job-QENHHP : .NET 10.0.0 (42.42.42.42424), X64 RyuJIT AVX2


Method Toolchain Mean Ratio Code Size Gen0 Allocated Alloc Ratio
ValueStringBuilderSmall \main\corerun.exe 170.2 ns 1.00 2,673 B 0.0203 256 B 1.00
ValueStringBuilderSmall \pr\corerun.exe 164.7 ns 0.97 409 B 0.0203 256 B 1.00
StringBuilderSmall \main\corerun.exe 145.7 ns 1.00 6,266 B - - NA
StringBuilderSmall \pr\corerun.exe 146.6 ns 1.01 5,452 B - - NA
ValueStringBuilderLarge \main\corerun.exe 6,534.0 ns 1.00 2,673 B 0.4654 5856 B 1.00
ValueStringBuilderLarge \pr\corerun.exe 5,228.6 ns 0.80 409 B 0.4654 5856 B 1.00
StringBuilderLarge \main\corerun.exe 4,900.4 ns 1.00 6,864 B - - NA
StringBuilderLarge \pr\corerun.exe 4,832.9 ns 0.99 5,860 B - - NA
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using BenchmarkDotNet.Configs;
using System;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Linq;
using System.Text;

[DisassemblyDiagnoser]
[MemoryDiagnoser(false)]
[HideColumns("Job", "Error", "StdDev", "Median", "RatioSD")]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByMethod)]
public class StringBuilderTest
{
    object[] formatArgs;
    string formatSmall, formatLarge;
    StringBuilder sb;

    [GlobalSetup]
    public void Setup()
    {
        sb = new StringBuilder();
        formatSmall = "Text={0}, DateTime={1:G}, Number={2}, NumberX={2:X}, EnumD={3:D}, EnumF={3:F}";
        formatLarge = formatSmall + ", " + string.Join(", ", Enumerable.Repeat("DateTime={1:yyyyMMdd-HH:mm:ss}", 100));
        formatArgs = ["Text", new DateTime(2025, 4, 5, 6, 7, 5), long.MinValue, DayOfWeek.Friday];

        StringBuilderLarge(); // allocation
    }

    [Benchmark] public string ValueStringBuilderSmall() => string.Format(CultureInfo.InvariantCulture, formatSmall, formatArgs);
    [Benchmark]
    public StringBuilder StringBuilderSmall()
    {
        sb.Clear();
        return sb.AppendFormat(CultureInfo.InvariantCulture, formatSmall, formatArgs);
    }

    [Benchmark] public string ValueStringBuilderLarge() => string.Format(CultureInfo.InvariantCulture, formatLarge, formatArgs);
    [Benchmark]
    public StringBuilder StringBuilderLarge()
    {
        sb.Clear();
        return sb.AppendFormat(CultureInfo.InvariantCulture, formatLarge, formatArgs);
    }
}

@jkotas
Copy link
Member

jkotas commented Sep 14, 2025

@EgorBot -intel

using BenchmarkDotNet.Attributes;
using System.Text;

public class Bench
{
    StringBuilder sb = new StringBuilder(100);

    [IterationSetup]
    public void IterationSetup() => sb.Clear();

    [Benchmark]
    public void AppendFormatSimple() => sb.AppendFormat("{0}{1}{2}", 2, 3, 4);
}

@jkotas
Copy link
Member

jkotas commented Sep 14, 2025

30+% regression in the simple micro-benchmark for StringBuilder.AppendFormat: EgorBot/runtime-utils#484 (comment)

@kzrnm
Copy link
Contributor Author

kzrnm commented Sep 15, 2025

@jkotas I reverted the changes related to AppendFormat. I’ll look into whether this can be addressed separately.

Copy link
Member

@jkotas jkotas left a comment

Choose a reason for hiding this comment

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

Thanks

@jkotas jkotas merged commit 524b082 into dotnet:main Sep 16, 2025
138 of 144 checks passed
@kzrnm kzrnm deleted the feature/vsb branch September 16, 2025 10:51
@github-actions github-actions bot locked and limited conversation to collaborators Oct 17, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

community-contribution Indicates that the PR has been added by a community member needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants