133 votes

Array.Copy vs Buffer.BlockCopy

Array.Copy y Buffer.BlockCopy font tous deux la même chose, mais BlockCopy est destiné à la copie rapide de tableaux primitifs au niveau de l'octet, alors que Copy est l'implémentation à usage général. Ma question est la suivante : dans quelles circonstances devriez-vous utiliser BlockCopy ? Devriez-vous l'utiliser à tout moment lorsque vous copiez des tableaux de type primitif, ou ne devriez-vous l'utiliser que si vous codez pour la performance ? Y a-t-il un danger inhérent à l'utilisation de Buffer.BlockCopy sur Array.Copy ?

3 votes

N'oubliez pas Marshal.Copy :-) . Utilisez Array.Copy pour les types de référence, les types de valeurs complexes et si le type ne change pas, Buffer.BlockCopy pour la "conversion" entre les types de valeurs, les tableaux d'octets et la magie des octets. Par exemple, la combinaison avec StructLayout est assez puissant si vous savez ce que vous faites. En ce qui concerne les performances, il semble qu'un appel non géré à memcpy / cpblk est le plus rapide pour cela - voir code4k.blogspot.nl/2010/10/ .

1 votes

J'ai fait quelques tests de référence avec byte[] . Il n'y avait aucune différence dans la version Release. Parfois, Array.Copy parfois Buffer.BlockCopy (légèrement) plus rapide.

0 votes

Une nouvelle réponse complète vient d'être postée ci-dessous. Notez que dans les cas où la taille du tampon est faible, la copie explicite de la boucle est généralement préférable.

157voto

Special Sauce Points 1341

Prélude

Je me joins à la fête en retard, mais avec 32 000 vues, ça vaut la peine de faire ça bien. La plupart du code de microbenchmarking dans les réponses postées jusqu'à présent souffre d'un ou plusieurs défauts techniques graves, y compris ne pas déplacer les allocations de mémoire hors des boucles de test (ce qui introduit des artefacts GC graves), ne pas tester les flux d'exécution variables par rapport aux flux d'exécution déterministes, le réchauffement JIT, et ne pas suivre la variabilité intra-test. En outre, la plupart des réponses n'ont pas testé les effets de la variation de la taille des tampons et des types de primitives (en ce qui concerne les systèmes 32 ou 64 bits). Pour répondre à cette question de manière plus complète, je l'ai reliée à un cadre de microbenchmarking personnalisé que j'ai développé et qui réduit la plupart des "gotchas" courants dans la mesure du possible. Les tests ont été effectués en mode .NET 4.0 Release sur une machine 32 bits et une machine 64 bits. La moyenne des résultats a été calculée sur 20 cycles de tests, chaque cycle comportant 1 million d'essais par méthode. Les types de primitives testées sont byte (1 octet), int (4 octets), et double (8 octets). Trois méthodes ont été testées : Array.Copy() , Buffer.BlockCopy() et une simple affectation par index dans une boucle. Les données sont trop volumineuses pour être publiées ici, je vais donc résumer les points importants.

Les points à retenir

  • Si la longueur de votre tampon est d'environ 75-100 ou moins, une routine de copie de boucle explicite est généralement plus rapide (d'environ 5%) que l'une ou l'autre des méthodes suivantes Array.Copy() o Buffer.BlockCopy() pour les 3 types de primitives testées sur les machines 32 et 64 bits. De plus, la routine de copie de boucle explicite présente une variabilité des performances nettement inférieure à celle des deux autres solutions. Ces bonnes performances sont presque certainement dues à localité de référence exploité par la mise en cache de la mémoire L1/L2/L3 du processeur en conjonction avec l'absence de surcharge d'appel de méthode.
    • Pour double tampons sur les machines 32 bits uniquement : La routine de copie de boucle explicite est meilleure que les deux alternatives pour toutes les tailles de tampon testées jusqu'à 100k. L'amélioration est de 3 à 5 % supérieure à celle des autres méthodes. Ceci est dû au fait que les performances de Array.Copy() y Buffer.BlockCopy() se dégradent totalement dès que l'on dépasse la largeur native de 32 bits. Je suppose donc que le même effet s'appliquerait à long ainsi que des tampons.
  • Pour des tailles de tampon supérieures à ~100, la copie explicite de boucle devient rapidement beaucoup plus lente que les 2 autres méthodes (avec l'exception particulière que nous venons de noter). La différence est la plus visible avec byte[] La copie explicite des boucles peut devenir 7 fois plus lente, voire plus, lorsque la taille du tampon est importante.
  • En général, pour les 3 types de primitives testées et pour toutes les tailles de tampon, Array.Copy() y Buffer.BlockCopy() ont eu des performances presque identiques. En moyenne, Array.Copy() semble avoir un très léger avantage d'environ 2% ou moins de temps pris (mais 0,2% - 0,5% de mieux est typique), bien que Buffer.BlockCopy() l'ont parfois battu. Pour des raisons inconnues, Buffer.BlockCopy() a une variabilité intra-test nettement plus élevée que celle de Array.Copy() . Cet effet n'a pas pu être éliminé bien que j'aie essayé de multiples mesures d'atténuation et que je n'aie pas de théorie exploitable sur la raison.
  • Parce que Array.Copy() est une méthode plus "intelligente", plus générale et beaucoup plus sûre, en plus d'être très légèrement plus rapide et d'avoir moins de variabilité en moyenne, elle devrait être préférée à Buffer.BlockCopy() dans presque tous les cas courants. Le seul cas d'utilisation où Buffer.BlockCopy() sera nettement meilleure est lorsque les types de valeurs des tableaux source et destination sont différents (comme indiqué dans la réponse de Ken Smith). Bien que ce scénario ne soit pas courant, Array.Copy() peut avoir des performances très médiocres dans ce cas, en raison du moulage continu de types de valeurs "sûrs", par rapport au moulage direct de Buffer.BlockCopy() .
  • Des preuves supplémentaires en dehors de StackOverflow que Array.Copy() est plus rapide que Buffer.BlockCopy() pour la copie de tableau du même type peut être trouvée aquí .

68voto

Ken Smith Points 9165

Un autre exemple de cas où il est judicieux d'utiliser Buffer.BlockCopy() C'est le cas lorsqu'on vous fournit un tableau de primitives (par exemple, des shorts) et que vous devez le convertir en un tableau d'octets (par exemple, pour le transmettre sur un réseau). J'utilise fréquemment cette méthode lorsque je traite de l'audio provenant de l'AudioSink de Silverlight. Elle fournit l'échantillon sous forme de short[] mais vous devez le convertir en un tableau de type byte[] lorsque vous construisez le paquet que vous soumettez à la Commission européenne. Socket.SendAsync() . Vous pouvez utiliser BitConverter et d'itérer dans le tableau un par un, mais il est beaucoup plus rapide (environ 20x dans mes tests) de faire cela :

Buffer.BlockCopy(shortSamples, 0, packetBytes, 0, shortSamples.Length * sizeof(short)).  

Et la même astuce fonctionne aussi en sens inverse :

Buffer.BlockCopy(packetBytes, readPosition, shortSamples, 0, payloadLength);

C'est à peu près ce qui se rapproche le plus, en C# sécurisé, de la méthode (void *) le type de gestion de la mémoire qui est si commun en C et C++.

6 votes

C'est une bonne idée. Avez-vous déjà rencontré des problèmes avec l'endianness ?

0 votes

Oui, je pense que vous pourriez rencontrer ce problème, en fonction de votre scénario. Mes propres scénarios ont généralement été les suivants : (a) j'ai besoin de basculer entre les tableaux d'octets et les tableaux courts sur la même machine, ou (b) je sais que j'envoie mes données à des machines de même endianness, et que je contrôle le côté distant. Mais si vous utilisez un protocole pour lequel la machine distante s'attend à ce que les données soient envoyées dans l'ordre du réseau plutôt que dans l'ordre de l'hôte, oui, cette approche vous poserait des problèmes.

0 votes

Ken a également un article sur BlockCopy sur son blog : blog.wouldbetheologian.com/2011/11/

65voto

MusiGenesis Points 49273

Puisque les paramètres à Buffer.BlockCopy sont basées sur des octets plutôt que sur des index, vous risquez davantage de bousiller votre code que si vous utilisez Array.Copy donc je n'utiliserais que Buffer.BlockCopy dans une section de mon code où les performances sont critiques.

9 votes

Tout à fait d'accord. Il y a trop de place pour l'erreur avec Buffer.BlockCopy. Restez simple, et n'essayez pas d'extraire du jus de votre programme avant de savoir où se trouve le jus (profilage).

5 votes

Et si vous avez affaire à un byte[] ? Y a-t-il d'autres problèmes avec BlockCopy ?

4 votes

@thecoop : si vous avez affaire à un byte[], vous pouvez probablement utiliser BlockCopy, à moins que la définition de "byte" ne soit modifiée ultérieurement en quelque chose d'autre qu'un byte, ce qui aurait probablement un effet assez négatif sur d'autres parties de votre code de toute façon. :) Le seul autre problème potentiel est que BlockCopy ne traite que les octets, et ne prend donc pas en compte l'endianness, mais cela n'interviendrait que sur une machine non-Windows, et seulement si vous aviez foiré le code en premier lieu. De plus, il pourrait y avoir une différence bizarre si vous utilisez mono.

16voto

Kevin Points 1445

D'après mes tests, les performances sont pas une raison de préférer Buffer.BlockCopy à Array.Copy. D'après mes tests, Array.Copy est en fait plus rapide que Buffer.BlockCopy.

var buffer = File.ReadAllBytes(...);

var length = buffer.Length;
var copy = new byte[length];

var stopwatch = new Stopwatch();

TimeSpan blockCopyTotal = TimeSpan.Zero, arrayCopyTotal = TimeSpan.Zero;

const int times = 20;

for (int i = 0; i < times; ++i)
{
    stopwatch.Start();
    Buffer.BlockCopy(buffer, 0, copy, 0, length);
    stopwatch.Stop();

    blockCopyTotal += stopwatch.Elapsed;

    stopwatch.Reset();

    stopwatch.Start();
    Array.Copy(buffer, 0, copy, 0, length);
    stopwatch.Stop();

    arrayCopyTotal += stopwatch.Elapsed;

    stopwatch.Reset();
}

Console.WriteLine("bufferLength: {0}", length);
Console.WriteLine("BlockCopy: {0}", blockCopyTotal);
Console.WriteLine("ArrayCopy: {0}", arrayCopyTotal);
Console.WriteLine("BlockCopy (average): {0}", TimeSpan.FromMilliseconds(blockCopyTotal.TotalMilliseconds / times));
Console.WriteLine("ArrayCopy (average): {0}", TimeSpan.FromMilliseconds(arrayCopyTotal.TotalMilliseconds / times));

Exemple de sortie :

bufferLength: 396011520
BlockCopy: 00:00:02.0441855
ArrayCopy: 00:00:01.8876299
BlockCopy (average): 00:00:00.1020000
ArrayCopy (average): 00:00:00.0940000

1 votes

Désolé que cette réponse soit plus un commentaire, mais elle était trop longue pour un commentaire. Puisque le consensus semblait être que Buffer.BlockCopy était meilleur pour la performance, j'ai pensé que tout le monde devrait être conscient que je n'ai pas été en mesure de confirmer ce consensus avec des tests.

10 votes

Je pense qu'il y a un problème avec votre méthodologie de test. La plupart des différences de temps que vous notez sont le résultat du démarrage de l'application, de sa mise en cache, de l'exécution du JIT, ce genre de choses. Essayez avec un tampon plus petit, mais quelques milliers de fois ; puis répétez l'ensemble du test dans une boucle une demi-douzaine de fois, et ne prêtez attention qu'à la dernière exécution. D'après mes propres tests, Buffer.BlockCopy() est peut-être 5% plus rapide que Array.Copy() pour des tableaux de 640 octets. Pas beaucoup plus rapide, mais un peu.

2 votes

J'ai mesuré la même chose pour un problème spécifique, j'ai pu voir aucune différence de performance entre Array.Copy() et Buffer.BlockCopy() . Si quoi que ce soit, BlockCopy a introduit unsafey qui a tué mon application. en une seule fois.

7voto

user3523091 Points 445

ArrayCopy est plus intelligent que BlockCopy. Elle trouve comment copier les éléments si la source et la destination sont le même tableau.

Si nous remplissons un tableau d'int avec 0,1,2,3,4 et appliquons :

Array.Copy(array, 0, array, 1, array.Length - 1);

on se retrouve avec 0,0,1,2,3 comme prévu.

Essayez ceci avec BlockCopy et nous obtenons : 0,0,2,3,4. Si j'assigne array[0]=-1 après cela, cela devient -1,0,2,3,4 comme prévu, mais si la longueur du tableau est paire, comme 6, nous obtenons -1,256,2,3,4,5. C'est dangereux. N'utilisez pas BlockCopy autrement que pour copier un tableau d'octets dans un autre.

Il existe un autre cas où vous ne pouvez utiliser Array.Copy que si la taille du tableau est supérieure à 2^31. Array.Copy dispose d'une surcharge avec une fonction long paramètre de taille. BlockCopy ne dispose pas de ce paramètre.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X