Подтвердить что ты не робот

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

Ответ 1

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

Я написал С++ SHA-2. В моем тесте я hash 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 по 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, когда вам нужно скопировать фрагменты данных.

Ответ 2

Все компиляторы, которые я знаю, заменят простой 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 будет сильно прорисован.

Ответ 3

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

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

Ответ 4

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

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

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

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

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

Ответ 5

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

Ответ 6

Если вы хотите максимальную производительность копирования, не использовать ни один из них.

Существует много возможностей для оптимизации копирования памяти - даже если вы готовы использовать для этого несколько потоков/ядер. См. Например:

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

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

Ответ 7

Просто небольшое добавление: разница в скорости между 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 */

Ответ 8

Профилирование показывает, что оператор: std::copy() всегда быстрее, чем memcpy() или быстрее - false.

Моя система:

HP-Compaq-dx7500-Microtower 3.13.0-24-общий № 47-Ubuntu SMP Fri 2 мая 23:30:00 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux.

gcc (Ubuntu 4.8.2-19ubuntu1) 4.8.2

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

    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() профиль: main: 21: now: 1422969084: 04859 истек: 2650 us
std:: copy() профиль: main: 27: now: 1422969084: 04862 истекло: 2745 us
memcpy() прошло 44 s std:: copy() прошло 45 секунд

g++ -O3 -o test_stdcopy test_stdcopy.cpp

memcpy() профиль: main: 21: now: 1422969601: 04939 прошло: 2385 us
std:: copy() профиль: main: 28: now: 1422969601: 04941 прошло: 2690 us
memcpy() истек 27 s 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