Лучше использовать 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%
Эти результаты показывают, что есть некоторая оптимизация, которую 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=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 */
Если вам действительно нужна максимальная производительность копирования (чего у вас нет), не используйте ни одну из них.
Можно многое сделать для оптимизации копирования памяти - даже больше, если вы хотите использовать для этого несколько потоков / ядер. Смотрите, например:
Чего не хватает / неоптимально в этой реализации 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