-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Description
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);
}
}