Почему gcc генерирует memmove вместо memcpy для копирования std:: vector?
С gcc 5.3 обе функции в следующем примере генерируют вызов memmove
. Было бы неуместно генерировать memcpy
?
#include <vector>
int blackhole(const std::vector<int>&);
int copy_vec1(const std::vector<int>& v1) {
const std::vector<int> v2{v1.begin(), v1.end()};
return blackhole(v2);
}
int copy_vec2(const std::vector<int>& v1) {
const auto v2 = v1;
return blackhole(v2);
}
3 ответов
я попытался скомпилировать этот код с помощью g++ 6.1.0. Я не совсем уверен в деталях, но я думаю, что memmove
вызов не генерируется непосредственно компилятором; скорее, он находится в коде, который реализует <vector>
.
когда я предварительно обрабатываю код с помощью
/o/apps/gcc-6.1.0/bin/g++ -E -std=c++14 c.cpp
я вижу два вызова __builtin_memmove
, оба из .../include/c++/6.1.0/bits/stl_algobase.h
. Глядя на этот заголовочный файл, я вижу этот комментарий:
// All of these auxiliary structs serve two purposes. (1) Replace
// calls to copy with memmove whenever possible. (Memmove, not memcpy,
// because the input and output ranges are permitted to overlap.)
// (2) If we're using random access iterators, then write the loop as
// a for loop with an explicit count.
я думаю, что происходит то, что код вызывается для копирование вектора более применимо к копиям, которые can перекрытие (например, вызов std::move
(?)).
(я не подтвердил, что memmove
вызовы, которые появляются в списке сборки, соответствуют __builtin_memmove
звонки stl_algobase.h
. Я приглашаю всех остальных последовать этому примеру.)
в зависимости от реализации, memmove()
может быть некоторые накладные расходы по отношению к memcpy()
, но разница незначительная. Это, наверное, просто не стоило создавать специальный код для копий, которые не могут перекрываться.
TL; DR GCC не оптимизирует вызов memmove
внутри std::copy
. При использовании двух массивов c-стиля это так. Замена &v2[0]
С *v2.data()
позволяет оптимизировать его в memcpy
.
ваш пример довольно шумный, поэтому давайте разберем его:
#include <vector>
#include <algorithm>
int a[5];
int b[5];
std::vector<int> v2;
я намеренно поместил переменные в область файла, чтобы предотвратить их оптимизацию без необходимости иметь дело с volatile
семантика.
сначала давайте попробуйте:
std::copy(&a[0], &a[5], &b[0]);
С -O3 -fdump-tree-optimized
это будет:
__builtin_memcpy (&b[0], &a[0], 20);
шагая через GDB показывает нам:
Breakpoint 1, main () at test.cpp:9
9 std::copy(&a[0], &a[0] + 5, &b[0]);
(gdb) s
std::copy<int*, int*> (__result=0x601080 <b>, __last=0x6010b4, __first=0x6010a0 <a>) at test.cpp:9
9 std::copy(&a[0], &a[0] + 5, &b[0]);
(gdb) s
std::__copy_move_a2<false, int*, int*> (__result=0x601080 <b>, __last=0x6010b4, __first=0x6010a0 <a>) at test.cpp:9
9 std::copy(&a[0], &a[0] + 5, &b[0]);
(gdb) s
std::__copy_move_a<false, int*, int*> (__result=<optimized out>, __last=<optimized out>, __first=<optimized out>) at test.cpp:9
9 std::copy(&a[0], &a[0] + 5, &b[0]);
(gdb) s
std::__copy_move<false, true, std::random_access_iterator_tag>::__copy_m<int> (__result=<optimized out>, __last=<optimized out>,
__first=<optimized out>) at /usr/include/c++/5.3.1/bits/stl_algobase.h:382
382 __builtin_memmove(__result, __first, sizeof(_Tp) * _Num);
(gdb) s
main () at test.cpp:10
10 }
Подождите, он использовал memmove
?! Ладно, продолжим.
о:
std::copy(&a[0], &a[5], v2.begin());
хорошо, что получает нас memmove
:
int * _2;
<bb 2>:
_2 = MEM[(int * const &)&v2];
__builtin_memmove (_2, &a[0], 20);
что отражено в собрании если мы делаем -S
. Шагая через GDB показывает нам процесс:
(gdb)
Breakpoint 1, main () at test.cpp:9
9 {
(gdb) s
10 std::copy(&a[0], &a[5], &v2[0]);
(gdb) s
std::copy<int*, int*> (__result=<optimized out>, __last=0x6010d4, __first=0x6010c0 <a>) at test.cpp:10
10 std::copy(&a[0], &a[5], &v2[0]);
(gdb) s
std::__copy_move_a2<false, int*, int*> (__result=<optimized out>, __last=0x6010d4, __first=0x6010c0 <a>) at test.cpp:10
10 std::copy(&a[0], &a[5], &v2[0]);
(gdb) s
std::__copy_move_a<false, int*, int*> (__result=<optimized out>, __last=<optimized out>, __first=<optimized out>) at test.cpp:10
10 std::copy(&a[0], &a[5], &v2[0]);
(gdb) s
std::__copy_move<false, true, std::random_access_iterator_tag>::__copy_m<int> (__result=<optimized out>, __last=<optimized out>,
__first=<optimized out>) at /usr/include/c++/5.3.1/bits/stl_algobase.h:382
382 __builtin_memmove(__result, __first, sizeof(_Tp) * _Num);
(gdb) s
__memmove_ssse3 () at ../sysdeps/x86_64/multiarch/memcpy-ssse3.S:55
А, понятно. Он использует оптимизированный memcpy
рутина предоставляемых библиотекой Си. Но подождите, это не имеет смысла. memmove
и memcpy
это две разные вещи!
смотреть на!--57-->исходный код для этой рутины мы видим маленькие проверки, разбросанные через:
85 #ifndef USE_AS_MEMMOVE
86 cmp %dil, %sil
87 jle L(copy_backward)
88 #endif
GDB подтверждает, что он рассматривает его как memmove
:
55 mov %rdi, %rax
(gdb) s
61 cmp %rsi, %rdi
(gdb) s
62 jb L(copy_forward)
(gdb) s
63 je L(write_0bytes)
но если заменить &v2[0]
С *v2.data()
он не вызывает GLIBC memmove
. Так что же происходит?
хорошо v2[0]
и v2.begin()
возвращают итераторы, а v2.data()
возвращает прямой указатель на память. Я думаю, что это по какой-то причине мешает GCC оптимизировать memmove
на memcpy
.[править]
обоснование для реализации использовать memmove
над memcpy
может быть ошибочным в этом случае.
memmove
отличается от memcpy
в этом области памяти в memmove
может перекрываться (и поэтому концептуально немного менее эффективно).
memcpy
есть ограничение, что две области памяти не должны перекрываться.
в случае конструктора копирования вектора области памяти никогда не будут перекрываться, поэтому можно утверждать, что memcpy
будет хороший выбор.