为什么在这个被破坏的std :: string dtor中有锁定的xadd指令?

HCS*_*CSF 1 c++ assembly gcc x86-64 atomic

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

#include <string>
#include <iostream>

int main() {
    std::string s("abc");
    std::cout << s;
}
Run Code Online (Sandbox Code Playgroud)

然后,我编译了它:

g++ -Wall test_string.cpp -o test_string -std=c++17 -O3 -g3 -ggdb3
Run Code Online (Sandbox Code Playgroud)

然后将其反编译,最有趣的部分是:

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

为什么锁_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?

后续问题是如何避免这种情况?

Bee*_*ope 8

它看起来像与字符串复制相关联的代码。锁定的指令将递减引用计数,然后operator delete仅在包含实际字符串数据的可能共享缓冲区的引用计数为零(即,未共享:没有其他字符串对象引用该引用)的情况下调用。

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

该功能你已经拆卸,_ZNSs4_Rep10_M_disposeERKSaIcE去轧液1std::basic_string<char>::_Rep::_M_dispose(std::allocator<char> const&)。这是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
Run Code Online (Sandbox Code Playgroud)

鉴于此,我们可以注释您提供的程序集,将每条指令映射回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>
Run Code Online (Sandbox Code Playgroud)

重要说明是,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来自。在链接时,要优化此知识为时已晚,因此残留的原子增量代码将保留,但永远不会执行。该机制在此答案中有更详细的描述。

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

后续问题是如何避免这种情况?

好了,您已经避免了这一lock add部分,因此,如果您关心的是您的工作,那将是一件好事。但是,您仍然需要担心:

由于C ++ 11中所做的更改,“写时复制” std::string实现不符合标准,因此,问题仍然在于,即使指定,为什么也要精确地获得此COW字符串行为-std=c++17

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

为了确认这一点,您可能要检查测试程序中_GLIBCXX_USE_CXX11_ABI宏的值,以及libstdc ++版本此处的版本信息可能会有用)。

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


1您可以像这样对它进行解密:echo '_ZNSs4_Rep10_M_disposeERKSaIcE' | c++filt

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