为什么gcc生成memmove而不是memcpy来复制std :: vector<>?

时间:2016-05-12 00:09:01

标签: c++ performance gcc assembly c++14

使用gcc 5.3,以下示例中的两个函数都会生成对li [[1]] [1] "X" "A" [[2]] [1] "X" "B" [[3]] [1] "X" "C" [[4]] [1] "X" "D" [[5]] [1] "X" "E" [[6]] [1] "X" "X" 的调用。生成lapply(li, function(pair) pair[sample(1:2)])[sample(1:6)] [[1]] [1] "X" "D" [[2]] [1] "B" "X" [[3]] [1] "E" "X" [[4]] [1] "X" "X" [[5]] [1] "X" "A" [[6]] [1] "C" "X"

是不合适的
memmove

Example on godbolt

3 个答案:

答案 0 :(得分:9)

我尝试使用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.

我认为发生的事情是,为复制向量而调用的代码更适用于可以重叠的副本(例如对std::move(?)的调用)。

(我尚未确认汇编列表中显示的memmove来电与__builtin_memmove中的stl_algobase.h来电相对应。我邀请其他任何人跟进这一点。)

根据实施情况,memmove()相对于memcpy()可能有一些开销,但差别很小。可能只是不值得为不能重叠的副本创建特例代码。

答案 1 :(得分:8)

TL; DR GCC未优化对memmovestd::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

啊,我明白了。它使用C库提供的优化memcpy例程。但等一下,这没有意义。 memmovememcpy是两回事!

查看此例程的source code,我们看到几乎没有检查:

  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 [citation needed]

答案 2 :(得分:5)

在这种情况下,实施者使用memmove超过memcpy的理由可能存在缺陷。

memmovememcpy的不同之处在于memmove中的内存区域可能重叠(因此概念上效率稍低)。

memcpy具有两个内存区域不得重叠的约束。

对于向量的复制构造函数,内存区域永远不会重叠,因此可以认为memcpy是更好的选择。