c++ - 为什么在这个解散的 std::string dtor 中有一个锁定的 xadd 指令?

标签 c++ gcc assembly x86-64 atomic

我有一个非常简单的代码:

#include <string>
#include <iostream>

int main() {
    std::string s("abc");
    std::cout << s;
}

然后,我编译它:
g++ -Wall test_string.cpp -o test_string -std=c++17 -O3 -g3 -ggdb3

然后反编译,最有趣的一段是:
00000000004009a0 <_ZNSs4_Rep10_M_disposeERKSaIcE.isra.10>:
  4009a0:       48 81 ff a0 11 60 00    cmp    rdi,0x6011a0
  4009a7:       75 01                   jne    4009aa <_ZNSs4_Rep10_M_disposeERKSaIcE.isra.10+0xa>
  4009a9:       c3                      ret    
  4009aa:       b8 00 00 00 00          mov    eax,0x0
  4009af:       48 85 c0                test   rax,rax
  4009b2:       74 11                   je     4009c5 <_ZNSs4_Rep10_M_disposeERKSaIcE.isra.10+0x25>
  4009b4:       83 c8 ff                or     eax,0xffffffff
  4009b7:       f0 0f c1 47 10          lock xadd DWORD PTR [rdi+0x10],eax
  4009bc:       85 c0                   test   eax,eax
  4009be:       7f e9                   jg     4009a9 <_ZNSs4_Rep10_M_disposeERKSaIcE.isra.10+0x9>
  4009c0:       e9 cb fd ff ff          jmp    400790 <_ZdlPv@plt>
  4009c5:       8b 47 10                mov    eax,DWORD PTR [rdi+0x10]
  4009c8:       8d 50 ff                lea    edx,[rax-0x1]
  4009cb:       89 57 10                mov    DWORD PTR [rdi+0x10],edx
  4009ce:       eb ec                   jmp    4009bc <_ZNSs4_Rep10_M_disposeERKSaIcE.isra.10+0x1c>

为什么_ZNSs4_Rep10_M_disposeERKSaIcE.isra.10 (这是 std::basic_string<char, std::char_traits<char>, std::allocator<char> >::_Rep::_M_dispose(std::allocator<char> const&) [clone .isra.10] )是一个以 xadd 为前缀的锁?

一个后续问题是我如何避免它?

最佳答案

它看起来像与 copy on write 相关联的代码字符串。锁定指令递减一个引用计数,然后调用 operator delete仅当包含实际字符串数据的可能共享缓冲区的引用计数为零时(即,它不是共享的:没有其他字符串对象引用它)。

由于 libstdc++ 是开源的,我们可以通过查看源代码来确认这一点!

你反汇编的函数,_ZNSs4_Rep10_M_disposeERKSaIcE de-mangles1 到 std::basic_string<char>::_Rep::_M_dispose(std::allocator<char> const&) .这是corresponding source对于 gcc-4.x 时代 2 中的 libstdc++:

    void
    _M_dispose(const _Alloc& __a)
    {
#if _GLIBCXX_FULLY_DYNAMIC_STRING == 0
      if (__builtin_expect(this != &_S_empty_rep(), false))
#endif
        {
          // Be race-detector-friendly.  For more info see bits/c++config.
          _GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&this->_M_refcount);
          if (__gnu_cxx::__exchange_and_add_dispatch(&this->_M_refcount,
                             -1) <= 0)
        {
          _GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&this->_M_refcount);
          _M_destroy(__a);
        }
        }
    }  // XXX MT

鉴于此,我们可以注释您提供的程序集,将每条指令映射回 C++ 源代码:
00000000004009a0 <_ZNSs4_Rep10_M_disposeERKSaIcE.isra.10>:

  # the next two lines implement the check:
  # if (__builtin_expect(this != &_S_empty_rep(), false))
  # which is an empty string optimization. The S_empty_rep singleton
  # is at address 0x6011a0 and if the current buffer points to that
  # we are done (execute the ret)
  4009a0: cmp    rdi,0x6011a0
  4009a7: jne    4009aa <_ZNSs4_Rep10_M_disposeERKSaIcE.isra.10+0xa>
  4009a9: ret

  # now we are in the implementation of
  # __gnu_cxx::__exchange_and_add_dispatch(&this->_M_refcount, -1)
  # which dispatches either to an atomic version of the add function
  # or the non-atomic version, depending on the value of `eax` which
  # is always directly set to zero, so the non-atomic version is 
  # *always called* (see details below)
  4009aa: mov    eax,0x0
  4009af: test   rax,rax
  4009b2: je     4009c5 <_ZNSs4_Rep10_M_disposeERKSaIcE.isra.10+0x25>

  # this is the atomic version of the decrement you were concerned about
  # but we never execute this code because the test above always jumps
  # to 4009c5 (the non-atomic version)
  4009b4: or     eax,0xffffffff
  4009b7: lock xadd DWORD PTR [rdi+0x10],eax
  4009bc: test   eax,eax
  # check if the result of the xadd was zero, if not skip the delete
  4009be: jg     4009a9 <_ZNSs4_Rep10_M_disposeERKSaIcE.isra.10+0x9>
  # the delete call
  4009c0: jmp    400790 <_ZdlPv@plt> # tailcall

  # the non-atomic version starts here, this is the code that is 
  # always executed
  4009c5: mov    eax,DWORD PTR [rdi+0x10]
  4009c8: lea    edx,[rax-0x1]
  4009cb: mov    DWORD PTR [rdi+0x10],edx
  # this jumps up to the test eax,eax check which calls operator delete
  # if the refcount was zero
  4009ce: jmp    4009bc <_ZNSs4_Rep10_M_disposeERKSaIcE.isra.10+0x1c>

一个关键注意事项是 lock xadd你关心的代码永远不会被执行。有一个mov eax, 0后跟一个 test rax, rax; je - 这个测试总是成功并且总是发生跳转,因为 rax始终为零。

这里发生的是__gnu_cxx::__atomic_add_dispatch以一种检查进程是否绝对是单线程的方式实现。如果它肯定是单线程的,那么它不会费心为 __atomic_add_dispatch 之类的事情使用昂贵的原子指令。 - 它只是使用常规的非原子添加。它通过检查 pthreads 函数的地址来做到这一点,__pthread_key_create - 如果为零,则 pthread库尚未链接,因此该进程绝对是单线程的。在您的情况下,此 pthread 函数的地址在链接时解析为 0 (你的编译命令行上没有 -lpthread),这是 mov eax, 0x0来自。在链接时,优化此知识为时已晚,因此残留的原子增量代码保留但从未执行。此机制在 this answer 中有更详细的描述。 .

执行的代码是函数的最后一部分,从 4009c5 开始.这段代码也减少了引用计数,但是以非原子的方式。在这两个选项之间决定的检查可能基于进程是否是多线程的,例如,是否 -lpthread已链接。无论出于何种原因,此检查在 __exchange_and_add_dispatch 内, 以防止编译器实际删除分支的原子一半的方式实现,即使在构建过程中的某个时刻知道永远不会被采用的事实(毕竟,硬编码 mov eax, 0以某种方式到达那里)。

A follow-up question is how I can avoid it?



好吧,您已经避开了 lock add部分,所以如果这是你关心的,你就可以了。但是,您仍然有理由担心:

复制写入 std::string实现 are not standards compliant due to changes made in C++11 ,所以问题仍然是为什么即使在指定 -std=c++17 时你也会得到这个 COW 字符串行为.

问题很可能与发行版有关:CentOS 7 默认使用古老的 gcc 版本 < 5,它仍然使用不兼容的 COW 字符串。但是,您提到您使用的是 gcc 8.2.1,默认情况下在使用非 COW 字符串的正常安装中。似乎如果您使用 RHEL“devtools”方法安装 8.2.1,您将获得一个新的 gcc,它仍然使用旧的 ABI 和针对旧系统 libstdc++ 的链接。

要确认这一点,您可能需要 check the value of _GLIBCXX_USE_CXX11_ABI macro在您的测试程序中,还有您的 libstdc++ version (版本信息 here 可能有用)。

您可以通过使用 CentOS 以外的不使用古 gcc 和 glibc 版本的操作系统来避免。如果您出于某种原因需要坚持使用 CentOS,则必须查看是否有支持的方式在该发行版上使用较新的 libstdc++ 版本。您还可以考虑使用容器化技术来构建独立于本地主机库版本的可执行文件。

1 你可以像这样去破坏它:echo '_ZNSs4_Rep10_M_disposeERKSaIcE' | c++filt .

2 我正在使用 gcc-4 时代的源代码,因为我猜这就是您最终在 CentOS 7 中使用的源代码。

关于c++ - 为什么在这个解散的 std::string dtor 中有一个锁定的 xadd 指令?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57205494/

相关文章:

c++ - 使用 WNDCLASSEX 创建窗口? [Cpp]

c++ - 根据标准,这些编译器中的哪个有错误?

c++ - wrong-looking编译错误调用模板类的模板成员函数

c++ - 在图上实现 BFS

c++ - 从 C++ 调用以 Julia 回调函数作为参数的 Julia 函数

c++ - 完成虚拟继承

c++ - 修改 "__cxa_allocate_exception"没有使用malloc

c - C 代码中的 Asm 变量

linux - 如果我设置的计数大于我通过 read() 系统调用读取的文件大小怎么办?

使用 avr-gcc 编译汇编程序