Почему `std :: copy` в 5 раз (!) Медленнее, чем` memcpy` в моей тестовой программе?

Это продолжение к этот вопрос где я разместил эту программу:

#include <algorithm>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <iomanip>
#include <iostream>
#include <vector>
#include <chrono>

class Stopwatch
{
public:
typedef std::chrono::high_resolution_clock Clock;

//! Constructor starts the stopwatch
Stopwatch() : mStart(Clock::now())
{
}

//! Returns elapsed number of seconds in decimal form.
double elapsed()
{
return 1.0 * (Clock::now() - mStart).count() / Clock::period::den;
}

Clock::time_point mStart;
};

struct test_cast
{
int operator()(const char * data) const
{
return *((int*)data);
}
};

struct test_memcpy
{
int operator()(const char * data) const
{
int result;
memcpy(&result, data, sizeof(result));
return result;
}
};

struct test_memmove
{
int operator()(const char * data) const
{
int result;
memmove(&result, data, sizeof(result));
return result;
}
};

struct test_std_copy
{
int operator()(const char * data) const
{
int result;
std::copy(data, data + sizeof(int), reinterpret_cast<char *>(&result));
return result;
}
};

enum
{
iterations = 2000,
container_size = 2000
};

//! Returns a list of integers in binary form.
std::vector<char> get_binary_data()
{
std::vector<char> bytes(sizeof(int) * container_size);
for (std::vector<int>::size_type i = 0; i != bytes.size(); i += sizeof(int))
{
memcpy(&bytes[i], &i, sizeof(i));
}
return bytes;
}

template<typename Function>
unsigned benchmark(const Function & function, unsigned & counter)
{
std::vector<char> binary_data = get_binary_data();
Stopwatch sw;
for (unsigned iter = 0; iter != iterations; ++iter)
{
for (unsigned i = 0; i != binary_data.size(); i += 4)
{
const char * c = reinterpret_cast<const char*>(&binary_data[i]);
counter += function(c);
}
}
return unsigned(0.5 + 1000.0 * sw.elapsed());
}

int main()
{
srand(time(0));
unsigned counter = 0;

std::cout << "cast:      " << benchmark(test_cast(),     counter) << " ms" << std::endl;
std::cout << "memcpy:    " << benchmark(test_memcpy(),   counter) << " ms" << std::endl;
std::cout << "memmove:   " << benchmark(test_memmove(),  counter) << " ms" << std::endl;
std::cout << "std::copy: " << benchmark(test_std_copy(), counter) << " ms" << std::endl;
std::cout << "(counter:  " << counter << ")" << std::endl << std::endl;

}

Я заметил, что по какой-то причине std::copy работает намного хуже чем memcpy. Вывод выглядит так на моем Mac с использованием gcc 4.7.

g++ -o test -std=c++0x -O0 -Wall -Werror -Wextra -pedantic-errors main.cpp
cast:      41 ms
memcpy:    46 ms
memmove:   53 ms
std::copy: 211 ms
(counter:  3838457856)

g++ -o test -std=c++0x -O1 -Wall -Werror -Wextra -pedantic-errors main.cpp
cast:      8 ms
memcpy:    7 ms
memmove:   8 ms
std::copy: 19 ms
(counter:  3838457856)

g++ -o test -std=c++0x -O2 -Wall -Werror -Wextra -pedantic-errors main.cpp
cast:      3 ms
memcpy:    2 ms
memmove:   3 ms
std::copy: 27 ms
(counter:  3838457856)

g++ -o test -std=c++0x -O3 -Wall -Werror -Wextra -pedantic-errors main.cpp
cast:      2 ms
memcpy:    2 ms
memmove:   3 ms
std::copy: 16 ms
(counter:  3838457856)

Как видите, даже при -O3это до 5 раз (!) медленнее чем memcpy.

Результаты похожи на Linux.

Кто-нибудь знает почему?

10

Решение

Это не результаты, которые я получаю:

> g++ -O3 XX.cpp
> ./a.out
cast:      5 ms
memcpy:    4 ms
std::copy: 3 ms
(counter:  1264720400)

Hardware: 2GHz Intel Core i7
Memory:   8G 1333 MHz DDR3
OS:       Max OS X 10.7.5
Compiler: i686-apple-darwin11-llvm-g++-4.2 (GCC) 4.2.1

На коробке Linux я получаю разные результаты:

> g++ -std=c++0x -O3 XX.cpp
> ./a.out
cast:      3 ms
memcpy:    4 ms
std::copy: 21 ms
(counter:  731359744)Hardware:  Intel(R) Xeon(R) CPU E5-2670 0 @ 2.60GHz
Memory:    61363780 kB
OS:        Linux ip-10-58-154-83 3.2.0-29-virtual #46-Ubuntu SMP
Compiler:  g++ (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3
3

Другие решения

я согласен с комментарий @ rici о разработке более значимого эталонного теста, поэтому я переписал ваш тест для эталонного копирования двух векторов, используя memcpy(), memmove(), std::copy() и std::vector оператор присваивания:

#include <algorithm>
#include <iostream>
#include <vector>
#include <chrono>
#include <random>
#include <cstring>
#include <cassert>

typedef std::vector<int> vector_type;

void test_memcpy(vector_type & destv, vector_type const & srcv)
{
vector_type::pointer       const dest = destv.data();
vector_type::const_pointer const src  = srcv.data();

std::memcpy(dest, src, srcv.size() * sizeof(vector_type::value_type));
}

void test_memmove(vector_type & destv, vector_type const & srcv)
{
vector_type::pointer       const dest = destv.data();
vector_type::const_pointer const src  = srcv.data();

std::memmove(dest, src, srcv.size() * sizeof(vector_type::value_type));
}

void test_std_copy(vector_type & dest, vector_type const & src)
{
std::copy(src.begin(), src.end(), dest.begin());
}

void test_assignment(vector_type & dest, vector_type const & src)
{
dest = src;
}

auto
benchmark(std::function<void(vector_type &, vector_type const &)> copy_func)
->decltype(std::chrono::milliseconds().count())
{
std::random_device rd;
std::mt19937 generator(rd());
std::uniform_int_distribution<vector_type::value_type> distribution;

static vector_type::size_type const num_elems = 2000;

vector_type dest(num_elems);
vector_type src(num_elems);

// Fill the source and destination vectors with random data.
for (vector_type::size_type i = 0; i < num_elems; ++i) {
src.push_back(distribution(generator));
dest.push_back(distribution(generator));
}

static int const iterations = 50000;

std::chrono::time_point<std::chrono::system_clock> start, end;

start = std::chrono::system_clock::now();

for (int i = 0; i != iterations; ++i)
copy_func(dest, src);

end = std::chrono::system_clock::now();

assert(src == dest);

return
std::chrono::duration_cast<std::chrono::milliseconds>(
end - start).count();
}

int main()
{
std::cout
<< "memcpy:     " << benchmark(test_memcpy)     << " ms" << std::endl
<< "memmove:    " << benchmark(test_memmove)    << " ms" << std::endl
<< "std::copy:  " << benchmark(test_std_copy)   << " ms" << std::endl
<< "assignment: " << benchmark(test_assignment) << " ms" << std::endl
<< std::endl;
}

Я немного переборщил с C ++ 11 просто для удовольствия.

Вот результаты, которые я получаю на своем 64-битном Ubuntu box с g ++ 4.6.3:

$ g++ -O3 -std=c++0x foo.cpp ; ./a.out
memcpy:     33 ms
memmove:    33 ms
std::copy:  33 ms
assignment: 34 ms

Результаты все вполне сопоставимы! Я получаю сопоставимые времена во всех тестовых случаях, когда я меняю целочисленный тип, например в long longтакже в векторе.

Если мой тест переписывания не будет нарушен, похоже, что ваш собственный тест не выполняет корректное сравнение. НТН!

8

Похоже, ответ таков: gcc может оптимизировать эти конкретные вызовы memmove и memcpy, но не std :: copy. gcc знает о семантике memmove и memcpy, и в этом случае может использовать тот факт, что известен размер (sizeof (int)), чтобы превратить вызов в одну инструкцию mov.

std :: copy реализован в терминах memcpy, но, очевидно, оптимизатору gcc не удается выяснить, что data + sizeof (int) — data точно sizeof (int). Таким образом, эталонный тест вызывает memcpy.

Я получил все это, вызывая GCC с -S и быстро пролистывать вывод; Я мог бы легко ошибиться, но то, что я увидел, похоже на ваши измерения.

Кстати, я думаю, что тест более или менее бессмысленно. Более правдоподобный реальный тест может создать реальную vector<int> src и int[N] dst, а затем сравнивая memcpy(dst, src.data(), sizeof(int)*src.size()) с std::copy(src.begin(), src.end(), &dst),

6

memcpy а также std::copy у каждого есть свое применение, std::copy должен (как указано Cheers ниже) быть таким же медленным, как memmove, потому что нет гарантии, что области памяти будут перекрываться. Это означает, что вы можете очень легко копировать несмежные области (так как он поддерживает итераторы) (подумайте о редко распределенных структурах, таких как связанный список и т. Д.… Даже о пользовательских классах / структурах, которые реализуют итераторы). memcpy только работа по смежным причинам и как таковая может быть сильно оптимизирована.

3

Согласно выводу ассемблера G ++ 4.8.1, test_memcpy:

movl    (%r15), %r15d

test_std_copy:

movl    $4, %edx
movq    %r15, %rsi
leaq    16(%rsp), %rdi
call    memcpy

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

Кстати, Clang 3.4 выдает идентичный код для обоих случаев:

movl    (%r14,%rbx), %ebp
0
По вопросам рекламы ammmcru@yandex.ru
Adblock
detector