Array.Copy vs Buffer.BlockCopy


124

І Array.Copy, і Buffer.BlockCopy роблять те ж саме, але BlockCopyспрямовано на швидке копіювання примітивного масиву на рівні байтів, тоді як Copyце реалізація загального призначення. Моє запитання - за яких обставин слід використовувати BlockCopy? Чи слід використовувати його в будь-який час, коли ви копіюєте масиви примітивного типу, або використовувати його потрібно лише в тому випадку, якщо ви кодуєте продуктивність? Чи є щось вроджене небезпечне у використанні Buffer.BlockCopyнад Array.Copy?


3
Не забувайте Marshal.Copy:-). Ну, використовуйте Array.Copyдля посилальних типів, складних типів значень, а якщо тип не змінюється, Buffer.BlockCopyдля "перетворення" між типами значень, байтовими масивами та байт-магією. F.ex. комбінація з StructLayoutдосить потужна, якщо ви знаєте, що робите. Що стосується продуктивності, то, здається, некерований виклик до memcpy/ cpblkє найшвидшим для цього - див. Code4k.blogspot.nl/2010/10/… .
атлас

1
Я робив кілька тестів на тести byte[]. Різниці у версії Release не було. Іноді Array.Copy, іноді Buffer.BlockCopy(трохи) швидше.
Bitterblue

Нова вичерпна відповідь щойно розміщена нижче. Зауважте, що у випадках з невеликими розмірами буфера зазвичай явна копія циклу найкраща.
Спеціальний соус

Я не думаю, що вони роблять завжди те ж саме - ви не можете використовувати Array.Copy для копіювання масиву Ints в масив байт, наприклад
mcmillab

Array.Copyце скоріше спеціалізована версія - наприклад, вона може копіювати тільки ті ж масиви рангів.
astrowalker

Відповіді:


59

Оскільки параметри до Buffer.BlockCopy базуються на байтах, а не на індексах, ви, швидше за все, накрутите ваш код, ніж якщо ви використовуєте Array.Copy, тому я використовував би лише Buffer.BlockCopyв критичному для роботи розділі свого коду.


9
Повністю згоден. Занадто багато місця для помилок у Buffer.BlockCopy. Зробіть це просто і не намагайтеся видавлювати жоден сік зі своєї програми, поки не дізнаєтесь, де знаходиться сік (профілювання).
Стівен

5
Що робити, якщо ви маєте справу з байтом []? Чи є які-небудь нові роботи з BlockCopy?
thecoop

4
@thecoop: якщо ви маєте справу з байтом [], тоді, ймовірно, добре використовувати BlockCopy, якщо тільки визначення "байт" згодом не буде змінено на щось інше, ніж байт, що, ймовірно, мало б негативний вплив на інші частини ваш код все одно. :) Єдиний інший потенційний хит - це те, що BlockCopy просто виконує прямі байти, тому він не бере до уваги нестабільність, але це буде грати лише на машині, що не є Windows, і лише якщо ви вкрутили код у перше місце. Крім того, може бути якась дивна різниця, якщо ви використовуєте моно.
MusiGenesis

6
В моєму власному тестуванні Array.Copy () за своєю продуктивністю дуже схожий на Buffer.BlockCopy (). Buffer.BlockCopy стабільно <10% швидше для мене при роботі з 640 байтовими масивами елементів (що саме мене найбільше цікавить). Але вам слід зробити власне тестування з власними даними, оскільки, ймовірно, це буде змінюватися залежно від даних, типів даних, розмірів масивів тощо. Слід зазначити, що обидва способи приблизно в 3 рази швидше, ніж використання Array.Clone (), і, можливо, на 20 разів швидше, ніж копіювання в циклі for.
Кен Сміт

3
@KevinMiller: UInt16це два байти на елемент. Якщо ви передасте цей масив до BlockCopy разом із кількістю елементів у масиві, звичайно, буде скопійовано лише половину масиву. Щоб це нормально працювало, вам потрібно передати кількість елементів, кратне розміру кожного елемента (2), як параметр довжини. msdn.microsoft.com/en-us/library/… та шукайте INT_SIZEв прикладах.
MusiGenesis

129

Прелюдія

Я приєднуюся до партії пізно, але з 32-кратним переглядом варто це правильно зробити. Більшість кодів мікробного маркування у розміщених відповідях поки що страждають від одного або декількох суворих технічних недоліків, включаючи не переміщення виділень пам'яті з тестових циклів (що вводить суворі артефакти GC), не тестування змінних та детермінованих потоків виконання, розминка JIT, і не відслідковувати мінливість тесту. Крім того, більшість відповідей не перевіряли впливу різних розмірів буфера та різних примітивних типів (стосовно 32-бітної або 64-бітної систем). Щоб вирішити це питання більш всебічно, я підключив його до розробленої нами спеціальної рамки мікробенчмаркінгу, яка скорочує більшість поширених "gotchas" настільки, наскільки це можливо. Тести проводилися в режимі випуску .NET 4.0 як для 32-бітної машини, так і для 64-розрядної машини. Результати були усереднені протягом 20 тестових циклів, в яких кожен цикл мав 1 мільйон випробувань на метод. Тестували примітивні типиbyte(1 байт), int(4 байта) і double(8 байт). Були протестовані три методи: Array.Copy(), Buffer.BlockCopy(), і просто за індексом присвоювання в циклі. Дані тут занадто об'ємні, щоб розміщувати тут, тому я підсумую важливі моменти.

Винос

  • Якщо довжина буфера становить приблизно 75-100 або менше, явна процедура копіювання циклу зазвичай швидша (приблизно на 5%), ніж будь-який Array.Copy()або Buffer.BlockCopy()для всіх 3 примітивних типів, перевірених як на 32-бітних, так і на 64-бітних машинах. Крім того, програма явного копіювання циклу має помітно меншу мінливість у порівнянні з двома альтернативами. Хороші показники майже напевно пояснюються локальністю опор, керованою кешуванням пам’яті процесора L1 / L2 / L3 у поєднанні з накладними викликами методу.
    • Тільки для doubleбуферів на 32-бітних машинах : Явна програма копіювання циклу краща, ніж обидві альтернативи для всіх перевірених розмірів буфера до 100 Кб. Поліпшення на 3-5% краще, ніж інші методи. Це пояснюється тим, що продуктивність Array.Copy()і Buffer.BlockCopy()повністю знижується при проходженні нативного 32-бітової ширини. Тому я припускаю, що такий же ефект буде застосований і до longбуферів.
  • Для розмірів буфера, що перевищують ~ 100, явне копіювання циклу швидко стає набагато повільніше, ніж для інших 2-х методів (за одним лише певним винятком). Відмінність найбільш помітна в тому випадку byte[], коли явне копіювання циклу може ставати 7x або більше повільніше при великих розмірах буфера.
  • Загалом, для всіх 3 -х примітивних типів випробувані і в усіх розмірах буфера, Array.Copy()і Buffer.BlockCopy()виконуються майже однаково. В середньому, Array.Copy()здається, дуже незначний край становить приблизно 2% або менше часу (але 0,2% - 0,5% краще, як правило, типово), хоча Buffer.BlockCopy()іноді його обіграв. З невідомих причин Buffer.BlockCopy()має помітно більшу мінливість внутрішнього тесту, ніж Array.Copy(). Цей ефект не вдалося усунути, незважаючи на те, що я намагався здійснити численні пом'якшення та не маючи діючої теорії про те, чому.
  • Оскільки Array.Copy()це "розумніший", більш загальний та набагато безпечніший метод, окрім того, що він є дещо швидшим та має меншу мінливість в середньому, його слід віддавати перевагу Buffer.BlockCopy()майже у всіх поширених випадках. Єдиний випадок використання, коли Buffer.BlockCopy()буде значно краще, це коли типи значень масиву джерела та призначення відрізняються (як зазначено у відповіді Кен Сміта). Незважаючи на те, що цей сценарій не є загальним, він Array.Copy()може працювати дуже погано через постійне "безпечне" значення виливання типів порівняно з прямим кастингом Buffer.BlockCopy().
  • Додаткові дані від зовнішнього StackOverflow , що Array.Copy()це швидше , ніж Buffer.BlockCopy()для масиву копіювання ж типу можна знайти тут .

Як і в стороні, він також виявляється, що навколо довжини масиву 100, коли .NET це Array.Clear()перший починає бити явний кліринг присвоювання циклу масиву (параметр для false, 0або null). Це відповідає моїм подібним висновкам вище. Ці окремі орієнтири були відкриті в Інтернеті тут: manski.net/2012/12/net-array-clear-vs-arrayx-0-performance
Спеціальний соус

Коли ви говорите розмір буфера; ти маєш на увазі в байтах чи підрахунку елементів?
dmarra

У моїй вище відповіді і "довжина буфера", і "розмір буфера" зазвичай відносяться до кількості елементів.
Спеціальний соус

У мене є приклад, коли мені потрібно часто копіювати близько 8 байт даних у буфер, зчитуючи з джерела, зміщеного на 5 байт. Я виявив, що явна копія циклу є значно швидшою, ніж використовувати Buffer.BlockCopy або 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 Однак якщо розмір копії> ~ 20 байт, явний цикл буде значно повільнішим.
Тод Каннінгем

@TodCunningham, 8 байт даних? Ви маєте на увазі довгий еквівалент? Або відкиньте і скопіюйте один елемент (швидко мертвий), або просто розгорніть цю петлю вручну.
astrowalker

67

Інший приклад, коли це має сенс використовувати, Buffer.BlockCopy()- коли вам забезпечений масив примітивів (скажімо, шорти), і вам потрібно перетворити його в масив байтів (скажімо, для передачі по мережі). Цей метод я часто використовую при роботі зі звуком із Silverlight AudioSink. Він надає зразок як short[]масив, але вам потрібно перетворити його в byte[]масив, коли ви будуєте пакет, який ви подаєте Socket.SendAsync(). Ви можете використовувати BitConverterі повторювати масив по одному, але це набагато швидше (приблизно 20 разів в моєму тестуванні), щоб зробити це:

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

І ця ж хитрість працює і в зворотному напрямку:

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

Це приблизно так близько, як ви наближаєтесь до безпечного C # до такого (void *)типу управління пам'яттю, яке так часто зустрічається у C та C ++.


6
Це класна ідея - ви коли-небудь стикаєтеся з проблемами з витримкою?
Філліп

Так, я думаю, що ви можете зіткнутися з цією проблемою, залежно від вашого сценарію. Мої власні сценарії, як правило, були: (а) мені потрібно перемикатися назад і назад між масивами байтів і короткими масивами на одній машині, або (b) мені трапляється знати, що я надсилаю свої дані машинам тієї ж машини ендіанство, і яким я керую віддалену сторону. Але якщо ви використовували протокол, для якого віддалена машина очікувала надсилання даних у мережевому порядку, а не хост-замовлення, так, такий підхід створить вам проблеми.
Кен Сміт

Кен також має статтю про BlockCopy у своєму блозі: blog.wouldbetheologian.com/2011/11/…
Дрю

4
Зверніть увагу, що оскільки .Net Core 2.1 ви можете це зробити без копіювання. MemoryMarshal.AsBytes<T>або MemoryMarshal.Cast<TFrom, TTo>дозвольте інтерпретувати послідовність одного примітиву як послідовність іншого примітиву.
Тімо

16

На основі мого тестування, продуктивність не є причиною віддавати перевагу Buffer.BlockCopy над Array.Copy. З мого тестування Array.Copy насправді швидше, ніж 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));

Приклад Вихід:

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
Вибачте, що ця відповідь більше коментар, але коментар був занадто довгим. Оскільки консенсус здавався таким, що Buffer.BlockCopy кращий для виконання, я вважав, що всі повинні знати, що я не зміг підтвердити цю консенсус тестуванням.
Кевін

10
Я думаю, що у вашій методиці тестування є проблема. Більшість різниць у часі, які ви зазначаєте, є результатом того, що додаток закручується, кешується, керується JIT, подібними речами. Спробуйте з меншим буфером, але кілька тисяч разів; а потім повторіть весь тест протягом циклу півдесятка разів і зверніть увагу лише на останній пробіг. У моєму власному тестуванні Buffer.BlockCopy () працює, можливо, на 5% швидше, ніж Array.Copy () для 640 байтових масивів. Не набагато швидше, але небагато.
Кен Сміт

2
Я вимірював те саме для конкретної проблеми, я не бачив різниці в продуктивності між Array.Copy () і Buffer.BlockCopy () . Якщо що-небудь, BlockCopy представив небезпечний, який фактично вбив мою програму одним випадком.
gatopeich

1
Так само, як додати Array.Copy довго підтримує позицію джерела, так що ввірваючись у великі байтові масиви, це не викине виключення з діапазону.
Alxwest

2
На основі щойно зробленого тесту ( bitbucket.org/breki74/tutis/commits/… ) я б сказав, що між двома методами, коли ви маєте справу з байтовими масивами, немає різниці між практичними показниками.
Ігор Брейц

4

ArrayCopy розумніший за BlockCopy. Він з'ясовує, як скопіювати елементи, якщо джерело та пункт призначення однакові.

Якщо ми заповнимо масив int з 0,1,2,3,4 і застосуємо:

Array.Copy (масив, 0, масив, 1, array.Length - 1);

ми закінчуємо 0,0,1,2,3, як очікувалося.

Спробуйте це з BlockCopy і ми отримаємо: 0,0,2,3,4. Якщо я призначуarray[0]=-1 після цього, він стає -1,0,2,3,4, як очікувалося, але якщо довжина масиву рівна, як 6, ми отримаємо -1,256,2,3,4,5. Небезпечні речі. Не використовуйте BlockCopy, окрім як для копіювання одного байтового масиву в інший.

Є ще один випадок, коли ви можете використовувати тільки Array.Copy: якщо розмір масиву перевищує 2 ^ 31. Array.Copy має перевантаження з longпараметром розміру. У BlockCopy цього немає.


2
Результати ваших тестів з BlockCopy не є несподіваними. Це тому, що блокова копія намагається копіювати фрагменти даних за один раз, а не один байт за один раз. У 32-бітовій системі вона копіює 4 байти одночасно, у 64-бітній системі вона копіює 8 байтів одночасно.
Фарап

Так очікується невизначена поведінка.
binki

2

Якщо зважити на цей аргумент, якщо не бути обережним, як вони складають цей показник, їх можна було б легко ввести в оману. Я написав дуже простий тест, щоб проілюструвати це. У моєму тесті нижче, якщо я поміняю порядок моїх тестів між початковим Buffer.BlockCopy першим або Array.Копіюйте те, що йде першим, майже завжди є найповільнішим (хоча воно є близьким). Це означає, що з безлічі причин, з якими я не збираюся просто запускати тести, кілька разів один за одним не дасть точних результатів.

Я вдався до підтримки тесту, як у 1000000 спроб кожна для масиву з 1000000 послідовних подвійних. Однак у цьому випадку я нехтую першими 900000 циклами і в середньому решту. У цьому випадку буфер є вищим.

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


5
Я не бачу жодних результатів у вашій відповіді. Будь ласка, включіть консольний вихід.
ToolmakerSteve

0

Просто хочу додати мій тестовий випадок, який знову показує, що BlockCopy не має переваги "PERFORMANCE" над Array.Copy. Вони, схоже, мають однакову продуктивність у режимі випуску на моїй машині (обоє займають близько 66 мс для копіювання 50 мільйонів цілих чисел). У режимі налагодження BlockCopy просто незначно швидше.

    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);
    }

3
Без правопорушень, але ваш тестовий результат не дуже корисний;) Перш за все, "на 20 м швидше" нічого не говорить, не знаючи загального часу. Ви також провели ці два випробування дуже різним чином. У випадку BlockCopy є додатковий виклик методу та розподіл цільового масиву, якого у вашому випадку Array.Copy немає. Завдяки коливанню багатопотокових частот (можливий перемикач завдань, основний перемикач) ви можете легко отримувати різні результати щоразу, коли виконуєте тест.
Зайчик83

@ Bunny83 дякую за коментар. Я трохи змінив місце розташування таймера, яке тепер має дати більш справедливе порівняння. І я трохи здивований, що блоккопіювання зовсім не швидше, ніж array.copy.
stt106
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.