Array.Copy против Buffer.BlockCopy

124

Array.Copy и Buffer.BlockCopy делают одно и то же, но BlockCopyнацелены на быстрое копирование примитивных массивов на уровне байтов, тогда как Copyэто реализация общего назначения. У меня вопрос - при каких обстоятельствах вам следует использовать BlockCopy? Следует ли вам использовать его в любое время, когда вы копируете массивы примитивных типов, или вам следует использовать его только в том случае, если вы кодируете для повышения производительности? Есть ли что-нибудь опасное в использовании Buffer.BlockCopyover Array.Copy?

thecoop
источник
3
Не забывай Marshal.Copy:-). Хорошо, используйте Array.Copyдля ссылочных типов, сложных типов значений и, если тип не меняется, Buffer.BlockCopyдля «преобразования» между типами значений, байтовыми массивами и байтовой магией. F.ex. комбинация с StructLayoutдовольно эффективна, если вы знаете, что делаете. Что касается производительности, кажется, что неуправляемый вызов memcpy/ cpblkявляется самым быстрым для этого - см. Code4k.blogspot.nl/2010/10/… .
atlaste
1
Я провел несколько тестов с расширением byte[]. В версии Release разницы не было. Иногда Array.Copy, иногда Buffer.BlockCopy(немного) быстрее.
Bitterblue
Новый исчерпывающий ответ опубликован ниже. Обратите внимание, что в случаях с небольшими размерами буфера обычно лучше всего использовать явное копирование цикла.
Special Sauce
Я не думаю, что они всегда делают одно и то же - вы не можете использовать Array.Copy для копирования массива Ints, например, в массив байтов
mcmillab
Array.Copyэто скорее специализированная версия - например, она может копировать только те же ранговые массивы.
astrowalker

Ответы:

59

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

MusiGenesis
источник
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.
Ken Smith
3
@KevinMiller: ну, UInt16это два байта на элемент. Если вы передадите этот массив в BlockCopy вместе с количеством элементов в массиве, конечно, будет скопирована только половина массива. Чтобы это работало правильно, вам нужно будет передать количество элементов, умноженное на размер каждого элемента (2), в качестве параметра длины. msdn.microsoft.com/en-us/library/… и выполните поиск INT_SIZEв примерах.
MusiGenesis
129

прелюдия

Я присоединяюсь к вечеринке поздно, но с 32k просмотров, это стоит сделать правильно. Большая часть кода микробенчмаркинга в опубликованных ответах до сих пор страдает одним или несколькими серьезными техническими недостатками, включая отсутствие перемещения выделения памяти из тестовых циклов (что приводит к серьезным артефактам сборки мусора), отсутствие тестирования переменных и детерминированных потоков выполнения, разогрева JIT, и не отслеживая вариабельность внутри теста. Кроме того, в большинстве ответов не проверялось влияние различных размеров буфера и различных типов примитивов (в отношении 32-битных или 64-битных систем). Чтобы ответить на этот вопрос более всесторонне, я подключил его к разработанной мной специальной структуре микробенчмаркинга, которая максимально сокращает большинство распространенных ошибок. Тесты проводились в режиме выпуска .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, явное копирование цикла быстро становится намного медленнее, чем два других метода (за одним только что отмеченным исключением). Разница наиболее заметна при byte[], где явное копирование цикла может стать в 7 раз или более медленнее при больших размерах буфера.
  • В общем, для всех 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
Special Sauce
Когда вы говорите размер буфера; вы имеете в виду в байтах или количестве элементов?
dmarra 08
В моем ответе выше и «длина буфера», и «размер буфера» обычно относятся к количеству элементов.
Special Sauce
У меня есть пример, в котором мне нужно часто копировать около 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
Отличная идея - вы когда-нибудь сталкивались с проблемами с порядком байтов?
Филипп
Да, я думаю, что вы можете столкнуться с этой проблемой, в зависимости от вашего сценария. Мои собственные сценарии, как правило, были либо (а) мне нужно переключаться между байтовыми массивами и короткими массивами на одной машине, либо (б) я знаю, что отправляю свои данные на машины того же endianness, и я контролирую удаленную сторону. Но если вы использовали протокол, для которого удаленная машина ожидала, что данные будут отправляться в сетевом порядке, а не в порядке хоста, да, такой подход вызовет у вас проблемы.
Ken Smith
У Кена также есть статья о BlockCopy в своем блоге: blog.wouldbetheologian.com/2011/11/…
Дрю Ноукс
4
Обратите внимание, что начиная с .Net Core 2.1 вы можете делать это без копирования. MemoryMarshal.AsBytes<T>или MemoryMarshal.Cast<TFrom, TTo>позволить вам интерпретировать вашу последовательность одного примитива как последовательность другого примитива.
Timo
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
Kevin
источник
1
Извините, что этот ответ является скорее комментарием, но он был слишком длинным для комментария. Поскольку все сходились во мнении, что Buffer.BlockCopy лучше подходит для производительности, я подумал, что все должны знать, что я не смог подтвердить этот консенсус с помощью тестирования.
Кевин
10
Думаю, проблема в вашей методологии тестирования. В основном разница во времени, которую вы замечаете, является результатом раскрутки приложения, кэширования самого себя, запуска JIT и тому подобного. Попробуйте это с меньшим размером буфера, но несколько тысяч раз; а затем повторите весь тест в цикле полдюжины раз, обращая внимание только на последний прогон. В моем собственном тестировании Buffer.BlockCopy () работает, возможно, на 5% быстрее, чем Array.Copy () для 640-байтовых массивов. Не намного быстрее, но немного.
Кен Смит
2
Я измерил то же самое для конкретной проблемы, я не увидел разницы в производительности между Array.Copy () и Buffer.BlockCopy () . Во всяком случае, BlockCopy представил unsafey, который фактически убил мое приложение в одном случае.
gatopeich
1
Так же, как добавление Array.Copy поддерживает long для исходной позиции, поэтому при разбиении на большие байтовые массивы он не генерирует исключение вне диапазона.
Alxwest
2
Основываясь на только что проведенном мной тесте ( bitbucket.org/breki74/tutis/commit/… ), я бы сказал, что практической разницы в производительности между двумя методами, когда вы имеете дело с байтовыми массивами, нет.
Игорь Брейц
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 этого нет.

user3523091
источник
2
Результаты ваших тестов с BlockCopy не являются неожиданными. Это потому, что блочное копирование пытается копировать фрагменты данных за раз, а не по одному байту. В 32-битной системе он копирует 4 байта за раз, в 64-битной системе за раз копируется 8 байтов.
Pharap
Так ожидалось неопределенное поведение.
binki
2

Чтобы взвесить этот аргумент, можно легко ввести в заблуждение, если не соблюдать осторожность при создании этого теста. Я написал очень простой тест, чтобы проиллюстрировать это. В моем тесте ниже, если я поменяю порядок своих тестов между запуском Buffer.BlockCopy первым или Array.Copy, тот, который идет первым, почти всегда будет самым медленным (хотя и близким). Это означает, что по ряду причин, по которым я не буду просто запускать тесты несколько раз, например, один за другим не даст точных результатов.

Я прибег к сохранению теста, как есть, с 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 не имеет преимущества «ПРОИЗВОДИТЕЛЬНОСТЬ» по сравнению с Array.Copy. Кажется, что они имеют одинаковую производительность в режиме выпуска на моей машине (для копирования 50 миллионов целых чисел требуется около 66 мсек). В режиме отладки 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);
    }
stt106
источник
3
Без обид, но результат вашего теста не особо полезен;) Во-первых, «на 20 мс быстрее» ничего вам не скажет, если не знать общего времени. Вы также провели эти два теста по-разному. Случай BlockCopy имеет дополнительный вызов метода и выделение вашего целевого массива, которого у вас нет в вашем случае Array.Copy. Из-за колебаний многопоточности (возможное переключение задач, переключение ядра) вы можете легко получать разные результаты каждый раз при выполнении теста.
Bunny83
@ Bunny83 спасибо за комментарий. Я немного изменил расположение таймера, теперь сравнение должно быть более справедливым. И я немного удивлен, что blockcopy вообще не быстрее array.copy.
stt106 05