c++ - 为什么 move ctor 比 copy ctor 慢?

标签 c++ performance c++11 move copy-constructor

我有下面的代码来测试std::string类的copy ctor和move ctor,结果让我吃惊,move ctor慢了~1.4倍比抄袭者。

据我了解, move 构造不需要分配内存,对于std::string的情况, move 构造的对象中可能有一个内部指针直接设置为 move 的对象,它应该比为缓冲区分配内存然后在复制构造时从对象中复制内容更快。

代码如下:

#include <string>
#include <iostream>

void CopyContruct(const std::string &s) {
  auto copy = std::string(s);
}

void MoveContruct(std::string &&s) {
  auto copy = std::move(s);
  //auto copy = std::string(std::move(s));
}

int main(int argc, const char *argv[]) {
  for (int i = 0; i < 50000000; ++i) {
    CopyContruct("hello world");
    //MoveContruct("hello world");
  }

  return 0;
}

编辑:

从两个函数的集合中,我可以看到对于 MoveConstruct 有一个 std::remove_reference 类模板的实例化,我认为这应该是罪魁祸首但我对汇编不熟悉,谁能详细说明一下?

以下代码是在https://godbolt.org/上反编译的使用 x86-64 gcc7.2:

CopyContruct(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
  push rbp
  mov rbp, rsp
  sub rsp, 48
  mov QWORD PTR [rbp-40], rdi
  mov rdx, QWORD PTR [rbp-40]
  lea rax, [rbp-32]
  mov rsi, rdx
  mov rdi, rax
  call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)
  lea rax, [rbp-32]
  mov rdi, rax
  call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string()
  nop
  leave
  ret
MoveContruct(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&&):
  push rbp
  mov rbp, rsp
  sub rsp, 48
  mov QWORD PTR [rbp-40], rdi
  mov rax, QWORD PTR [rbp-40]
  mov rdi, rax
  call std::remove_reference<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&>::type&& std::move<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&>(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&)
  mov rdx, rax
  lea rax, [rbp-32]
  mov rsi, rdx
  mov rdi, rax
  call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&&)
  lea rax, [rbp-32]
  mov rdi, rax
  call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string()
  nop
  leave
  ret

编辑2:

事情变得有趣了,我把 std::string 改成 std::vector 正如@FantasticMrFox 在评论中提到的,结果是相反的, MoveConstructCopyConstruct~1.9 倍,看来 std::remove_reference 不是罪魁祸首,而是优化 这两个类可能是。

编辑3:

以下代码在 MacOS 上使用 Apple LLVM 版本 8.0.0 (clang-800.0.42.1) 编译,优化标志为 -O3。

    .section    __TEXT,__text,regular,pure_instructions
    .macosx_version_min 10, 11
    .globl  __Z12CopyContructRKNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE
    .align  4, 0x90
__Z12CopyContructRKNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE: ## @_Z12CopyContructRKNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE
    .cfi_startproc
## BB#0:
    pushq   %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    pushq   %rbx
    subq    $24, %rsp
Ltmp3:
    .cfi_offset %rbx, -24
    movq    %rdi, %rax
    leaq    -32(%rbp), %rbx
    movq    %rbx, %rdi
    movq    %rax, %rsi
    callq   __ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEC1ERKS5_
    movq    %rbx, %rdi
    callq   __ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEED1Ev
    addq    $24, %rsp
    popq    %rbx
    popq    %rbp
    retq
    .cfi_endproc

    .globl  __Z12MoveContructONSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE
    .align  4, 0x90
__Z12MoveContructONSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE: ## @_Z12MoveContructONSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE
    .cfi_startproc
## BB#0:
    pushq   %rbp
Ltmp4:
    .cfi_def_cfa_offset 16
Ltmp5:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp6:
    .cfi_def_cfa_register %rbp
    subq    $32, %rsp
    movq    16(%rdi), %rax
    movq    %rax, -8(%rbp)
    movq    (%rdi), %rax
    movq    8(%rdi), %rcx
    movq    %rcx, -16(%rbp)
    movq    %rax, -24(%rbp)
    movq    $0, 16(%rdi)
    movq    $0, 8(%rdi)
    movq    $0, (%rdi)
    leaq    -24(%rbp), %rdi
    callq   __ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEED1Ev
    addq    $32, %rsp
    popq    %rbp
    retq
    .cfi_endproc

最佳答案

这种微基准测试通常具有误导性,因为它没有测试您认为它测试的东西。

但是,对于您的情况,我可以解释您所看到的测量结果最可能的原因。

std::string,在所有现代实现中,都使用称为“小缓冲区优化”或 SBO 的东西。 (@FantasticMrFox 在关于使用享元的评论中的断言是错误的。我认为除了空字符串之外,没有任何流行的实现使用过享元。他的意思是写时复制,过去 GNU 的标准库使用它,但是GNU 切换了,因为兼容的 C++11 字符串不能使用 COW。)

在这个优化中,字符串对象内部预留了一些空间来存储短字符串,避免为它们分配堆。

这意味着字符串的复制和 move 构造函数大致如下实现:

copy(source) {
  if source length > internal buffer capacity
    allocate space
  copy source buffer to my buffer
}

move(source) {
  if source uses internal buffer {
    copy source buffer to my buffer
    set source length to zero
    set first byte of source buffer to zero
  } else {
    steal source buffer
  }
}

如您所见, move 构造函数有点复杂。它也比某些实现中的优化更优化,但一般逻辑保持不变。

因此,对于小的缓冲区字符串(我怀疑您正在测试的字符串适合您的特定实现),只需要做更少的复制工作,因为不需要重置源字符串。

但是当你打开完全优化时,编译器可能会识别出一些死存储并将它们删除。 (当然,编译器可能会删除您的整个基准测试,因为它实际上并没有做任何事情。)

关于c++ - 为什么 move ctor 比 copy ctor 慢?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48144898/

相关文章:

c++ - 迭代 boost::icl::interval_set

C++:用多个定界符拆分字符串并在结果中保留定界符?

database - 存储 HTTP session 数据的最快方法?

c# - 有没有更好的方法来计算消息队列 (MSMQ) 中的消息数?

c++ - Lambda 构造性能和静态 lambda?

c++ - 创建一个将字符串常量返回给枚举的类的廉价方法,反之亦然?

c++ - 为什么 for_each 可以在没有 std::prefix 的情况下工作

c++ - 反转 x 轴的透视投影 (glm::perspective)

c++ - 在C++中,构造时用 'this'指针初始化一个类成员

jquery - asp.net 网站性能 - 1 个页面缓慢会影响同一浏览器 session 上的其余页面