Почему 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);
}

пример на godbolt.

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 будет хороший выбор.