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

Pra*_*tic 9 c++ performance assembly gcc c++14

使用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);
}
Run Code Online (Sandbox Code Playgroud)

关于Godbolt的例子.

Kei*_*son 9

我尝试使用g ++ 6.1.0编译这段代码.我完全不确定细节,但我认为memmove调用不是由编译器直接生成的; 相反,它是在实现的代码中<vector>.

当我使用预处理代码时

/o/apps/gcc-6.1.0/bin/g++ -E -std=c++14 c.cpp
Run Code Online (Sandbox Code Playgroud)

我看到两个电话__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.
Run Code Online (Sandbox Code Playgroud)

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

(我还没有确认memmove汇编列表中出现的呼叫对应于__builtin_memmove呼叫stl_algobase.h.我邀请其他人跟进这一点.)

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

  • 有趣的事实:x86 gcc会将`memmove`编译为内联到`rep movsd`*if*[它知道src和dst不重叠(`restrict`)](https://godbolt.org/g/ DNia0o).只有某些`-mtune`设置首先内联memcpy/memmove,例如`-m32 -mtune = intel`(但不是`-mtune = haswell`). (3认同)
  • @Barry:不可以.我建议被调用的代码也用于范围*可以*重叠的情况. (2认同)
  • @dats:可能存在***差异(尽管符合标准的实现可以使用完全相同的代码).问题是,是否值得努力找出在每种情况下使用哪一个.我没有深入研究生成的代码或标题,但我可以想象,为不同的情况共享代码的优势超过了使用`memcpy()`的次要速度优势.如果您正在进行显式调用并且您碰巧知道源和目标不重叠,请务必使用`memcpy()`. (2认同)

use*_*422 9

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;
Run Code Online (Sandbox Code Playgroud)

我故意将变量放在文件范围内,以防止优化它们而不必处理volatile语义.

首先让我们试试:

std::copy(&a[0], &a[5], &b[0]);
Run Code Online (Sandbox Code Playgroud)

随着-O3 -fdump-tree-optimized这变成:

__builtin_memcpy (&b[0], &a[0], 20);
Run Code Online (Sandbox Code Playgroud)

单步执行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  }
Run Code Online (Sandbox Code Playgroud)

等等用memmove吗?!好吧,让我们继续吧.

关于什么:

std::copy(&a[0], &a[5], v2.begin());
Run Code Online (Sandbox Code Playgroud)

好的,这让我们memmove:

int * _2;

<bb 2>:
_2 = MEM[(int * const &)&v2];
__builtin_memmove (_2, &a[0], 20);
Run Code Online (Sandbox Code Playgroud)

如果我们这样做,这反映在组装中-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
Run Code Online (Sandbox Code Playgroud)

啊,我明白了.它使用memcpyC库提供的优化例程.但等一下,这没有意义.memmove并且memcpy是两件不同的事情!

查看此例程的源代码,我们看到几乎没有检查:

  85 #ifndef USE_AS_MEMMOVE
  86         cmp     %dil, %sil
  87         jle     L(copy_backward)
  88 #endif
Run Code Online (Sandbox Code Playgroud)

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)
Run Code Online (Sandbox Code Playgroud)

但是如果我们&v2[0]*v2.data()它代替它就不会叫GLIBC memmove.发生什么了?

好了v2[0],v2.begin()返回迭代器,同时v2.data()返回一个指向内存的直接指针.我认为这是出于某种原因阻止GCC优化memmove成a memcpy.[引证需要]


Ric*_*ges 5

为实现者使用的理由memmovememcpy可能在这种情况下是有缺陷的.

memmove不同之处在于memcpy存储区域memmove可能重叠(因此概念上效率稍低).

memcpy 具有两个存储区域不得重叠的约束.

在向量的复制构造函数的情况下,内存区域永远不会重叠,因此可以认为这memcpy将是更好的选择.