Après avoir examiné les autres réponses et pris en compte les commentaires de CodeInChaos, ainsi que la réponse de CodeInChaos toujours biaisée (bien que moins), j'ai pensé qu'une solution finale de couper-coller était nécessaire. Donc, tout en mettant à jour ma réponse, j'ai décidé de tout faire.
Pour une version à jour de ce code, veuillez visiter le nouveau référentiel Hg sur Bitbucket: https://bitbucket.org/merarischroeder/secureswiftrandom . Je vous recommande de copier et coller le code de: https://bitbucket.org/merarischroeder/secureswiftrandom/src/6c14b874f34a3f6576b0213379ecdf0ffc7496ea/Code/Alivate.SolidSwiftRandom/SolidSwiftRandom.cs?at=default&fileviewer=file-view-default (assurez - vous que vous cliquez sur le bouton Raw pour faciliter la copie et vous assurer que vous avez la dernière version, je pense que ce lien va vers une version spécifique du code, pas la dernière).
Notes mises à jour:
- Concernant certaines autres réponses - Si vous connaissez la longueur de la sortie, vous n'avez pas besoin d'un StringBuilder, et lorsque vous utilisez ToCharArray, cela crée et remplit le tableau (vous n'avez pas besoin de créer d'abord un tableau vide)
- Concernant d'autres réponses - Vous devez utiliser NextBytes, plutôt que d'en obtenir une à la fois pour les performances
- Techniquement, vous pouvez épingler le tableau d'octets pour un accès plus rapide .. cela en vaut généralement la peine lorsque vous itérez plus de 6-8 fois sur un tableau d'octets. (Pas fait ici)
- Utilisation de RNGCryptoServiceProvider pour le meilleur caractère aléatoire
- Utilisation de la mise en cache d'un tampon de 1 Mo de données aléatoires - l'analyse comparative montre que la vitesse d'accès en octets uniques mis en cache est ~ 1000 fois plus rapide - ce qui prend 9 ms sur 1 Mo contre 989 ms pour la mise en cache.
- Rejet optimisé de la zone de biais au sein de ma nouvelle classe.
Solution finale à la question:
static char[] charSet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".ToCharArray();
static int byteSize = 256; //Labelling convenience
static int biasZone = byteSize - (byteSize % charSet.Length);
public string GenerateRandomString(int Length) //Configurable output string length
{
byte[] rBytes = new byte[Length]; //Do as much before and after lock as possible
char[] rName = new char[Length];
SecureFastRandom.GetNextBytesMax(rBytes, biasZone);
for (var i = 0; i < Length; i++)
{
rName[i] = charSet[rBytes[i] % charSet.Length];
}
return new string(rName);
}
Mais vous avez besoin de ma nouvelle classe (non testée):
/// <summary>
/// My benchmarking showed that for RNGCryptoServiceProvider:
/// 1. There is negligable benefit of sharing RNGCryptoServiceProvider object reference
/// 2. Initial GetBytes takes 2ms, and an initial read of 1MB takes 3ms (starting to rise, but still negligable)
/// 2. Cached is ~1000x faster for single byte at a time - taking 9ms over 1MB vs 989ms for uncached
/// </summary>
class SecureFastRandom
{
static byte[] byteCache = new byte[1000000]; //My benchmark showed that an initial read takes 2ms, and an initial read of this size takes 3ms (starting to raise)
static int lastPosition = 0;
static int remaining = 0;
/// <summary>
/// Static direct uncached access to the RNGCryptoServiceProvider GetBytes function
/// </summary>
/// <param name="buffer"></param>
public static void DirectGetBytes(byte[] buffer)
{
using (var r = new RNGCryptoServiceProvider())
{
r.GetBytes(buffer);
}
}
/// <summary>
/// Main expected method to be called by user. Underlying random data is cached from RNGCryptoServiceProvider for best performance
/// </summary>
/// <param name="buffer"></param>
public static void GetBytes(byte[] buffer)
{
if (buffer.Length > byteCache.Length)
{
DirectGetBytes(buffer);
return;
}
lock (byteCache)
{
if (buffer.Length > remaining)
{
DirectGetBytes(byteCache);
lastPosition = 0;
remaining = byteCache.Length;
}
Buffer.BlockCopy(byteCache, lastPosition, buffer, 0, buffer.Length);
lastPosition += buffer.Length;
remaining -= buffer.Length;
}
}
/// <summary>
/// Return a single byte from the cache of random data.
/// </summary>
/// <returns></returns>
public static byte GetByte()
{
lock (byteCache)
{
return UnsafeGetByte();
}
}
/// <summary>
/// Shared with public GetByte and GetBytesWithMax, and not locked to reduce lock/unlocking in loops. Must be called within lock of byteCache.
/// </summary>
/// <returns></returns>
static byte UnsafeGetByte()
{
if (1 > remaining)
{
DirectGetBytes(byteCache);
lastPosition = 0;
remaining = byteCache.Length;
}
lastPosition++;
remaining--;
return byteCache[lastPosition - 1];
}
/// <summary>
/// Rejects bytes which are equal to or greater than max. This is useful for ensuring there is no bias when you are modulating with a non power of 2 number.
/// </summary>
/// <param name="buffer"></param>
/// <param name="max"></param>
public static void GetBytesWithMax(byte[] buffer, byte max)
{
if (buffer.Length > byteCache.Length / 2) //No point caching for larger sizes
{
DirectGetBytes(buffer);
lock (byteCache)
{
UnsafeCheckBytesMax(buffer, max);
}
}
else
{
lock (byteCache)
{
if (buffer.Length > remaining) //Recache if not enough remaining, discarding remaining - too much work to join two blocks
DirectGetBytes(byteCache);
Buffer.BlockCopy(byteCache, lastPosition, buffer, 0, buffer.Length);
lastPosition += buffer.Length;
remaining -= buffer.Length;
UnsafeCheckBytesMax(buffer, max);
}
}
}
/// <summary>
/// Checks buffer for bytes equal and above max. Must be called within lock of byteCache.
/// </summary>
/// <param name="buffer"></param>
/// <param name="max"></param>
static void UnsafeCheckBytesMax(byte[] buffer, byte max)
{
for (int i = 0; i < buffer.Length; i++)
{
while (buffer[i] >= max)
buffer[i] = UnsafeGetByte(); //Replace all bytes which are equal or above max
}
}
}
Pour l'histoire - mon ancienne solution pour cette réponse, utilisé objet aléatoire:
private static char[] charSet =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".ToCharArray();
static rGen = new Random(); //Must share, because the clock seed only has Ticks (~10ms) resolution, yet lock has only 20-50ns delay.
static int byteSize = 256; //Labelling convenience
static int biasZone = byteSize - (byteSize % charSet.Length);
static bool SlightlyMoreSecurityNeeded = true; //Configuration - needs to be true, if more security is desired and if charSet.Length is not divisible by 2^X.
public string GenerateRandomString(int Length) //Configurable output string length
{
byte[] rBytes = new byte[Length]; //Do as much before and after lock as possible
char[] rName = new char[Length];
lock (rGen) //~20-50ns
{
rGen.NextBytes(rBytes);
for (int i = 0; i < Length; i++)
{
while (SlightlyMoreSecurityNeeded && rBytes[i] >= biasZone) //Secure against 1/5 increased bias of index[0-7] values against others. Note: Must exclude where it == biasZone (that is >=), otherwise there's still a bias on index 0.
rBytes[i] = rGen.NextByte();
rName[i] = charSet[rBytes[i] % charSet.Length];
}
}
return new string(rName);
}
Performance:
- SecureFastRandom - Première exécution unique = ~ 9-33 ms . Imperceptible. En cours : 5 ms (parfois jusqu'à 13 ms) sur 10 000 itérations, avec une seule itération moyenne = 1,5 microsecondes. . Remarque: nécessite généralement 2, mais parfois jusqu'à 8 rafraîchissements de cache - dépend du nombre d'octets uniques dépassant la zone de polarisation
- Aléatoire - Premier cycle unique = ~ 0-1 ms . Imperceptible. En cours : 5 ms sur 10 000 itérations. Avec une seule itération moyenne = 0,5 microsecondes. . À peu près à la même vitesse.
Consultez également:
Ces liens sont une autre approche. La mise en mémoire tampon pourrait être ajoutée à cette nouvelle base de code, mais le plus important était d'explorer différentes approches pour éliminer les biais et de comparer les vitesses et les avantages / inconvénients.