我有一个非常简单的代码:
#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/