Лучше использовать std :: memcpy () или std :: copy () с точки зрения производительности?


163

Лучше использовать, memcpyкак показано ниже, или лучше с std::copy()точки зрения производительности? Зачем?

char *bits = NULL;
...

bits = new (std::nothrow) char[((int *) copyMe->bits)[0]];
if (bits == NULL)
{
    cout << "ERROR Not enough memory.\n";
    exit(1);
}

memcpy (bits, copyMe->bits, ((int *) copyMe->bits)[0]);

Обратите внимание, что charможет быть подписанным или неподписанным, в зависимости от реализации. Если число байтов может быть> = 128, то используйте unsigned charдля своих байтовых массивов. ( (int *)Актерский состав будет также более безопасным (unsigned int *).)
Дэн Бреслау

13
Почему ты не используешь std::vector<char>? Или , так как вы говорите bits, std::bitset?
GManNickG

2
На самом деле, не могли бы вы объяснить мне, что (int*) copyMe->bits[0]делает?
user3728501

4
не уверен, почему что-то, похожее на такой беспорядок с таким небольшим жизненным контекстом, было на +81, но эй. @ user3728501 Я предполагаю, что начало буфера intопределяет его размер, но это похоже на рецепт для катастрофы, определенной реализацией, как и многие другие вещи здесь.
underscore_d

2
Фактически, это (int *)приведение является просто неопределенным поведением, а не определенным реализацией. Попытка сделать наложение шрифтов с помощью приведения нарушает строгие правила псевдонимов и, следовательно, не определяется Стандартом. (Кроме того, в C ++, хотя и не в C, вы не можете вводить каламбур через a union.) Практически единственным исключением является то, что вы конвертируете в вариант char*, но допуск не является симметричным.
underscore_d

Ответы:


207

Я собираюсь пойти против общей мудрости здесь, которая std::copyбудет иметь небольшую, почти незаметную потерю производительности. Я только что сделал тест и обнаружил, что это не соответствует действительности: я заметил разницу в производительности. Однако победителем стал std::copy.

Я написал реализацию C ++ SHA-2. В моем тесте я хэшировал 5 строк, используя все четыре версии SHA-2 (224, 256, 384, 512), и зацикливался 300 раз. Я измеряю время, используя Boost.timer. Этого счетчика 300 циклов достаточно, чтобы полностью стабилизировать мои результаты. Я запускал тест по 5 раз каждый, чередуя memcpyверсию и std::copyверсию. Мой код использует преимущества сбора данных как можно большим количеством фрагментов (многие другие реализации работают с char/ char *, тогда как я работаю с T/ T *(где Tсамый большой тип в пользовательской реализации, который имеет правильное поведение переполнения), поэтому быстрый доступ к памяти на Наибольшие типы, которые я могу, имеют решающее значение для производительности моего алгоритма. Вот мои результаты:

Время (в секундах) для завершения запуска тестов SHA-2

std::copy   memcpy  % increase
6.11        6.29    2.86%
6.09        6.28    3.03%
6.10        6.29    3.02%
6.08        6.27    3.03%
6.08        6.27    3.03%

Общее среднее увеличение скорости std :: copy over memcpy: 2,99%

Мой компилятор - gcc 4.6.3 на Fedora 16 x86_64. Мои флаги оптимизации есть -Ofast -march=native -funsafe-loop-optimizations.

Код для моих реализаций SHA-2.

Я решил провести тест на моей реализации MD5. Результаты были гораздо менее стабильными, поэтому я решил сделать 10 прогонов. Тем не менее, после моих первых нескольких попыток я получил результаты, которые сильно отличались от одного запуска к другому, поэтому я предполагаю, что происходила какая-то активность ОС. Я решил начать все сначала.

Те же настройки компилятора и флаги. Существует только одна версия MD5, и она быстрее, чем SHA-2, поэтому я сделал 3000 циклов на подобном наборе из 5 тестовых строк.

Вот мои последние 10 результатов:

Время (в секундах) до завершения теста MD5

std::copy   memcpy      % difference
5.52        5.56        +0.72%
5.56        5.55        -0.18%
5.57        5.53        -0.72%
5.57        5.52        -0.91%
5.56        5.57        +0.18%
5.56        5.57        +0.18%
5.56        5.53        -0.54%
5.53        5.57        +0.72%
5.59        5.57        -0.36%
5.57        5.56        -0.18%

Общее среднее снижение скорости std :: copy over memcpy: 0,11%

Код для моей реализации MD5

Эти результаты показывают, что есть некоторая оптимизация, которую std :: copy использовал в моих тестах SHA-2, которую std::copyнельзя было использовать в моих тестах MD5. В тестах SHA-2 оба массива были созданы в той же функции, которая вызвала std::copy/ memcpy. В моих тестах MD5 один из массивов был передан функции в качестве параметра функции.

Я провел немного больше тестов, чтобы увидеть, что я могу сделать, чтобы сделать std::copyбыстрее снова. Ответ оказался простым: включите оптимизацию времени ссылки. Это мои результаты с включенным LTO (опция -flto в gcc):

Время (в секундах) завершения теста MD5 с параметром -flto

std::copy   memcpy      % difference
5.54        5.57        +0.54%
5.50        5.53        +0.54%
5.54        5.58        +0.72%
5.50        5.57        +1.26%
5.54        5.58        +0.72%
5.54        5.57        +0.54%
5.54        5.56        +0.36%
5.54        5.58        +0.72%
5.51        5.58        +1.25%
5.54        5.57        +0.54%

Общее среднее увеличение скорости std :: copy over memcpy: 0.72%

Таким образом, за использование, похоже, не снижается производительность std::copy. На самом деле, похоже, увеличение производительности.

Объяснение результатов

Так почему же это может std::copyповысить производительность?

Во-первых, я не ожидал бы, что это будет медленнее для любой реализации, если включена оптимизация встраивания. Все компиляторы встраиваются агрессивно; это, возможно, самая важная оптимизация, поскольку она позволяет выполнять множество других оптимизаций. std::copyможет (и я подозреваю, что все реализации реального мира) обнаруживают, что аргументы легко копируются и что память распределяется последовательно. Это означает, что в худшем случае, когда memcpyэто законно, std::copyдолжно работать не хуже. Тривиальная реализация, от std::copyкоторой зависит, memcpyдолжна соответствовать критериям вашего компилятора «всегда вставляйте это при оптимизации для скорости или размера».

Тем не менее, std::copyтакже хранит больше своей информации. При вызове std::copyфункция сохраняет типы без изменений. memcpyоперирует void *, что отбрасывает практически всю полезную информацию. Например, если я передам массив std::uint64_t, компилятор или разработчик библиотеки могут воспользоваться преимуществами 64-разрядного выравнивания std::copy, но это может оказаться более сложным memcpy. Многие реализации алгоритмов, как эта, работают, сначала работая с невыровненной частью в начале диапазона, затем с выровненной частью, затем с невыровненной частью в конце. Если все это гарантированно выровнено, то код становится проще и быстрее, и предиктору ветвления в вашем процессоре становится проще.

Преждевременная оптимизация?

std::copyнаходится в интересной позиции. Я ожидаю, что это никогда не будет медленнее, memcpyа иногда и быстрее, с любым современным оптимизирующим компилятором. Более того, все, что вы можете memcpy, вы можете std::copy. memcpyне допускает никакого перекрытия в буферах, тогда как std::copyподдерживает перекрытие в одном направлении (с std::copy_backwardдругим направлением перекрытия). memcpyработает только на указатели, std::copyработает на любых итераторы ( std::map, std::vector, std::deque, или мой собственный пользовательский тип). Другими словами, вы должны просто использовать, std::copyкогда вам нужно скопировать куски данных вокруг.


35
Я хочу подчеркнуть, что это не означает, что std::copyэто на 2,99% или 0,72% или -0,11% быстрее, чем memcpyэто время для выполнения всей программы. Тем не менее, я обычно чувствую, что тесты в реальном коде более полезны, чем тесты в фальшивом коде. Вся моя программа получила это изменение в скорости выполнения. Реальные эффекты только двух схем копирования будут иметь большие различия, чем показано здесь, если рассматривать их отдельно, но это показывает, что они могут иметь измеримые различия в реальном коде.
Дэвид Стоун

2
Я хочу не согласиться с вашими выводами, но результаты есть результаты: /. Однако, один вопрос (я знаю, что это было давно, и вы не помните исследования, поэтому просто прокомментируйте, как вы думаете), вы, вероятно, не изучали ассемблерный код;
ST3

2
На мой взгляд, memcpyи std::copyимеет разные реализации, поэтому в некоторых случаях компилятор оптимизирует окружающий код и фактический код копирования памяти как единый фрагмент кода. Другими словами, иногда одно лучше, чем другое, и даже другими словами, решение о том, что использовать, является преждевременной или даже глупой оптимизацией, потому что в каждой ситуации вам приходится проводить новые исследования и, более того, программы обычно разрабатываются, поэтому после некоторые незначительные изменения могут превратиться в преимущество функции над другими.
ST3

3
@ ST3: я бы предположил, что в худшем случае std::copyэто тривиальная встроенная функция, которая вызывается только memcpyтогда, когда это допустимо. Базовое встраивание устранит любую отрицательную разницу в производительности. Я обновлю пост с небольшим объяснением того, почему std :: copy может быть быстрее.
Дэвид Стоун

7
Очень информативный анализ. Re Общее среднее снижение скорости std :: copy over memcpy: 0,11% , хотя число верное, результаты не являются статистически значимыми. 95% -ный доверительный интервал для разности средних составляет (-0,013 с, 0,025), что включает ноль. Как вы отметили, что есть различия от других источников и с вашими данными, вы, вероятно, сказали бы, что производительность такая же. Для справки, два других результата являются статистически значимыми. Вероятность того, что вы увидите разницу во времени в такой экстремальной ситуации, составляет примерно 1 на 100 миллионов (первый) и 1 на 20 000 (последний).
TooTone

78

Все известные мне компиляторы заменят простой std::copyна, memcpyкогда это уместно, или даже лучше, векторизируют копию, чтобы она была еще быстрее, чем a memcpy.

В любом случае: профиль и узнай сам. Разные компиляторы будут делать разные вещи, и вполне возможно, что они не будут делать именно то, что вы просите.

Смотрите эту презентацию по оптимизации компилятора (pdf).

Вот что делает GCC для простого std::copyтипа POD.

#include <algorithm>

struct foo
{
  int x, y;    
};

void bar(foo* a, foo* b, size_t n)
{
  std::copy(a, a + n, b);
}

Вот разборка (только с -Oоптимизацией), показывающая вызов memmove:

bar(foo*, foo*, unsigned long):
    salq    $3, %rdx
    sarq    $3, %rdx
    testq   %rdx, %rdx
    je  .L5
    subq    $8, %rsp
    movq    %rsi, %rax
    salq    $3, %rdx
    movq    %rdi, %rsi
    movq    %rax, %rdi
    call    memmove
    addq    $8, %rsp
.L5:
    rep
    ret

Если вы измените подпись функции на

void bar(foo* __restrict a, foo* __restrict b, size_t n)

затем memmoveстановится memcpyдля небольшого улучшения производительности. Обратите внимание, что memcpyсамо по себе будет сильно векторизовано.


1
Как я могу сделать профилирование. Какой инструмент использовать (в Windows и Linux)?
user576670

5
@ Конрад, ты прав. Но memmoveне должно быть быстрее - скорее, оно должно быть медленнее, потому что оно должно учитывать возможность того, что два диапазона данных перекрываются. Я думаю, std::copyразрешает дублирование данных, и поэтому он должен вызывать memmove.
Чарльз Сальвия

2
@Konrad: Если memmove всегда был быстрее чем memcpy, то memcpy будет вызывать memmove. То, что std :: copy на самом деле может отправлять (если вообще что-то), определяется реализацией, поэтому не стоит упоминать о спецификациях без упоминания о реализации.
Фред Нурк

1
Хотя простая программа для воспроизведения этого поведения, скомпилированная с -O3 в GCC, показывает мне a memcpy. Это заставляет меня верить, что GCC проверяет, есть ли совпадение памяти.
Jweyrich

1
@Konrad: стандарт std::copyпозволяет перекрываться в одном направлении, но не в другом. Начало вывода не может находиться в пределах диапазона ввода, но начало ввода может находиться в пределах диапазона вывода. Это немного странно, потому что порядок назначений определен, и вызов может быть UB, даже если эффект этих назначений в этом порядке определен. Но я полагаю, что ограничение позволяет оптимизировать векторизацию.
Стив Джессоп

24

Всегда использовать std::copyпотому , что memcpyограничивается только C-стиле POD структур, и компилятор, вероятно , заменить вызовы std::copyс , memcpyесли цели, на самом деле POD.

Кроме того, std::copyможет использоваться со многими типами итераторов, а не только с указателями. std::copyявляется более гибким без потери производительности и явным победителем.


Почему вы хотите копировать вокруг итераторов?
Atmocreations

3
Вы копируете не итераторы, а диапазон, определенный двумя итераторами. Например, std::copy(container.begin(), container.end(), destination);скопирует содержимое container(все между beginи end) в буфер, указанный как destination. std::copyне требует, как махинации &*container.begin()или &container.back() + 1.
Дэвид Стоун

16

Теоретически, memcpyможет иметь небольшое , незаметное , бесконечно малое преимущество в производительности, только потому, что оно не имеет тех же требований, что и std::copy. Со страницы руководства memcpy:

Чтобы избежать переполнений, размер массивов, на которые указывают параметры назначения и источника, должен составлять не менее num байтов и не должен перекрываться (для перекрывающихся блоков памяти memmove является более безопасным подходом).

Другими словами, memcpyможно игнорировать возможность перекрытия данных. (Передача перекрывающихся массивов memcpy- неопределенное поведение.) Поэтому memcpyнет необходимости явно проверять это условие, в то время как std::copyего можно использовать, если OutputIteratorпараметр не находится в исходном диапазоне. Обратите внимание, что это не то же самое, что сказать, что исходный диапазон и целевой диапазон не могут перекрываться.

Так как std::copyтребования к нему несколько иные, теоретически он должен быть немного (с чрезмерным акцентом на немного ) медленнее, поскольку он, вероятно, будет проверять наличие перекрывающихся C-массивов или делегировать копирование C-массивов memmove, что необходимо для выполнения чек. Но на практике вы (и большинство профилировщиков), вероятно, даже не обнаружите никакой разницы.

Конечно, если вы не работаете с POD , вы всеmemcpy равно не сможете их использовать .


7
Это верно для std::copy<char>. Но std::copy<int>можно предположить, что его входы являются внутренними. Это будет иметь гораздо большее значение, потому что это влияет на каждый элемент. Перекрытие - это разовая проверка.
MSalters

2
@MSalters, правда, но большинство реализаций, memcpyкоторые я видел, проверяют выравнивание и пытаются копировать слова, а не побайтно.
Чарльз Сальвиа

1
std :: copy () также может игнорировать перекрывающуюся память. Если вы хотите поддерживать перекрывающуюся память, вы должны самостоятельно написать логику для вызова std :: reverse_copy () в соответствующих ситуациях.
Cygon

2
Можно привести противоположный аргумент: при переходе через memcpyинтерфейс он теряет информацию о выравнивании. Следовательно, memcpyдолжен выполнять проверки выравнивания во время выполнения, чтобы обрабатывать невыровненные начала и конца. Эти чеки могут быть дешевыми, но они не бесплатны. Принимая во внимание, что std::copyможно избежать этих проверок и векторизовать. Кроме того, компилятор может доказать, что массивы источника и назначения не перекрываются и снова векторизируются без необходимости выбора между пользователем memcpyи memmove.
Максим Егорушкин

11

Мое правило простое. Если вы используете C ++, предпочитайте библиотеки C ++, а не C :)


40
C ++ был явно разработан для использования библиотек C. Это не был несчастный случай. Часто лучше использовать std :: copy, чем memcpy в C ++, но это не имеет ничего общего с тем, какой из них является C, и такой аргумент обычно является неправильным подходом.
Фред Нурк

2
@FredNurk Обычно вы хотите избежать слабой области C, где C ++ обеспечивает более безопасную альтернативу.
Phil1970

@ Phil1970 Я не уверен, что C ++ намного безопаснее в этом случае. Нам все еще нужно пройти действительные итераторы, которые не выходят за пределы, и т. Д. Я думаю, что возможность использовать std::end(c_arr)вместо c_arr + i_hope_this_is_the_right_number_of elementsбезопаснее? и, возможно, что еще важнее, понятнее. И в этом конкретном случае я бы хотел подчеркнуть следующее: std::copy()он более идиоматичен, более понятен, если типы итераторов изменяются позже, приводит к более четкому синтаксису и т. Д.
underscore_d

1
@underscore_d std::copyбезопаснее, потому что он правильно копирует переданные данные, если они не являются POD-типами. memcpyс удовольствием скопирует std::stringобъект в новое представление побайтно.
Дженс

3

Небольшое дополнение: разница в скорости между memcpy()и std::copy()может варьироваться в зависимости от того, включена оптимизация или нет. С g ++ 6.2.0 и без оптимизаций memcpy()явно выигрывает:

Benchmark             Time           CPU Iterations
---------------------------------------------------
bm_memcpy            17 ns         17 ns   40867738
bm_stdcopy           62 ns         62 ns   11176219
bm_stdcopy_n         72 ns         72 ns    9481749

Когда оптимизация включена ( -O3), все выглядит примерно так же:

Benchmark             Time           CPU Iterations
---------------------------------------------------
bm_memcpy             3 ns          3 ns  274527617
bm_stdcopy            3 ns          3 ns  272663990
bm_stdcopy_n          3 ns          3 ns  274732792

Чем больше массив, тем менее заметен эффект, но даже в N=1000 memcpy()два раза быстрее, когда оптимизация не включена.

Исходный код (требуется Google Benchmark):

#include <string.h>
#include <algorithm>
#include <vector>
#include <benchmark/benchmark.h>

constexpr int N = 10;

void bm_memcpy(benchmark::State& state)
{
  std::vector<int> a(N);
  std::vector<int> r(N);

  while (state.KeepRunning())
  {
    memcpy(r.data(), a.data(), N * sizeof(int));
  }
}

void bm_stdcopy(benchmark::State& state)
{
  std::vector<int> a(N);
  std::vector<int> r(N);

  while (state.KeepRunning())
  {
    std::copy(a.begin(), a.end(), r.begin());
  }
}

void bm_stdcopy_n(benchmark::State& state)
{
  std::vector<int> a(N);
  std::vector<int> r(N);

  while (state.KeepRunning())
  {
    std::copy_n(a.begin(), N, r.begin());
  }
}

BENCHMARK(bm_memcpy);
BENCHMARK(bm_stdcopy);
BENCHMARK(bm_stdcopy_n);

BENCHMARK_MAIN()

/* EOF */

18
Измерение производительности с отключенной оптимизацией ... ну ... в значительной степени бессмысленно ... Если вы заинтересованы в производительности, вы не сможете компилировать без оптимизации.
болов

3
@bolov Не всегда. В некоторых случаях важно иметь относительно быструю отлаживаемую программу.
Желудь

2

Если вам действительно нужна максимальная производительность копирования (чего у вас нет), не используйте ни одну из них .

Можно многое сделать для оптимизации копирования памяти - даже больше, если вы хотите использовать для этого несколько потоков / ядер. Смотрите, например:

Чего не хватает / неоптимально в этой реализации memcpy?

и вопрос, и некоторые ответы предложили варианты реализации или ссылки на реализации.


4
режим педантизации: с обычным предупреждением, что «не использовать ни один из них » означает, что вы доказали, что имеете очень специфическую ситуацию / требование, для которого ни одна стандартная функция, предоставляемая вашей реализацией, не является достаточно быстрой ; в противном случае, меня обычно беспокоит то, что люди, которые еще не доказали это, отвлекаются на преждевременную оптимизацию копирования кода вместо обычно более полезных частей своей программы.
underscore_d

-2

Профилирование показывает это утверждение: std::copy()всегда так быстро, как memcpy()или быстрее, ложно.

Моя система:

HP-Compaq-dx7500-Microtower 3.13.0-24-generic # 47-Ubuntu SMP пт 2 мая 23:30:00 UTC 2014 x86_64 x86_64 x86_64 GNU / Linux.

gcc (Ubuntu 4.8.2-19ubuntu1) 4.8.2

Код (язык: c ++):

    const uint32_t arr_size = (1080 * 720 * 3); //HD image in rgb24
    const uint32_t iterations = 100000;
    uint8_t arr1[arr_size];
    uint8_t arr2[arr_size];
    std::vector<uint8_t> v;

    main(){
        {
            DPROFILE;
            memcpy(arr1, arr2, sizeof(arr1));
            printf("memcpy()\n");
        }

        v.reserve(sizeof(arr1));
        {
            DPROFILE;
            std::copy(arr1, arr1 + sizeof(arr1), v.begin());
            printf("std::copy()\n");
        }

        {
            time_t t = time(NULL);
            for(uint32_t i = 0; i < iterations; ++i)
                memcpy(arr1, arr2, sizeof(arr1));
            printf("memcpy()    elapsed %d s\n", time(NULL) - t);
        }

        {
            time_t t = time(NULL);
            for(uint32_t i = 0; i < iterations; ++i)
                std::copy(arr1, arr1 + sizeof(arr1), v.begin());
            printf("std::copy() elapsed %d s\n", time(NULL) - t);
        }
    }

g ++ -O0 -o test_stdcopy test_stdcopy.cpp

memcpy () профиль: главный: 21: сейчас: 1422969084: 04859 истек: 2650 us
std :: copy () профиль: main: 27: сейчас: 1422969084: 04862 истек: 2745 us
memcpy () истек 44 s std :: copy ( ) прошло 45 с

g ++ -O3 -o test_stdcopy test_stdcopy.cpp

memcpy () профиль: главный: 21: сейчас: 1422969601: истек 04939: 2385 us
std :: copy () профиль: main: 28: сейчас: 1422969601: 04941 истек: 2690 us
memcpy () прошло 27 с std :: copy ( ) прошло 43 с

Red Alert указал, что код использует memcpy из массива в массив и std :: copy из массива в вектор. Это может быть причиной для более быстрого memcpy.

Так как есть

v.reserve (SizeOf (arr1));

не должно быть никакой разницы в копировании в вектор или массив.

Код исправлен для использования массива в обоих случаях. memcpy еще быстрее:

{
    time_t t = time(NULL);
    for(uint32_t i = 0; i < iterations; ++i)
        memcpy(arr1, arr2, sizeof(arr1));
    printf("memcpy()    elapsed %ld s\n", time(NULL) - t);
}

{
    time_t t = time(NULL);
    for(uint32_t i = 0; i < iterations; ++i)
        std::copy(arr1, arr1 + sizeof(arr1), arr2);
    printf("std::copy() elapsed %ld s\n", time(NULL) - t);
}

memcpy()    elapsed 44 s
std::copy() elapsed 48 s 

1
неправильно, ваше профилирование показывает, что копирование в массив происходит быстрее, чем копирование в вектор. Не по теме.
Red Alert

Я могу ошибаться, но в вашем исправленном примере с memcpy вы не копируете arr2 в arr1, а с помощью std :: copy вы копируете arr1 в arr2? ... Что вы можете сделать, это сделать несколько, чередуя эксперименты (один раз пакет memcpy, один раз пакет std :: copy, затем снова снова с memcopy и т. д., несколько раз). Тогда я бы использовал clock () вместо time (), потому что кто знает, что может делать ваш компьютер в дополнение к этой программе. Только мои два цента, хотя ... :-)
paercebal

7
Таким образом, переключение std::copyс вектора на массив каким-то образом заставило memcpyзанять почти вдвое больше времени? Эти данные весьма подозрительны. Я скомпилировал ваш код, используя gcc с -O3, и сгенерированная сборка одинакова для обоих циклов. Таким образом, любая разница во времени, которую вы наблюдаете на своей машине, является случайной.
Red Alert
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.