Array.Copy vs Buffer.BlockCopy

124

Array.Copy i Buffer.BlockCopy robią to samo, ale BlockCopymają na celu szybkie kopiowanie tablic pierwotnych na poziomie bajtów, podczas gdy Copyjest to implementacja ogólnego przeznaczenia. Moje pytanie brzmi - w jakich okolicznościach należy używać BlockCopy? Czy należy go używać w dowolnym momencie podczas kopiowania tablic typów pierwotnych, czy też należy go używać tylko wtedy, gdy kodujesz pod kątem wydajności? Czy jest coś z natury niebezpiecznego w używaniu Buffer.BlockCopywięcej Array.Copy?

thecoop
źródło
3
Nie zapomnij Marshal.Copy:-). Cóż, użyj Array.Copydla typów referencyjnych, złożonych typów wartości i jeśli typ się nie zmienia, Buffer.BlockCopydo „konwersji” między typami wartości, tablicami bajtów i magią bajtów. F.ex. połączenie z StructLayoutjest dość potężne, jeśli wiesz, co robisz. Jeśli chodzi o wydajność, wydaje się, że najszybsze w tym celu wywołanie memcpy/ cpblkjest niezarządzane - patrz code4k.blogspot.nl/2010/10/… .
atlaste
1
Zrobiłem kilka testów porównawczych byte[]. Nie było różnicy w wersji wydania. Czasami Array.Copy, czasami Buffer.BlockCopy(nieco) szybciej.
Bitterblue
Nowa wyczerpująca odpowiedź właśnie została zamieszczona poniżej. Zauważ, że w przypadkach z małymi rozmiarami buforów, jawne kopiowanie w pętli jest zwykle najlepsze.
Special Sauce
Nie sądzę, że zawsze robią to samo - na przykład nie można użyć Array.Copy do skopiowania tablicy Ints do tablicy Bajtów
mcmillab
Array.Copyjest raczej wersją wyspecjalizowaną - na przykład może kopiować tylko te same tablice rangi.
astrowalker

Odpowiedzi:

59

Ponieważ parametry Buffer.BlockCopysą oparte na bajtach, a nie na indeksach, bardziej prawdopodobne jest, że zepsujesz kod, niż gdybyś używał Array.Copy, więc użyłbym tylko Buffer.BlockCopyw sekcji mojego kodu krytycznej dla wydajności.

MusiGenesis
źródło
9
Zgadzać się w zupełności. Jest za dużo miejsca na błędy w Buffer.BlockCopy. Utrzymuj to w prostocie i nie próbuj wyciskać żadnego soku z programu, dopóki nie dowiesz się, gdzie jest sok (profilowanie).
Stephen,
5
A co, jeśli masz do czynienia z bajtem []? Czy są jakieś inne problemy z BlockCopy?
thecoop
4
@thecoop: jeśli masz do czynienia z bajtem [], prawdopodobnie dobrze jest użyć BlockCopy, chyba że definicja "bajtu" zostanie później zmieniona na coś innego niż bajt, co prawdopodobnie miałoby dość negatywny wpływ na inne części Twój kod mimo wszystko. :) Jedynym potencjalnym problemem jest to, że BlockCopy po prostu robi zwykłe bajty, więc nie bierze pod uwagę endianness, ale mogłoby to mieć znaczenie tylko na komputerze innym niż Windows i tylko wtedy, gdybyś schrzanił kod pierwsze miejsce. Ponadto, jeśli używasz mono, może występować dziwna różnica.
MusiGenesis
6
W moich własnych testach Array.Copy () ma bardzo podobną wydajność do Buffer.BlockCopy (). Buffer.BlockCopy jest dla mnie konsekwentnie <10% szybszy, gdy mam do czynienia z tablicami bajtowymi o wielkości 640 elementów (co jest sortowaniem, które najbardziej mnie interesuje). Powinieneś jednak przeprowadzić własne testy z własnymi danymi, ponieważ prawdopodobnie będą się one różnić w zależności od danych, typów danych, rozmiarów tablic i tak dalej. Powinienem zauważyć, że obie metody są około 3x szybsze niż użycie Array.Clone () i być może 20x szybsze niż kopiowanie ich w pętli for.
Ken Smith
3
@KevinMiller: uh, UInt16to dwa bajty na element. Jeśli przekażesz tę tablicę do BlockCopy wraz z liczbą elementów w tablicy, oczywiście tylko połowa tablicy zostanie skopiowana. Aby to działało poprawnie, jako parametr długości należy podać liczbę elementów razy rozmiar każdego elementu (2). msdn.microsoft.com/en-us/library/ ... i wyszukaj INT_SIZEw przykładach.
MusiGenesis
129

Preludium

Dołączam do imprezy późno, ale przy 32 tysiącach wyświetleń warto zrobić to dobrze. Większość kodu mikroznakowania w opublikowanych dotychczas odpowiedziach ma jedną lub więcej poważnych błędów technicznych, w tym brak przenoszenia alokacji pamięci poza pętle testowe (co wprowadza poważne artefakty GC), brak testowania zmiennych a deterministyczne przepływy wykonywania, rozgrzewka JIT, a nie śledzenie zmienności wewnątrztestowej. Ponadto w większości odpowiedzi nie testowano skutków różnych rozmiarów buforów i różnych typów pierwotnych (w odniesieniu do systemów 32-bitowych lub 64-bitowych). Aby bardziej kompleksowo odpowiedzieć na to pytanie, podłączyłem go do opracowanego przeze mnie niestandardowego schematu mikroznakowania, który ogranicza większość typowych „pułapek” w możliwie największym stopniu. Testy przeprowadzono w trybie wydania platformy .NET 4.0 zarówno na komputerze 32-bitowym, jak i 64-bitowym. Wyniki uśredniono z 20 serii testowych, z których każdy miał 1 milion prób na metodę. Testowane były typy prymitywnebyte(1 bajt), int(4 bajty) i double(8 bajtów). Badano trzy sposoby: Array.Copy(), Buffer.BlockCopy()i proste zadanie za indeksowanie w pętli. Dane są zbyt obszerne, aby je tutaj opublikować, więc podsumuję najważniejsze punkty.

Na wynos

  • Jeśli długość twojego bufora wynosi około 75-100 lub mniej, jawna procedura kopiowania w pętli jest zwykle szybsza (o około 5%) niż jeden Array.Copy()lub Buffer.BlockCopy()wszystkie 3 typy pierwotne testowane na komputerach 32-bitowych i 64-bitowych. Ponadto jawna procedura kopiowania w pętli ma zauważalnie mniejszą zmienność wydajności w porównaniu z dwiema alternatywami. Dobra wydajność jest prawie na pewno spowodowana lokalnością odniesienia wykorzystywaną przez buforowanie pamięci CPU L1 / L2 / L3 w połączeniu z brakiem narzutu wywołania metody.
    • Tylko dla doublebuforów na komputerach 32-bitowych : jawna procedura kopiowania w pętli jest lepsza niż obie alternatywy dla wszystkich testowanych rozmiarów buforów do 100 KB. Poprawa jest o 3-5% lepsza niż w przypadku innych metod. Dzieje się tak, ponieważ wydajność Array.Copy()i Buffer.BlockCopy()zostaje całkowicie obniżona po przejściu natywnej 32-bitowej szerokości. Dlatego zakładam, że ten sam efekt miałby również zastosowanie do longbuforów.
  • W przypadku rozmiarów buforów przekraczających ~ 100, jawne kopiowanie w pętli szybko staje się znacznie wolniejsze niż pozostałe 2 metody (z jednym konkretnym wyjątkiem, który właśnie wspomniano). Różnica jest najbardziej zauważalna w byte[]przypadku, gdy jawne kopiowanie w pętli może stać się 7x lub wolniejsze przy dużych rozmiarach buforów.
  • Ogólnie rzecz biorąc, dla wszystkich 3 typów pierwotnych przetestowanych i dla wszystkich rozmiarów buforów Array.Copy()i Buffer.BlockCopy()wykonanych prawie identycznie. Średnio Array.Copy()wydaje się mieć bardzo niewielką przewagę wynoszącą około 2% lub mniej czasu potrzebnego (ale 0,2% - 0,5% lepiej jest typowe), chociaż Buffer.BlockCopy()czasami go pokonuje. Z nieznanych powodów Buffer.BlockCopy()ma zauważalnie większą zmienność wewnątrztestową niż Array.Copy(). Tego efektu nie udało się wyeliminować, mimo że próbowałem wielu środków zaradczych i nie miałem działającej teorii, dlaczego.
  • Ponieważ Array.Copy()jest to „mądrzejsza”, ogólniejsza i znacznie bezpieczniejsza metoda, oprócz tego, że jest bardzo nieznacznie szybsza i średnio ma mniejszą zmienność, powinna być preferowana Buffer.BlockCopy()w prawie wszystkich typowych przypadkach. Jedynym przypadkiem użycia, w którym Buffer.BlockCopy()będzie znacznie lepszy, jest sytuacja, gdy typy wartości tablicy źródłowej i docelowej są różne (jak wskazano w odpowiedzi Kena Smitha). Chociaż ten scenariusz nie jest powszechny, Array.Copy()może tutaj działać bardzo słabo ze względu na ciągłe rzutowanie typu „bezpiecznego” typu wartości w porównaniu z rzutowaniem bezpośrednim Buffer.BlockCopy().
  • Dodatkowe dowody z zewnątrz StackOverflow, które Array.Copy()są szybsze niż w Buffer.BlockCopy()przypadku kopiowania tablic tego samego typu, można znaleźć tutaj .
Sos Specjalny
źródło
Na marginesie, ale także okazuje się, że wokół długości matrycy 100 jest przy NET jest Array.Clear()najpierw zaczyna pokonać wyraźne rozliczeń zadanie pętli tablicy (ustawienie false, 0lub null). Jest to zgodne z moimi podobnymi ustaleniami powyżej. Te oddzielne testy porównawcze zostały odkryte w Internecie tutaj: manski.net/2012/12/net-array-clear-vs-arrayx-0-performance
Special Sauce
Kiedy mówisz rozmiar bufora; masz na myśli w bajtach lub liczbie elementów?
dmarra
W mojej powyższej odpowiedzi zarówno „długość bufora”, jak i „rozmiar bufora” ogólnie odnoszą się do liczby elementów.
Special Sauce
Mam przykład, w którym muszę często kopiować około 8 bajtów danych do odczytu bufora ze źródła przesunięcia o 5 bajtów. Okazało się, że jawna kopia pętli jest znacznie szybsza niż użycie Buffer.BlockCopy lub 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 Jeśli jednak rozmiar kopii> ~ 20 bajtów, jawna pętla jest znacznie wolniejsza.
Tod Cunningham
@TodCunningham, 8 bajtów danych? Masz na myśli długi odpowiednik? Albo rzuć i skopiuj pojedynczy element (bardzo szybko) lub po prostu rozwiń tę pętlę ręcznie.
astrowalker
67

Innym przykładem sensownego użycia Buffer.BlockCopy()jest sytuacja, w której otrzymujesz tablicę prymitywów (powiedzmy, short) i musisz przekonwertować ją na tablicę bajtów (powiedzmy, do transmisji przez sieć). Często używam tej metody, gdy mam do czynienia z dźwiękiem z Silverlight AudioSink. Dostarcza próbkę jako short[]tablicę, ale musisz ją przekonwertować na byte[]tablicę podczas budowania pakietu, do którego przesyłasz Socket.SendAsync(). Możesz użyć BitConverteri iterować po tablicy jeden po drugim, ale jest to o wiele szybsze (około 20x w moich testach), aby zrobić to:

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

Ta sama sztuczka działa również w odwrotnej kolejności:

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

Jest to tak bliskie, jak w bezpiecznym C #, (void *)rodzaju zarządzania pamięcią, które jest tak powszechne w C i C ++.

Ken Smith
źródło
6
To fajny pomysł - czy kiedykolwiek napotkasz problemy z endianizmem?
Phillip
Tak, myślę, że możesz napotkać ten problem, w zależności od twojego scenariusza. Moje własne scenariusze zazwyczaj były następujące: (a) muszę przełączać się między tablicami bajtów i krótkimi tablicami na tej samej maszynie lub (b) wiem, że wysyłam dane do maszyn o tej samej endianness, i które kontroluję po stronie odległej. Ale jeśli używasz protokołu, dla którego zdalna maszyna oczekuje, że dane będą wysyłane w kolejności sieciowej, a nie w kolejności hostów, tak, takie podejście spowodowałoby problemy.
Ken Smith
Ken ma również artykuł o BlockCopy na swoim blogu: blog.wouldbetheologian.com/2011/11/…
Drew Noakes
4
Zauważ, że od .Net Core 2.1 możesz to zrobić bez kopiowania. MemoryMarshal.AsBytes<T>lub MemoryMarshal.Cast<TFrom, TTo>pozwolić ci zinterpretować twoją sekwencję jednego prymitywu jako sekwencję innego prymitywu.
Timo
16

Na podstawie moich testów wydajność nie jest powodem do preferowania Buffer.BlockCopy nad Array.Copy. Z mojego testu Array.Copy jest faktycznie szybszy niż 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));

Przykładowe dane wyjściowe:

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
źródło
1
Przepraszam, że ta odpowiedź była bardziej komentarzem, ale była zbyt długa na komentarz. Ponieważ konsensus wydawał się być taki, że Buffer.BlockCopy jest lepszy pod względem wydajności, pomyślałem, że wszyscy powinni mieć świadomość, że nie byłem w stanie potwierdzić tego konsensusu testami.
Kevin
10
Myślę, że jest problem z twoją metodologią testowania. Większość różnicy czasu, którą zauważasz, jest wynikiem działania aplikacji, buforowania samej siebie, uruchamiania JIT i tym podobnych rzeczy. Spróbuj z mniejszym buforem, ale kilka tysięcy razy; a następnie powtórz cały test w pętli pół tuzina razy i zwróć uwagę tylko na ostatnie uruchomienie. Moje własne testy pokazują, że Buffer.BlockCopy () działa może 5% szybciej niż Array.Copy () dla tablic o wielkości 640 bajtów. Niewiele szybciej, ale trochę.
Ken Smith,
2
Zmierzyłem to samo dla konkretnego problemu, nie widziałem różnicy w wydajności między Array.Copy () i Buffer.BlockCopy () . Jeśli już, BlockCopy wprowadził unsafey, który faktycznie zabił moją aplikację w jednym przypadku.
gatopeich
1
Podobnie jak w przypadku dodania Array.Copy obsługuje długie pozycje źródłowe, więc rozbicie na tablice o dużych bajtach nie spowoduje wyrzucenia wyjątku spoza zakresu.
Alxwest
2
Opierając się na przeprowadzonym właśnie teście ( bitbucket.org/breki74/tutis/commits/ ), powiedziałbym, że nie ma praktycznej różnicy w wydajności między tymi dwiema metodami, gdy mamy do czynienia z tablicami bajtowymi.
Igor Brejc
4

ArrayCopy jest inteligentniejszy niż BlockCopy. Dowiaduje się, jak skopiować elementy, jeśli źródło i miejsce docelowe są tą samą tablicą.

Jeśli wypełnimy tablicę int 0,1,2,3,4 i zastosujemy:

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

zgodnie z oczekiwaniami otrzymujemy 0,0,1,2,3.

Spróbuj tego z BlockCopy i otrzymamy: 0,0,2,3,4. Jeśli przypiszę array[0]=-1po tym, otrzymamy -1,0,2,3,4 zgodnie z oczekiwaniami, ale jeśli długość tablicy jest parzysta, np. 6, otrzymamy -1,256,2,3,4,5. Niebezpieczne rzeczy. Nie używaj BlockCopy inaczej niż do kopiowania jednej tablicy bajtów do drugiej.

Istnieje inny przypadek, w którym możesz użyć tylko Array.Copy: jeśli rozmiar tablicy jest dłuższy niż 2 ^ 31. Array.Copy ma przeciążenie z longparametrem rozmiaru. BlockCopy tego nie ma.

user3523091
źródło
2
Wyniki testów z BlockCopy nie są nieoczekiwane. Dzieje się tak, ponieważ funkcja Block copy próbuje kopiować fragmenty danych na raz, a nie tylko po jednym bajcie. W systemie 32-bitowym kopiuje 4 bajty naraz, w systemie 64-bitowym kopiuje 8 bajtów na raz.
Pharap
Tak oczekiwane niezdefiniowane zachowanie.
binki
2

Aby rozważyć ten argument, jeśli ktoś nie jest ostrożny, w jaki sposób tworzą ten wzorzec, może łatwo zostać wprowadzony w błąd. Aby to zilustrować, napisałem bardzo prosty test. W moim teście poniżej, jeśli zamienię kolejność moich testów między uruchomieniem Buffer.BlockCopy najpierw lub Array.Copy, ten, który idzie pierwszy, jest prawie zawsze najwolniejszy (chociaż jest bliski). Oznacza to, że z wielu powodów, którym nie będę się zajmować, po prostu wielokrotne przeprowadzanie testów, szczególnie jeden po drugim, nie da dokładnych wyników.

Uciekłem się do utrzymania testu, tak jak w przypadku 1000000 prób dla tablicy 1000000 sekwencyjnych podwójnych. Jednak w tym czasie pomijam pierwsze 900000 cykli, a resztę uśredniam. W takim przypadku bufor jest lepszy.

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
źródło
5
Nie widzę żadnych wyników dotyczących czasu w Twojej odpowiedzi. Dołącz wyjście konsoli.
ToolmakerSteve
0

Chcę tylko dodać mój przypadek testowy, który ponownie pokazuje, że BlockCopy nie ma przewagi „PERFORMANCE” w porównaniu z Array.Copy. Wydaje się, że mają taką samą wydajność w trybie wydania na moim komputerze (kopiowanie 50 milionów liczb całkowitych trwa około 66 ms). W trybie debugowania BlockCopy jest tylko nieznacznie szybsze.

    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
źródło
3
Bez obrazy, ale wynik testu nie jest zbyt pomocny;) Przede wszystkim „20 ms szybciej” nic Ci nie mówi, nie znając całkowitego czasu. Te dwa testy również przeprowadziliście w bardzo różny sposób. Przypadek BlockCopy ma dodatkowe wywołanie metody i alokację tablicy docelowej, której nie masz w przypadku Array.Copy. Ze względu na fluktuacje wielowątkowe (możliwy przełącznik zadań, przełącznik rdzeniowy) można łatwo uzyskać różne wyniki za każdym razem, gdy wykonujesz test.
Bunny83
@ Bunny83 dzięki za komentarz. Nieznacznie zmodyfikowałem lokalizację licznika czasu, co powinno teraz dać bardziej sprawiedliwe porównanie. I jestem trochę zdziwiony, że blockcopy wcale nie jest szybsze niż array.copy.
stt106