为什么移动ctor比复制ctor慢?

nee*_*vek 0 c++ performance move copy-constructor c++11

我有以下代码来测试复制ctor并移动std::string类的ctor,结果让我感到惊讶,移动ctor 比复制ctor慢约1.4倍.

据我所知,移动构造不需要分配内存,因为在这种std::string情况下,移动构造对象中可能有一个内部指针直接设置为移动对象的内部指针,它应该比为缓冲区分配内存更快然后在复制构造时复制对象中的内容.

这是代码:

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

编辑:

从这两个函数的汇编中,我可以看到,因为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
Run Code Online (Sandbox Code Playgroud)

EDIT2:

事情变得有趣,我改变了std::stringstd::vector作为@FantasticMrFox注释中,结果是相反的,MoveConstruct~1.9比快倍CopyConstruct,这似乎std::remove_reference不是罪魁祸首,但优化这两个类的可能.

EDIT3:

以下代码在带有Apple LLVM 8.0.0版(clang-800.0.42.1)的MacOS上编译,优化标志为-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
Run Code Online (Sandbox Code Playgroud)

Seb*_*edl 5

这种微基准测试通常会产生误导,因为它不会测试您认为测试的东西.

但是,在您的情况下,我可以解释您所看到的测量的最可能原因.

std::string在所有现代实现中,使用称为"小缓冲区优化"或SBO的东西.(@ FantasticMrFox在关于使用flyweight的评论中的断言是错误的.我不认为任何流行的实现曾经使用flyweight除了空字符串.他的意思是写拷贝,过去GNU的标准库使用,但是GNU被切换掉,因为兼容的C++ 11字符串不能使用COW.)

在此优化中,字符串对象内部保留一些空间以存储短字符串并避免为它们分配堆.

这意味着字符串的复制和移动构造函数大致如下所示:

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

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

因此对于小缓冲区字符串(我怀疑你正在测试的那个适合你的特定实现),复制的工作量就少了,因为源字符串不需要重置.

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