Array.Copy et Buffer.BlockCopy

124

Array.Copy et Buffer.BlockCopy font tous deux la même chose, mais BlockCopyvisent à une copie rapide de tableaux primitifs au niveau des octets, alors que Copyc'est l'implémentation à usage général. Ma question est - dans quelles circonstances devriez-vous utiliser BlockCopy? Devez-vous l'utiliser à tout moment lorsque vous copiez des tableaux de types primitifs, ou devriez-vous l'utiliser uniquement si vous codez pour les performances? Y a-t-il quelque chose de intrinsèquement dangereux à utiliser Buffer.BlockCopyover Array.Copy?

thecoop
la source
3
N'oubliez pas Marshal.Copy:-). Eh bien, utilisez Array.Copypour les types de référence, les types de valeurs complexes et si le type ne change pas, Buffer.BlockCopypour la "conversion" entre les types de valeurs, les tableaux d'octets et la magie d'octets. Par ex. la combinaison avec StructLayoutest assez puissante si vous savez ce que vous faites. En ce qui concerne les performances, il semble qu'un appel non géré vers memcpy/ cpblksoit le plus rapide pour cela - voir code4k.blogspot.nl/2010/10/… .
atlaste
1
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 vite.
Bitterblue du
Nouvelle réponse complète juste publiée ci-dessous. Notez que dans les cas de petites tailles de tampon, la copie en boucle explicite est généralement préférable.
Special Sauce
Je ne pense pas qu'ils font toujours la même chose - vous ne pouvez pas utiliser Array.Copy pour copier un tableau d'Ints dans un tableau d'octets par exemple
mcmillab
Array.Copyest plutôt une version spécialisée - par exemple, elle ne peut copier que les mêmes tableaux de rangs.
astrowalker

Réponses:

59

Étant donné que les paramètres Buffer.BlockCopysont basés sur des octets plutôt que sur des index, vous êtes plus susceptible de bousiller votre code que si vous les utilisez Array.Copy, donc je ne les utiliserais que Buffer.BlockCopydans une section critique pour les performances de mon code.

MusiGenesis
la source
9
Entièrement 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 tant que vous ne savez pas où se trouve le jus (profilage).
Stephen
5
Et si vous avez affaire à un octet []? Y a-t-il d'autres pièges avec BlockCopy?
thecoop
4
@thecoop: si vous avez affaire à un octet [], vous pouvez probablement utiliser BlockCopy, à moins que la définition de "octet" ne soit plus tard modifiée en autre chose qu'un octet, 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 fait que des octets directs, donc il ne prend pas en compte l'endianness, mais cela n'entrerait en jeu que sur une machine non Windows, et seulement si vous aviez foiré le code dans la première place. En outre, il peut y avoir une différence étrange si vous utilisez le mono.
MusiGenesis
6
Dans mes propres tests, Array.Copy () est très similaire en performances à Buffer.BlockCopy (). Buffer.BlockCopy est systématiquement <10% plus rapide pour moi lorsque je traite des tableaux de 640 octets d'éléments (ce qui m'intéresse le plus). Mais vous devez effectuer vos propres tests avec vos propres données, car elles varieront vraisemblablement en fonction des données, des types de données, de la taille des tableaux, etc. Je dois noter que les deux méthodes sont environ 3 fois plus rapides que l'utilisation de Array.Clone (), et peut-être 20 fois plus rapides que de le copier dans une boucle for.
Ken Smith
3
@KevinMiller: euh, UInt16c'est deux octets par élément. Si vous passez ce tableau à BlockCopy avec le nombre d'éléments du tableau, bien sûr, seule la moitié du tableau sera copiée. Pour que cela fonctionne correctement, vous devez passer le nombre d'éléments multiplié par la taille de chaque élément (2) comme paramètre de longueur. msdn.microsoft.com/en-us/library/… et recherchez INT_SIZEdans les exemples.
MusiGenesis
129

Prélude

Je rejoins la fête tardivement, mais avec 32k vues, cela vaut la peine de bien faire les choses. La plupart du code de microbenchmarking dans les réponses publiées jusqu'à présent souffrent d'une ou plusieurs failles techniques graves, notamment le fait de ne pas déplacer les allocations de mémoire hors des boucles de test (ce qui introduit de graves artefacts GC), de ne pas tester les flux d'exécution variables ou déterministes, le préchauffage JIT, et ne pas suivre la variabilité intra-test. En outre, la plupart des réponses ne testaient pas les effets des différentes tailles de tampon et des différents types de primitifs (par rapport aux systèmes 32 bits ou 64 bits). Pour aborder cette question de manière plus complète, je l'ai connectée à un cadre de microbenchmarking personnalisé que j'ai développé et qui réduit la plupart des «pièges» courants dans la mesure du possible. Les tests ont été exécutés en mode de version .NET 4.0 sur une machine 32 bits et une machine 64 bits. Les résultats ont été en moyenne sur 20 séries de tests, dans lesquelles chaque série comportait 1 million d'essais par méthode. Les types primitifs testés étaientbyte(1 octet), int(4 octets) et double(8 octets). Trois méthodes ont été testées: Array.Copy(), Buffer.BlockCopy(), et 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 plats à emporter

  • 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'un Array.Copy()ou l' autre ou Buffer.BlockCopy()pour les 3 types primitifs testés sur les machines 32 bits et 64 bits. De plus, la routine de copie de boucle explicite présente une variabilité de performances nettement inférieure par rapport aux deux alternatives. Les bonnes performances sont presque sûrement dues à la localité de référence exploitée par la mise en cache de la mémoire CPU L1 / L2 / L3 en conjonction avec aucune surcharge d'appel de méthode.
    • Pour les doubletampons sur les machines 32 bits uniquement : la routine de copie en boucle explicite est meilleure que les deux alternatives pour toutes les tailles de tampon testées jusqu'à 100 Ko. L'amélioration est de 3 à 5% meilleure que les autres méthodes. Cela est dû au fait que les performances de Array.Copy()et Buffer.BlockCopy()deviennent totalement dégradées lors du dépassement de la largeur native de 32 bits. Je suppose donc que le même effet s'appliquerait également aux longtampons.
  • Pour des tailles de tampon dépassant ~ 100, la copie en boucle explicite devient rapidement beaucoup plus lente que les 2 autres méthodes (avec la seule exception qui vient d'être notée). La différence est la plus notable avec byte[], où la copie en boucle explicite peut devenir 7x ou plus lente avec de grandes tailles de tampon.
  • En général, pour les 3 types primitifs testés et dans toutes les tailles de tampon, Array.Copy()et Buffer.BlockCopy()effectué presque de manière identique. En moyenne, Array.Copy()semble avoir un très léger avantage d'environ 2% ou moins de temps (mais 0,2% à 0,5% de mieux est typique), bien qu'il l' Buffer.BlockCopy()ait parfois battu. Pour des raisons inconnues, Buffer.BlockCopy()a une variabilité intra-test sensiblement plus élevée que Array.Copy(). Cet effet n'a pas pu être éliminé bien que j'aie essayé de multiples atténuations et que je n'ai pas de théorie exploitable sur le pourquoi.
  • Parce qu'il Array.Copy()s'agit d'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ù ce Buffer.BlockCopy()sera nettement meilleur est celui où les types de valeurs de tableau source et destination sont différents (comme indiqué dans la réponse de Ken Smith). Bien que ce scénario ne soit pas courant, il Array.Copy()peut fonctionner très mal ici en raison de la conversion continue de type valeur «sûre», par rapport à la distribution directe de Buffer.BlockCopy().
  • Des preuves supplémentaires provenant de l'extérieur de StackOverflow qui Array.Copy()sont plus rapides que Buffer.BlockCopy()pour la copie de tableau de même type peuvent être trouvées ici .
Sauce spéciale
la source
En aparté, il se aussi que autour d' une longueur de tableau de 100 est quand est .NET Array.Clear()première commence à battre une compensation d'affectation explicite de la boucle d'un tableau (réglage à false, 0ou null). Ceci est cohérent avec mes conclusions similaires ci-dessus. Ces benchmarks séparés ont été découverts en ligne ici: manski.net/2012/12/net-array-clear-vs-arrayx-0-performance
Special Sauce
Quand vous dites la taille du tampon; voulez-vous dire en octets ou en nombre d'éléments?
dmarra
Dans ma réponse ci-dessus, la «longueur du tampon» et la «taille du tampon» font généralement référence au nombre d'éléments.
Sauce spéciale
J'ai un exemple où j'ai besoin de copier fréquemment environ 8 octets de données dans un tampon lisant à partir d'un décalage de source de 5 octets. J'ai trouvé que la copie de boucle explicite était beaucoup plus rapide que d'utiliser Buffer.BlockCopy ou Array.Copy. Loop Results for 1000000 iterations 17.9515ms. Buffer.BlockCopy Results for 1000000 iterations 39.8937ms. Array.Copy Results for 1000000 iterations 45.9059ms Cependant, si la taille de la copie> ~ 20 octets, la boucle explicite est nettement plus lente.
Tod Cunningham
@TodCunningham, 8 octets de données? Vous voulez dire long équivalent? Soit lancer et copier un élément unique (ultra-rapide) ou simplement dérouler cette boucle manuellement.
astrowalker le
67

Un autre exemple où il est judicieux d'utiliser Buffer.BlockCopy()est lorsque vous êtes fourni avec un tableau de primitives (par exemple, des courts-circuits) et que vous devez le convertir en un tableau d'octets (par exemple, pour une transmission sur un réseau). J'utilise fréquemment cette méthode lorsque je traite l'audio du Silverlight AudioSink. Il fournit l'exemple sous forme de short[]tableau, mais vous devez le convertir en byte[]tableau lorsque vous créez le paquet auquel vous soumettez Socket.SendAsync(). Vous pouvez utiliser BitConverteret parcourir le tableau un par un, mais c'est beaucoup plus rapide (environ 20x dans mes tests) juste pour faire ceci:

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

Et la même astuce fonctionne également en sens inverse:

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

C'est à peu près aussi proche que vous obtenez en C # sécurisé du (void *)type de gestion de la mémoire qui est si courant en C et C ++.

Ken Smith
la source
6
C'est une bonne idée - avez-vous déjà rencontré des problèmes avec l'endian?
Phillip
Ouais, je pense que vous pourriez rencontrer ce problème, selon votre scénario. Mes propres scénarios ont généralement été soit (a) je dois 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 du même endianness, et dont je contrôle le côté distant. Mais si vous utilisiez un protocole pour lequel la machine distante s'attendait à 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.
Ken Smith
Ken a également un article sur BlockCopy sur son blog: blog.wouldbetheologian.com/2011/11/…
Drew Noakes
4
Notez que depuis .Net Core 2.1, vous pouvez le faire sans copier. MemoryMarshal.AsBytes<T>ou MemoryMarshal.Cast<TFrom, TTo>vous permet d'interpréter votre séquence d'une primitive comme une séquence d'une autre primitive.
Timo
16

D'après mes tests, les performances ne sont pas une raison pour 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
Kevin
la source
1
Désolé, cette réponse est plutôt un commentaire, mais elle était trop longue pour un commentaire. Puisque le consensus semblait être que Buffer.BlockCopy était meilleur pour performace, je pensais que tout le monde devrait être conscient que je n'étais pas en mesure de confirmer ce consensus avec des tests.
Kevin
10
Je pense qu'il y a un problème avec votre méthodologie de test. La plupart du décalage horaire que vous notez est le résultat de la rotation de l'application, de la mise en cache, de l'exécution du JIT, ce genre de choses. Essayez-le avec un tampon plus petit, mais quelques milliers de fois; puis répétez le test entier dans une boucle une demi-douzaine de fois, et ne faites attention qu'à la dernière exécution. Mes propres tests ont Buffer.BlockCopy () peut-être 5% plus rapide que Array.Copy () pour les tableaux de 640 octets. Pas beaucoup plus vite, mais un peu.
Ken Smith
2
J'ai mesuré la même chose pour un problème spécifique, je ne pouvais voir aucune différence de performances entre Array.Copy () et Buffer.BlockCopy () . Si quoi que ce soit, BlockCopy a introduit une non-sécurité qui a en fait tué mon application dans un cas.
gatopeich
1
Tout comme pour ajouter Array.Copy prend en charge long pour la position source, donc le fractionnement en grands tableaux d'octets ne lèvera pas d'exception hors plage.
Alxwest
2
Sur la base des tests que je viens de faire ( bitbucket.org/breki74/tutis/commits/… ), je dirais qu'il n'y a pas de différence de performance pratique entre les deux méthodes lorsque vous traitez avec des tableaux d'octets.
Igor Brejc
4

ArrayCopy est plus intelligent que BlockCopy. Il explique comment copier des éléments si la source et la destination sont le même tableau.

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

Array.Copy (tableau, 0, tableau, 1, tableau.Longueur - 1);

nous nous retrouvons avec 0,0,1,2,3 comme prévu.

Essayez ceci avec BlockCopy et nous obtenons: 0,0,2,3,4. Si array[0]=-1j'assigne 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. Des trucs dangereux. N'utilisez BlockCopy que pour copier un tableau d'octets dans un autre.

Il existe un autre cas où vous ne pouvez utiliser Array.Copy: si la taille du tableau est plus longue que 2 ^ 31. Array.Copy a une surcharge avec un longparamètre de taille. BlockCopy n'a pas cela.

user3523091
la source
2
Les résultats de vos tests avec BlockCopy ne sont pas inattendus. C'est parce que Block Copy essaie de copier des morceaux de données à la fois plutôt qu'un octet à la fois. Sur un système 32 bits, il copie 4 octets à la fois, sur un système 64 bits, il copie 8 octets à la fois.
Pharap
Donc comportement indéfini attendu.
binki
2

Pour peser sur cet argument, si l'on ne fait pas attention à la manière dont ils créent ce repère, ils pourraient facilement être induits en erreur. J'ai écrit un test très simple pour illustrer cela. Dans mon test ci-dessous, si je permute l'ordre de mes tests entre le démarrage de Buffer.BlockCopy en premier ou Array.Copy celui qui commence est presque toujours le plus lent (bien que ce soit un proche). Cela signifie que pour un tas de raisons que je n'entrerai pas dans le simple fait d'exécuter les tests plusieurs fois, les uns après les autres ne donneront pas de résultats précis.

J'ai eu recours au maintien du test tel quel avec 1000000 essais chacun pour un tableau de 1000000 doubles séquentiels. Cependant, je néglige ensuite les 900000 premiers cycles et fait la moyenne du reste. Dans ce cas, le tampon est supérieur.

private static void BenchmarkArrayCopies()
        {
            long[] bufferRes = new long[1000000];
            long[] arrayCopyRes = new long[1000000];
            long[] manualCopyRes = new long[1000000];

            double[] src = Enumerable.Range(0, 1000000).Select(x => (double)x).ToArray();

            for (int i = 0; i < 1000000; i++)
            {
                bufferRes[i] = ArrayCopyTests.ArrayBufferBlockCopy(src).Ticks;
            }

            for (int i = 0; i < 1000000; i++)
            {
                arrayCopyRes[i] = ArrayCopyTests.ArrayCopy(src).Ticks;
            }

            for (int i = 0; i < 1000000; i++)
            {
                manualCopyRes[i] = ArrayCopyTests.ArrayManualCopy(src).Ticks;
            }

            Console.WriteLine("Loop Copy: {0}", manualCopyRes.Average());
            Console.WriteLine("Array.Copy Copy: {0}", arrayCopyRes.Average());
            Console.WriteLine("Buffer.BlockCopy Copy: {0}", bufferRes.Average());

            //more accurate results - average last 1000

            Console.WriteLine();
            Console.WriteLine("----More accurate comparisons----");

            Console.WriteLine("Loop Copy: {0}", manualCopyRes.Where((l, i) => i > 900000).ToList().Average());
            Console.WriteLine("Array.Copy Copy: {0}", arrayCopyRes.Where((l, i) => i > 900000).ToList().Average());
            Console.WriteLine("Buffer.BlockCopy Copy: {0}", bufferRes.Where((l, i) => i > 900000).ToList().Average());
            Console.ReadLine();
        }

public class ArrayCopyTests
    {
        private const int byteSize = sizeof(double);

        public static TimeSpan ArrayBufferBlockCopy(double[] original)
        {
            Stopwatch watch = new Stopwatch();
            double[] copy = new double[original.Length];
            watch.Start();
            Buffer.BlockCopy(original, 0 * byteSize, copy, 0 * byteSize, original.Length * byteSize);
            watch.Stop();
            return watch.Elapsed;
        }

        public static TimeSpan ArrayCopy(double[] original)
        {
            Stopwatch watch = new Stopwatch();
            double[] copy = new double[original.Length];
            watch.Start();
            Array.Copy(original, 0, copy, 0, original.Length);
            watch.Stop();
            return watch.Elapsed;
        }

        public static TimeSpan ArrayManualCopy(double[] original)
        {
            Stopwatch watch = new Stopwatch();
            double[] copy = new double[original.Length];
            watch.Start();
            for (int i = 0; i < original.Length; i++)
            {
                copy[i] = original[i];
            }
            watch.Stop();
            return watch.Elapsed;
        }
    }

https://github.com/chivandikwa/Random-Benchmarks

Thulani Chivandikwa
la source
5
Je ne vois aucun résultat dans votre réponse. Veuillez inclure la sortie de la console.
ToolmakerSteve
0

Je veux juste ajouter mon cas de test qui montre à nouveau que BlockCopy n'a aucun avantage 'PERFORMANCE' sur Array.Copy. Ils semblent avoir les mêmes performances en mode release sur ma machine (les deux prennent environ 66 ms pour copier 50 millions d'entiers). En mode débogage, BlockCopy est légèrement plus rapide.

    private static T[] CopyArray<T>(T[] a) where T:struct 
    {
        T[] res = new T[a.Length];
        int size = Marshal.SizeOf(typeof(T));
        DateTime time1 = DateTime.Now;
        Buffer.BlockCopy(a,0,res,0, size*a.Length);
        Console.WriteLine("Using Buffer blockcopy: {0}", (DateTime.Now - time1).Milliseconds);
        return res;
    }

    static void Main(string[] args)
    {
        int simulation_number = 50000000;
        int[] testarray1 = new int[simulation_number];

        int begin = 0;
        Random r = new Random();
        while (begin != simulation_number)
        {
            testarray1[begin++] = r.Next(0, 10000);
        }

        var copiedarray = CopyArray(testarray1);

        var testarray2 = new int[testarray1.Length];
        DateTime time2 = DateTime.Now;
        Array.Copy(testarray1, testarray2, testarray1.Length);
        Console.WriteLine("Using Array.Copy(): {0}", (DateTime.Now - time2).Milliseconds);
    }
stt106
la source
3
Aucune offense mais le résultat de votre test n'est pas vraiment utile;) Tout d'abord "20ms plus vite" ne vous dit rien sans connaître le temps global. Vous avez également effectué ces deux tests de manière très différente. Le cas BlockCopy a un appel de méthode supplémentaire et l'allocation de votre tableau cible que vous n'avez pas dans votre cas Array.Copy. En raison des fluctuations du multithreading (changement de tâche possible, commutateur principal), vous pouvez facilement obtenir des résultats différents à chaque fois que vous exécutez le test.
Bunny83
@ Bunny83 merci pour le commentaire. J'ai légèrement modifié l'emplacement de la minuterie, ce qui devrait maintenant donner une comparaison plus juste. Et je suis un peu surpris que la blockcopy ne soit pas du tout plus rapide que array.copy.
stt106