Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Use cached strings for ToString on integer values 0 to 9 #18383

Merged
merged 1 commit into from
Jun 9, 2018

Conversation

stephentoub
Copy link
Member

When doing ToString on single-digit integers, just return a cached string. This helps avoid string allocation/formatting costs for this very common case, while only costing one comparison for other cases.

Microbenchmark:

using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using BenchmarkDotNet.Attributes.Jobs;

[MemoryDiagnoser]
[InProcess]
public class Benchmarks
{
    private static void Main() => BenchmarkRunner.Run<Benchmarks>();

    [Benchmark]
    public static string ToString1() => 1.ToString();

    [Benchmark]
    public static string ToString11() => 11.ToString();
}

Before:

Method Mean Error StdDev Gen 0 Allocated
ToString1 20.13 ns 0.4800 ns 0.5527 ns 0.0057 24 B
ToString11 23.45 ns 0.5625 ns 1.0970 ns 0.0076 32 B

After:

Method Mean Error StdDev Gen 0 Allocated
ToString1 14.79 ns 0.3820 ns 0.3752 ns - 0 B
ToString11 23.11 ns 0.5537 ns 0.6154 ns 0.0076 32 B

cc: @jkotas, @mjsabby, @ahsonkhan

When doing ToString on single-digit integers, just return a cached string.  This helps avoid string allocation/formatting costs for this very common case, while only costing one comparison for other cases.
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.

Nice

@briansull
Copy link

briansull commented Jun 8, 2018

The value 10 is also a commonly used, if you wan to add one more to the cache.

@@ -1095,6 +1097,13 @@ private static unsafe void UInt32ToNumber(uint value, ref NumberBuffer number)
private static unsafe string UInt32ToDecStr(uint value, int digits)
{
int bufferLength = Math.Max(digits, FormattingHelpers.CountDigits(value));

Choose a reason for hiding this comment

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

Why do we have to call CountDigits, when we have a small 'value' 0-10 ?
Can't we first test the value and see it is range. [0-10]

Choose a reason for hiding this comment

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

CountDigits is generally quite fast already and in the 0-10 case, it would be just 2 comparison instead of one (without adding the overhead of the 0-10 check for all other values):

@stephentoub
Copy link
Member Author

stephentoub commented Jun 8, 2018

The value 10 is also a commonly used, if you wan to add one more to the cache.

We could experiment with that. It would entail not only adding one more string but also changing the bufferLength == 1 comparison to something more like digits <= 1 && (uint)value <= 10 (which would skip the fast path for the niche case where digits is 2 and value is 10), or bufferLength == 1 || (digits <= 2 && value == 10), or something like that.

// For single-digit values that are very common, especially 0 and 1, just return cached strings.
if (bufferLength == 1)
{
return s_singleDigitStringCache[value];

Choose a reason for hiding this comment

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

if (value <= 10)
{
     return s_smallNumberStringCache[value];
}

Copy link
Member Author

Choose a reason for hiding this comment

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

That also needs to factor in digits. This can be called with something like digits == 5 and value == 2, and it needs to return "00002".

Copy link

@ahsonkhan ahsonkhan Jun 8, 2018

Choose a reason for hiding this comment

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

In that case, should we be validating that digits is -1/etc., or is it safe to always return single digits as "digit" if bufferLength is 1?

Copy link
Member Author

Choose a reason for hiding this comment

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

In that case, should we be validating that digits is -1/etc., or is it safe to always return single digits as "digit" if bufferLength is 1?

No matter what the digits value is, there will always be at least one digit rendered.

@@ -1339,6 +1348,13 @@ private static unsafe string UInt64ToDecStr(ulong value, int digits)
digits = 1;

int bufferLength = Math.Max(digits, FormattingHelpers.CountDigits(value));

// For single-digit values that are very common, especially 0 and 1, just return cached strings.
if (bufferLength == 1)
Copy link
Member

Choose a reason for hiding this comment

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

Skip range check for this one (moving bufferLength after loop) and use

string[] ditgitCache = s_singleDigitStringCache;
if ((uint)value < (uint)ditgitCache.Length)
{
    return ditgitCache[value];
}

Would also allow you to have any number in cache? (within reason)

Copy link
Member Author

Choose a reason for hiding this comment

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

Would also allow you to have any number in cache? (within reason)

As noted earlier, it would also need to check digits. That's why I did the comparison against bufferLength.

Copy link
Member

Choose a reason for hiding this comment

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

Ahh... leading zeros...

Copy link

@briansull briansull Jun 8, 2018

Choose a reason for hiding this comment

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

Then we can use this:

if ((value <= s_smallNumberStringCache.Length) && 
    (digits <= FormattingHelpers.CountDigits(value)))
{
     return s_smallNumberStringCache[value];
}

Note value is already unsigned (a least in this variant of the method)

Copy link
Member Author

Choose a reason for hiding this comment

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

Right, and I've been simply noting it would need to be measured.

Copy link

@ahsonkhan ahsonkhan left a comment

Choose a reason for hiding this comment

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

Awesome!

@stephentoub
Copy link
Member Author

I'm looking into the commonality of other values, and will follow-up if it looks fruitful. For now, my goal was really just focused on 0 and 1, and 2-9 came along for the ride (cheaper to support them than to not support them).

@stephentoub stephentoub merged commit 12f8a27 into dotnet:master Jun 9, 2018
@stephentoub stephentoub deleted the singledigitstring branch June 9, 2018 02:53
dotnet-bot pushed a commit to dotnet/corefx that referenced this pull request Jun 9, 2018
When doing ToString on single-digit integers, just return a cached string.  This helps avoid string allocation/formatting costs for this very common case, while only costing one comparison for other cases.

Signed-off-by: dotnet-bot-corefx-mirror <[email protected]>
Anipik pushed a commit to dotnet/corefx that referenced this pull request Jun 9, 2018
When doing ToString on single-digit integers, just return a cached string.  This helps avoid string allocation/formatting costs for this very common case, while only costing one comparison for other cases.

Signed-off-by: dotnet-bot-corefx-mirror <[email protected]>
dotnet-bot pushed a commit to dotnet/corert that referenced this pull request Jun 9, 2018
When doing ToString on single-digit integers, just return a cached string.  This helps avoid string allocation/formatting costs for this very common case, while only costing one comparison for other cases.

Signed-off-by: dotnet-bot <[email protected]>
jkotas pushed a commit to dotnet/corert that referenced this pull request Jun 10, 2018
When doing ToString on single-digit integers, just return a cached string.  This helps avoid string allocation/formatting costs for this very common case, while only costing one comparison for other cases.

Signed-off-by: dotnet-bot <[email protected]>
picenka21 pushed a commit to picenka21/runtime that referenced this pull request Feb 18, 2022
When doing ToString on single-digit integers, just return a cached string.  This helps avoid string allocation/formatting costs for this very common case, while only costing one comparison for other cases.

Commit migrated from dotnet/coreclr@12f8a27
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants