Moyen le plus rapide (en terme de performance) pour mettre une chaîne en un byte [] tableau en C #

voix
7

Quel est le meilleur moyen de transformer une chaîne en un tableau d'octets [] en C #? J'envoie tonnes de données de chaîne par des prises et doivent optimiser chaque opération. Actuellement, je transforme les chaînes pour octet [] avant d'envoyer des tableaux à l'aide:

private static readonly Encoding encoding = new ASCIIEncoding();
//...
byte[] bytes = encoding.GetBytes(someString);
socket.Send(bytes);
//...
Créé 26/08/2009 à 23:30
source utilisateur
Dans d'autres langues...                            


9 réponses

voix
15

Si toutes vos données sont vraiment va être ASCII, alors vous pourriez être en mesure de le faire un peu plus vite que ASCIIEncoding, qui a différents (tout à fait raisonnable) bits de traitement des erreurs , etc. Vous pouvez également être en mesure d'accélérer en évitant la création de nouveaux octets les tableaux tout le temps. En supposant que vous avez une limite supérieure à laquelle tous vos messages seront sous:

void QuickAndDirtyAsciiEncode(string chars, byte[] buffer)
{
    int length = chars.Length;
    for (int i = 0; i < length; i++)
    {
        buffer[i] = (byte) (chars[i] & 0x7f);
    }
}

Vous souhaitez faire quelque chose comme:

readonly byte[] Buffer = new byte[8192]; // Reuse this repeatedly
...
QuickAndDirtyAsciiEncode(text, Buffer);
// We know ASCII takes one byte per character
socket.Send(Buffer, text.Length, SocketFlags.None);

Cette optimisation est assez désespérée cependant. Je tiens avec ASCIIEncodingjusqu'à ce que je l' avais prouvé que cela était le goulot d' étranglement (ou du moins que ce genre de bidouille grotty ne vous aide pas).

Créé 26/08/2009 à 23:47
source utilisateur

voix
9

Je dirais que la façon dont vous le faites est maintenant beaucoup de bien. Si vous êtes vraiment préoccupé par une très faible optimisation de niveau comme ça, la meilleure recommandation que je peux faire est d'obtenir réflecteur. Avec réflecteur, vous pouvez consulter le code vous-même (la plupart du temps), et voir ce que les algorithmes sont. Si le réflecteur ne vous montre pas, vous pouvez toujours télécharger Microsofts SSCLI (Shared Source Infrastructure Common Language) pour voir le code C ++ derrière les méthodes MethodImplOptions.InternalCall.

Pour référence, voici la mise en œuvre effective de Encoding.ASCII.GetBytes:

public override int GetBytes(string chars, int charIndex, int charCount, byte[] bytes, int byteIndex)
{
    if ((chars == null) || (bytes == null))
    {
        throw new ArgumentNullException();
    }
    if ((charIndex < 0) || (charCount < 0))
    {
        throw new ArgumentOutOfRangeException();
    }
    if ((chars.Length - charIndex) < charCount)
    {
        throw new ArgumentOutOfRangeException();
    }
    if ((byteIndex < 0) || (byteIndex > bytes.Length))
    {
        throw new ArgumentOutOfRangeException();
    }
    if ((bytes.Length - byteIndex) < charCount)
    {
        throw new ArgumentException();
    }
    int num = charIndex + charCount;
    while (charIndex < num)
    {
        char ch = chars[charIndex++];
        if (ch >= '\x0080')
        {
            ch = '?';
        }
        bytes[byteIndex++] = (byte) ch;
    }
    return charCount;
}
Créé 26/08/2009 à 23:34
source utilisateur

voix
1

La caractéristique de performance de la mise en œuvre d' une fonction de bibliothèque memcpy à usage général en utilisant un registre SIMD est beaucoup plus coloré que d' une mise en œuvre équivalente à l' aide d' un registre à usage général ...

  - Intel 64 et IA-32 Architectures Optimisation Manuel de référence (Avril 2018) §3.7 .6.1


Pour crier vitesse à la conversion à moyen et à plus grande taille des blocs de données entre 8 bits byte[]et le texte « large » (16 bits, Unicode), vous aurez envie d'envisager des solutions qui déploient les SIMD instructions (élargissement) et (rétrécissement) . Dans .NET , ce sont nouvellement disponibles en intrinstics x64 JIT, émis pour les accélération matérielle types et (voir ici pour plus d' informations). La version générique est définie dans le paquet, qui reste actuellement en cours de développement assez actif. Comme illustré ci - dessous, vous aussi voudrez probablement inclure le paquet, car il est préférable technique de chargement / stockage SIMD recommandé par les auteurs.PUNPCKLBW PUNPCKHBWPACKUSWBSystem.NumericsVectorVector<T>Vector<T>System.Numerics.VectorsSystem.Runtime.CompilerServices.UnsafeVector<T>

L'accélération SIMD correspondante est activée uniquement pour les processeurs capables en mode x64 , mais sinon .NET fournit fallback transparent à l' émulation de code dans la System.Numerics.Vectorsbibliothèque, de sorte que le code démontré ici ne fait fonctionner de manière fiable à travers l'écosystème .NET plus large, peut - être avec des performances réduites. Pour tester le code ci - dessous, j'ai utilisé une application de la console sur le plein .NET Framework 4.7.2 ( "bureau") en x64 (SIMD) et x86 (émulé) modes.

Depuis que je ne voudrais pas priver personne de l'occasion d'apprendre les techniques pertinentes, je vais utiliser Vector.Widenpour illustrer la byte[]à char[]direction C # 7 . De cet exemple, faire le reverse-- -à- dire , en utilisant Vector.Narrowpour mettre en œuvre la direction de rétrécissement - est simple et est laissé comme un exercice pour le lecteur.

Avertissement:
Les méthodes proposées ici sont entièrement codage pascourant, ils dépouillent simplement / étendre - ou étroite / élargir - octets bruts à /partir octets bruts sans égard pourcartographie de caractères,encodage de texte, ouautres propriétés linguistiques. Lorsqueélargissement,octets excédentaires sont mis à zéro, et lorsquerétrécissement,octetsexcès sont éliminés.

D' autres ont discuté des nombreux risques associés à cette pratique sur cette page et ailleurs, donc s'il vous plaît soigneusement examiner et de comprendre la nature de cette opération avant de se demander si elle est adaptée à votre situation. Pour plus de clarté, la validation en ligne est éludée de l'exemple de code ci - dessous, mais ce pourrait être ajouté à la boucle interne avec un impact minimal sur le bénéfice SIMD.

Vous avez été averti . Bien que pas SIMD accéléré, les techniques canoniques en utilisant un câble par Encodingexemple sont recommandés pour les scénarios d'applications presque tous réalistes . Bien que l'OP ne fait demander spécifiquement la performance maximale (ou plus flagrante, comme un effort de la dernière chance pour inoculer contre downvotes par la police de dénigrement), à côté , je résume correctement les techniques appropriées, qui devraient normalement sanctionnés être utilisés.

Pour élargir un tableau d'octets à un .NET String, invoquer le GetString () méthode sur une instance codant orienté octet approprié:

String Encoding.ASCII.GetString(byte[] bytes)

Pour affiner un .NET Stringà un (par exemple, Ascii) du tableau d'octets, invoquer les GetBytes () méthode sur une instance codant orienté octet approprié:

byte[] Encoding.ASCII.GetBytes(char[] chars)


Ok, maintenant à la partie amusante - SIMD compatible extrêmement rapide ( « vectorisé ») C # code pour l' élargissement « muet » d'un tableau d'octets. Pour rappel, voici quelques dépendances qui devraient être mentionnés:

// ... 
using System.Numerics;                  // nuget: System.Numerics.Vectors
using System.Runtime.CompilerServices;  // nuget: System.Runtime.CompilerServices.Unsafe
// ... 

Voici la fonction enveloppe de point d'entrée publique. Si vous préférez une version qui retourne au char[]lieu de String, il est prévu à la fin de ce post.

/// <summary>
/// 'Widen' each byte in 'bytes' to 16-bits with no consideration for
/// character mapping or encoding.
/// </summary>
public static unsafe String ByteArrayToString(byte[] bytes)
{
    // note: possible zeroing penalty; consider buffer pooling or 
    // other ways to allocate target?
    var s = new String('\0', bytes.Length);

    if (s.Length > 0)
        fixed (char* dst = s)
        fixed (byte* src = bytes)
            widen_bytes_simd(dst, src, s.Length);
    return s;
}

Vient ensuite le corps principal de la boucle de travail. Notez la boucle de prologue qui aligne la destination à une limite de mémoire de 16 octets, si nécessaire, par la copie par octet jusqu'à 15 octets de source. Cela garantit le fonctionnement le plus efficace du principal « quad-quadwise boucle » qui, avec une seule paire de SIMD PUNPCKLBW/PUNPCKHBWinstructions, écrit 32 octets à la fois (16 octets de source sont extraites et stockées sous forme de 16 caractères larges occupant 32 octets). Pré-alignement, ainsi que le choix de dst alignement (par opposition à src ) sont des recommandations officielles du manuel Intel précité. De même, le fonctionnement aligné implique que , lorsque la boucle principale se termine, la source peut contenir jusqu'à 15 octets résiduels de suivi; ceux - ci sont terminés par une courte boucle épilogue.

static unsafe void widen_bytes_simd(char* dst, byte* src, int c)
{
    for (; c > 0 && ((long)dst & 0xF) != 0; c--)
        *dst   = (char)*src  ;

    for (; (c -= 0x10) >= 0; src  = 0x10, dst  = 0x10)
        Vector.Widen(Unsafe.AsRef<Vector<byte>>(src),
                     out Unsafe.AsRef<Vector<ushort>>(dst   0),
                     out Unsafe.AsRef<Vector<ushort>>(dst   8));

    for (c  = 0x10; c > 0; c--)
        *dst   = (char)*src  ;
}

C'est en fait tout ce qu'il ya à faire! Il fonctionne comme un charme et, comme vous le verrez ci - dessous, il ne « cri » comme annoncés .

Mais d' abord, en désactivant l'option de débogage de vs2017 « Désactiver les optimisations JIT, » nous pouvons examiner le flux d'instructions natif SIMD que le x64 JIT génère pour la « libération » construire sur .NET 4.7.2 . Voici la partie pertinente de la boucle principale intérieure qui fait sauter à travers les données de 32 octets à la fois. Notez que le JIT a réussi à émettre le fetch / modèle de magasin théoriquement minimal.

L_4223  mov         rax,rbx  
L_4226  movups      xmm0,xmmword ptr [rax] ; fetch 16 bytes
L_4229  mov         rax,rdi  
L_422C  lea         rdx,[rdi 10h]  
L_4230  movaps      xmm2,xmm0  
L_4233  pxor        xmm1,xmm1  
L_4237  punpcklbw   xmm2,xmm1               ; interleave 8-to-16 bits (lo)
L_423B  movups      xmmword ptr [rax],xmm2  ; store 8 bytes (lo) to 8 wide chars (16 bytes)
L_423E  pxor        xmm1,xmm1  
L_4242  punpckhbw   xmm0,xmm1               ; interleave 8-to-16 bits (hi)
L_4246  movups      xmmword ptr [rdx],xmm0  ; store 8 bytes (hi) to 8 wide chars (16 bytes)
L_4249  add         rbx,10h  
L_424D  add         rdi,20h  
L_4251  add         esi,0FFFFFFF0h  
L_4254  test        esi,esi  
L_4256  jge         L_4223  
L_4258  ...

Les résultats des tests de performance:
Je l' ai testé le code SIMD contre quatre autres techniques qui remplissent la même fonction. Pour les codeurs .NET listés ci - dessous, ce fut un appel à la GetChars(byte[], int, int)méthode.

  • naïve implémentation C # d'une boucle dangereuse par octet
  • encodage .NET pour le « Windows 1252 » codepage
  • encodage .NET pour ASCII
  • codant pour .NET UTF-8 (pas de nomenclature, aucune projection)
  • Code SIMD indiqué dans cet article

L'essai comprenait un travail identique pour tous et la validation des résultats identiques de toutes les unités en cours de test. Octets de test étaient aléatoires et ASCII uniquement ( [0x01 - 0x7F]) afin d'assurer un résultat identique de toutes les unités de test. Taille d'entrée était 1Mo aléatoire, au maximum, avec un log 2 biais vers les petites tailles telles que la taille moyenne était d' environ 80K.

Pour l'équité, l'ordre d'exécution a été systématiquement mis en rotation à travers les 5 unités pour chaque itération. Pour le préchauffage, les horaires ont été rejetés et remis à zéro une fois, à l'itération 100. Le faisceau d'essai ne réalise pas de répartition pendant la phase de test et un GC complet est forcé et attendues chaque 10000 itérations.

                 tiques relative, normalisée à meilleur résultat
                  .NET Framework 4.7.3056.0 x64 (communiqué)
   iter naïve gagnant-1252 ascii utf-8 SIMD
------- ----------- ------------ ------------ -------- ---- -----------
  10000 | 294,5 186,2 145,6 131,5 100,0
  20000 | 305,3 191,9 149,4 137,7 100,0
  30000 | 308,5 195,8 151,5 139,2 100,0
  40000 | 312,1 198,5 153,2 141,8 100,0
  50000 | 313,8 199,1 154,1 142,0 100,0
  60000 | 310,6 196,7 153,0 140,5 100,0
  70000 | 312,9 197,3 153,6 141,1 100,0
  80000 | 313,7 197,8 154,1 141,6 100,0
  90000 | 313,7 197,9 154,3 141,3 100,0
 100000 | 313,3 196,9 153,7 141,1 100,0

gcServer = False; LatencyMode.Interactive; Vector.IsHardwareAccelerated = True

Sur le préféré x64 plate - forme lorsque l' optimisation JIT est activé et SIMD est disponible, il n'y avait pas de concours. Le code SIMD gère environ 150% plus rapide que le prochain candidat. Le Encoding.Default, qui est généralement le « Windows 1252 » codepage, particulièrement mal réalisée, environ 3 fois plus lent que le code SIMD.

Plus tôt je l' ai mentionné que la distribution des tailles de données de test a été fortement log-biaisée vers zéro. Sans cette étape - ce qui signifie une distribution uniforme des tailles de 0 à 1048576 octets (taille moyenne de test 512K) - SIMD continue de dépasser le pack avec toutes les autres unités porte relativement pire par rapport au code ci - dessus.

naïf 153,45%
gagnant-1252 358,84%
ascii 221,38%
utf-8 161,62%
SIMD 100,00%

En ce qui concerne le cas, UTF-8 et SIMD non SIMD (émulation) sont extrêmement proches - généralement dans les 3-4% de l'autre - et beaucoup mieux que le reste. J'ai trouvé que ce résultat est doublement surprenant: que le code source de UTF8Encoding était si rapide (beaucoup d'optimisation chemin rapide), puis que le but général SIMD code d'émulation a été en mesure de faire correspondre ce code à l' écoute à cet effet.




Addenda:

Dans le code ci - dessus, je l' ai déjà possible un O ( n pénalité de performance) (associé à l' excès de remise à zéro) d'utiliser le new String(Char,int)constructeur pour allouer la chaîne cible. Pour être complet, voici un point d'entrée alternative qui pourrait éviter le problème en lieu de renvoyer les données élargies comme ushort[]:

/// <summary>
/// 'Widen' each byte in 'bytes' to 16-bits with no consideration for
/// character mapping or encoding
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe char[] WidenByteArray(byte[] bytes)
{
    var rgch = new char[bytes.Length];
    if (rgch.Length > 0)
        fixed (char* dst = rgch)
        fixed (byte* src = bytes)
            widen_bytes_simd(dst, src, rgch.Length);
    return rgch;
}
Créé 25/07/2018 à 07:17
source utilisateur

voix
1

Sans la moindre idée de vos besoins d'accès concurrentiel (ou toute autre chose): Pouvez-vous à donner naissance à des fils sur le ThreadPool qui convertissent les chaînes en tableaux d'octets et déposez-les dans une file d'attente, et d'avoir un plus de fil à regarder la file d'attente et l'envoi des données?

Créé 27/08/2009 à 01:44
source utilisateur

voix
1

Qu'est-ce que vous essayez d'optimiser pour? CPU? La bande passante?

Si vous êtes d'optimiser la bande passante, vous pouvez essayer de compresser les données de chaîne au préalable.

Tout d'abord, le profil de votre code, savoir ce que les bits sont lents, avant d'essayer d'optimiser à un niveau si bas.

Créé 26/08/2009 à 23:40
source utilisateur

voix
1

J'imagine la fonction GetBytes () est déjà bien optimisé pour cela. Je ne peux pas penser à des suggestions pour améliorer la vitesse de votre code existant.

EDIT - Vous savez, je ne sais pas si cela est plus rapide ou non. Mais voici une autre méthode utilisant la BinaryFormatter:

BinaryFormatter bf = new BinaryFormatter();
MemoryStream ms = new MemoryStream();
bf.Serialize(ms, someString);
byte[] bytes =  ms.ToArray();
ms.Close();
socket.Send(bytes);

La raison pour laquelle je pense que cela pourrait être plus rapide est qu'il saute l'étape de codage. Je suis également pas tout à fait sûr que cela fonctionnera correctement. Mais vous pouvez essayer et voir. Bien sûr, si vous avez besoin de l'encodage ascii alors ce ne sera pas utile.

Je viens d'avoir une autre pensée. Je crois que ce code reviendrait double du nombre d'octets que d'utiliser GetBytes avec le codage ASCII. La raison en est que toutes les chaînes dans .NET utilisent unicode dans les coulisses. Et bien sûr Unicode utilise 2 octets par caractère, alors que ASCII utilise seulement 1. Ainsi, le BinaryFormatter est probablement pas la chose à utiliser dans ce cas parce que vous seriez doubler la quantité de données que vous envoyez sur le socket.

Créé 26/08/2009 à 23:34
source utilisateur

voix
0

Juste une autre astuce: Je ne sais pas comment vous créez vos chaînes initiales, mais rappelez-vous que StringBuilder.Append ( « quelque chose ») est vraiment plus rapide que quelque chose comme myString + = « quelque chose ».

Dans le processus de création des chaînes, et de les envoyer via une connexion socket, je serais surprized si le goulot d'étranglement a été la conversion de chaînes en tableaux d'octets. Mais je suis très intéressé si quelqu'un tester cela avec un profileur.

Ben

Créé 08/10/2009 à 12:47
source utilisateur

voix
0

Je vous suggère de profiler ce que vous faites. Je trouve douteux que la vitesse de la conversion d'une chaîne à un tableau d'octets est un problème plus important dans le débit à la vitesse de la douille elle-même.

Créé 27/08/2009 à 02:48
source utilisateur

voix
0

Comme d' autres l' ont dit, la classe de codage est déjà optimisé pour cette tâche, il sera probablement difficile de le rendre plus rapide. Il y a un micro-optimisation que vous pourriez faire: utiliser Encoding.ASCIIplutôt que new ASCIIEncoding(). Mais comme tout le monde le sait, les micro-optimisations sont mauvaises;)

Créé 26/08/2009 à 23:41
source utilisateur

Cookies help us deliver our services. By using our services, you agree to our use of cookies. Learn more