代码之家  ›  专栏  ›  技术社区  ›  neevek

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

  •  0
  • neevek  · 技术社区  · 7 年前

    我有以下代码来测试 std::string 课堂上,结果让我很惊讶,move-ctor是 ~1.4 比copy ctor慢倍。

    据我所知,移动构造不需要分配内存 标准::字符串 在这种情况下,move-constructed对象中可能有一个内部指针直接设置为move-object的指针,它应该比为缓冲区分配内存,然后在复制构建时从对象中复制内容更快。

    代码如下:

    #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::vector 正如@FantasticMrFox在评论中提到的,结果恰恰相反, 移动构造(MoveConstruct) ~1.9 时间快于 CopyConstruct ,似乎 std::remove\u引用 不是罪魁祸首,但 优化 这两类中的一类可能是。

    编辑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
    
    2 回复  |  直到 7 年前
        1
  •  5
  •   Sebastian Redl    7 年前

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

    然而,在你的情况下,我可以解释你看到的测量结果最可能的原因。

    std::string ,在所有现代实现中,都使用所谓的“小缓冲区优化”或SBO。(@FantasticMrFox在评论中关于使用flyweight的断言是错误的。我认为除了空字符串之外,任何流行的实现都没有使用过flyweight。他指的是写时拷贝,这是GNU的标准库过去使用的,但GNU放弃了,因为兼容的C++11字符串不能使用COW。)

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

    这意味着string的copy和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构造函数有点复杂。它也比某些实现中的优化程度略高,但一般逻辑保持不变。

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

    但是,当您启用完全优化时,编译器可能会识别一些死存储并删除它们。(当然,编译器可能只是删除整个基准测试,因为它实际上什么都不做。)

        2
  •  1
  •   Yakk - Adam Nevraumont    7 年前

    什么时候 I feed your code to clang or gcc with -O3 我从叮当声中得到:

    main: # @main
      mov eax, 50000000
    .LBB0_1: # =>This Inner Loop Header: Depth=1
      add eax, -25
      jne .LBB0_1
      xor eax, eax
      ret
    

    和gcc:

    main:
      xor eax, eax
      ret
    

    我确实将函数放置在匿名名称空间中,以消除必须导出函数本身的干扰。但主要是被完全优化掉了。

    微基准通常具有误导性。