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

Это лучше использовать 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]);

8 ответов

Я собираюсь пойти против общей мудрости здесь, что 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 когда вам нужно скопировать куски данных вокруг.

Все известные мне компиляторы заменят простые std::copy с memcpy когда это уместно или даже лучше, векторизовать копию так, чтобы она была еще быстрее, чем 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 сам будет сильно векторизован.

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

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

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

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

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

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

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

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

Небольшое дополнение: разница в скорости между 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=1000memcpy() примерно в два раза быстрее, когда оптимизация не включена.

Исходный код (требуется 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 */

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

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

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

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

Профилирование показывает это утверждение: 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 с std:: copy () прошло 45 с

g ++ -O3 -o test_stdcopy test_stdcopy.cpp

memcpy () профиль: главный: 21: сейчас:1422969601:04939 прошло:2385 нас
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 
Другие вопросы по тегам