KEMBAR78
RandomNumberGenerator bad performance on GetString and possible reason · Issue #92130 · dotnet/runtime · GitHub
Skip to content

RandomNumberGenerator bad performance on GetString and possible reason #92130

@Ponant

Description

@Ponant

Description

I was using .Net 7 and did not have available a way to generate a random string based on the Base64 url encoded characters and SO answers were showing low perf code. So I made one during this summer until I noticed yesterday that .Net 8 has a new GetString so I jumped into it . The one I have is faster up to 10 times for a 32 long string. Culprit and code below.

Configuration

.Net 8 RC1, Windows 11 Pro 64 V 22H2, Thinkpad X1 Yoga

Data for length = 11 (YouTube ids)

Method Mean Error StdDev Median Gen0 Allocated
GetString2 158.7 ns 3.22 ns 7.64 ns 160.7 ns 0.0114 48 B
GetString 1,002.2 ns 20.08 ns 46.93 ns 1,027.0 ns 0.0114 48 B

Analysis

Problem is calling GetInt32 in the loop

       private static void GetItemsCore<T>(ReadOnlySpan<T> choices, Span<T> destination)
        {
            for (int i = 0; i < destination.Length; i++)
            {
                destination[i] = choices[GetInt32(choices.Length)];
            }
        }

Limits

Limits of my approach. I must have a choices string for which the length is a power of 2 <256. That is the case for Length64String and it is enough for my purposes and I believe for most web developpers.
I did not dig into your code to see if you have the same limitation, i.e. whether you need to abide to in order to ensure a uniform distribution.

Solution

If you have that same limitation (I do not expect, a priori), then the API must be corrected to ensure uniformity.
If you do not have this limitation, I still believe it is worth having specialized methods to generate strings that are a power of 2 while < 256 or review the GetItemsCore to improve perfomance.

Code Program.cs

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Security.Cryptography;

BenchmarkRunner.Run<RandomBase64StringGenerator>();

[MemoryDiagnoser]
public class RandomBase64StringGenerator
{
    const int length = 11; 

    [Benchmark]
    public void GetString2()
    {
       RandomGenerator.New64(length);
    }

    [Benchmark]
    public void GetString()
    {
        RandomNumberGenerator.GetString(RandomGenerator.Constants.Length64String,length);
    }
}

public sealed class RandomGenerator
{
    public class Constants
    {
        public const string Length64String = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-";
    }

    const int _stackAllocThreshold = 32;

    public static string New64(int length = 11)
    {
        return GetRandomString(length, Constants.Length64String);
    }

    private static string GetRandomString(int length, ReadOnlySpan<char> characters)
    {
        // No plan to go beyond _stackAllocThreshold, so we throw
        if (length <= 0 || length > _stackAllocThreshold)
            throw new ArgumentException($"{nameof(length)} must be at least one" +
                $" and lower or equal to {_stackAllocThreshold}");

        Span<byte> bytes = stackalloc byte[length];
        Span<char> chars = stackalloc char[length];

        RandomNumberGenerator.Fill(bytes);

        int mask = 256 / characters.Length;

        for (var i = 0; i < bytes.Length; i++)
            chars[i] = characters[bytes[i] / mask];

        return new string(chars);
    }
}

Metadata

Metadata

Assignees

Labels

area-System.Securityin-prThere is an active PR which will close this issue when it is mergedtenet-performancePerformance related issue

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions