Array.Copy vs Buffer.BlockCopy

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

Ответ 1

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

Ответ 2

Прелюдия

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

The Takeaways

  • Если длина вашего буфера составляет около 75-100 или менее, то явная процедура копирования цикла обычно выполняется быстрее (примерно на 5%), чем Array.Copy() или Buffer.BlockCopy() для всех трех примитивных типов, протестированных как на 32-битных, так и на 64-битные машины. Кроме того, явная процедура копирования цикла имеет заметно меньшую изменчивость в производительности по сравнению с двумя альтернативами. Хорошая производительность почти наверняка обусловлена ​​локалью ссылки, используемой кэшированием ЦП L1/L2/L3 в сочетании с отсутствием служебных вызовов метода.
    • Для double буферов только на 32-разрядных машинах. Явная процедура копирования циклов лучше, чем обе альтернативы для всех размеров буферов, проверенных до 100 КБ. Улучшение на 3-5% лучше, чем другие методы. Это связано с тем, что производительность Array.Copy() и Buffer.BlockCopy() полностью ухудшается при передаче собственной 32-битной ширины. Таким образом, я предполагаю, что тот же эффект применим и к буферам long.
  • Для размеров буфера, превышающих ~ 100, явное циклическое копирование быстро становится намного медленнее, чем другие 2 метода (с единственным исключением, которое только что было отмечено). Разница наиболее заметна с помощью byte[], где явное циклическое копирование может стать 7x или более медленным при больших размерах буфера.
  • В общем, для всех трех примитивных типов, проверенных и во всех размерах буфера, 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() для копирования массива одного типа, можно найти здесь .

Ответ 3

Другим примером того, когда имеет смысл использовать Buffer.BlockCopy(), является то, когда вам предоставляется массив примитивов (например, shorts) и нужно преобразовать его в массив байтов (скажем, для передачи по сети). Я часто использую этот метод при работе с аудио из 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);

Это примерно так же близко, как и вы, в безопасном С#, к управлению памятью (void *), которое так распространено в C и С++.

Ответ 4

Основываясь на моем тестировании, производительность не является основанием для предпочтения 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

Ответ 5

ArrayCopy умнее BlockCopy. Он определяет, как копировать элементы, если источник и место назначения являются одним и тем же массивом.

Если мы заполняем массив int 0,1,2,3,4 и применяем:

Array.Copy(array, 0, array, 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 этого не делает.

Ответ 6

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

Я прибегал к поддержке теста, как и 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

Ответ 7

Просто хочу добавить свой тестовый пример, который снова показывает, что 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);
    }