yuqi-zheng

_GLIBCXX_USE_CXX11_ABI=0:为什么旧 ABI 这么慢


_GLIBCXX_USE_CXX11_ABI=0 编译标志强制 GCC 使用旧版(C++11 之前)的标准库实现。它存在的唯一理由是二进制兼容性——当你必须链接一个由 GCC 4.x 编译且无法重新编译的闭源 .so.a 时。

在所有其他情况下,保持默认的新 ABI 是正确选择。两者的性能差距远非微不足道。


核心差异

两个标准库组件在新旧 ABI 之间存在本质区别:std::stringstd::list

std::string:写时复制 vs 短字符串优化

旧 ABI (=0)新 ABI (=1,默认)
策略写时复制(COW)短字符串优化(SSO)
内存即使 1 个字符也堆分配短字符串(< 16 字节)直接内联存储,无需 malloc
并发每次复制都需原子更新引用计数深拷贝(或 SSO),无共享状态

SSO 的优势对创建大量短字符串的程序是决定性的——ID、键、路径组件、日志消息。每一次 malloc 都被消除了。

COW 的并发代价更隐蔽但同样真实:跨线程复制 const string 需要对共享引用计数进行原子递增。当足够多的线程同时这样做时,原子竞争就成为瓶颈。

std::list::size():O(n) vs O(1)

这是最危险的差异。在旧 ABI 下,std::list::size()O(n)——遍历整个链表计数。在新 ABI 下,C++11 要求 O(1)——大小作为成员变量存储。

在循环条件中使用旧 ABI 的 list.size(),会把 O(n) 算法变成 O(n²)。


基准测试

字符串创建(SSO)

创建 1000 万个短字符串:

for (int i = 0; i < 10000000; ++i) {
    std::string s = "short";
    len_sum += s.length();
}
ABI耗时加速比
旧 (=0)504 ms基准
新 (=1)44 ms11.6 倍

旧 ABI 调用了 1000 万次 malloc。新 ABI 从未接触堆。

std::list::size()

对 1000 万元素的 list 调用一次 size()

ABI耗时加速比
旧 (=0)39 ms基准
新 (=1)0.0001 ms34 万倍

旧 ABI 遍历了 1000 万个节点。新 ABI 只读了一个整数。

多线程字符串复制

4 个线程,每个线程复制一个 50 字符的字符串 100 万次:

ABI耗时加速比
旧 (=0)943 ms基准
新 (=1)570 ms1.65 倍

COW 中的原子引用计数操作导致缓存一致性流量,拖慢了所有线程。


什么时候确实需要 =0

只有一个正当理由:二进制兼容性。如果你必须链接一个由 GCC < 5 编译的库,且没有源代码,就需要 _GLIBCXX_USE_CXX11_ABI=0 来匹配 std::stringstd::list 的内存布局。这就是 TensorFlow 和某些 CUDA SDK 构建要求它的原因。

如果有源代码,重新编译。如果是新写的代码,使用默认值。


如何检查

#if _GLIBCXX_USE_CXX11_ABI
    // 新 ABI
#else
    // 旧 ABI
#endif

或者检查预处理输出:

echo | g++ -dM -E - | grep GLIBCXX_USE_CXX11_ABI

如果没有输出,说明你使用的是默认的新 ABI。如果显示 #define _GLIBCXX_USE_CXX11_ABI 0,说明构建系统中有什么东西在强制使用旧 ABI。


总结

旧 ABI 是一个兼容性垫片,不是性能选项。它让 std::list::size() 退化为线性时间,强制 std::string 为短字符串进行堆分配,并在并发字符串复制时引入原子竞争。除非遗留的二进制依赖要求,否则没有任何理由使用它。