在 GCC 4.8中,C + + 11 thread_local 变量的性能代价是什么?

来自 海湾合作委员会4.8草案变更日志:

G + + 现在实现了 C + + 11 thread_local关键字; 这与 GNU __thread关键字主要是因为它允许动态 初始化和销毁语义 需要对非函数局部引用进行运行时惩罚 thread_local变量即使不需要动态初始化, 因此,用户可能希望继续使用 __thread的 TLS 变量 静态初始化语义静态初始化语义。

这种运行时惩罚的性质和起源究竟是什么?

显然,为了支持非函数本地 thread_local变量,在每个线程主要的条目之前都需要有一个线程初始化阶段(就像全局变量有一个静态初始化阶段一样) ,但是它们是否指的是运行时的损失?

粗略地说,gcc 的 thread _ local 的新实现的架构是什么?

23009 次浏览

(免责声明: 我不太了解海湾合作委员会的内部,所以这也是一个有根据的猜测。)

动态 thread_local初始化是在提交 462819c中添加的,其中一个更改是:

* semantics.c (finish_id_expression): Replace use of thread_local
variable with a call to its wrapper.

因此,运行时的代价是,thread_local变量的每个引用都将成为一个函数调用。让我们来看一个简单的测试用例:

// 3.cpp
extern thread_local int tls;
int main() {
tls += 37;   // line 6
tls &= 11;   // line 7
tls ^= 3;    // line 8
return 0;
}


// 4.cpp


thread_local int tls = 42;

在编译 * 时,我们看到 每个tls引用的使用变成了对 _ZTW3tls的函数调用,这个函数会延迟地初始化变量一次:

00000000004005b0 <main>:
main():
4005b0:   55                          push   rbp
4005b1:   48 89 e5                    mov    rbp,rsp
4005b4:   e8 26 00 00 00              call   4005df <_ZTW3tls>    // line 6
4005b9:   8b 10                       mov    edx,DWORD PTR [rax]
4005bb:   83 c2 25                    add    edx,0x25
4005be:   89 10                       mov    DWORD PTR [rax],edx
4005c0:   e8 1a 00 00 00              call   4005df <_ZTW3tls>    // line 7
4005c5:   8b 10                       mov    edx,DWORD PTR [rax]
4005c7:   83 e2 0b                    and    edx,0xb
4005ca:   89 10                       mov    DWORD PTR [rax],edx
4005cc:   e8 0e 00 00 00              call   4005df <_ZTW3tls>    // line 8
4005d1:   8b 10                       mov    edx,DWORD PTR [rax]
4005d3:   83 f2 03                    xor    edx,0x3
4005d6:   89 10                       mov    DWORD PTR [rax],edx
4005d8:   b8 00 00 00 00              mov    eax,0x0              // line 9
4005dd:   5d                          pop    rbp
4005de:   c3                          ret


00000000004005df <_ZTW3tls>:
_ZTW3tls():
4005df:   55                          push   rbp
4005e0:   48 89 e5                    mov    rbp,rsp
4005e3:   b8 00 00 00 00              mov    eax,0x0
4005e8:   48 85 c0                    test   rax,rax
4005eb:   74 05                       je     4005f2 <_ZTW3tls+0x13>
4005ed:   e8 0e fa bf ff              call   0 <tls> // initialize the TLS
4005f2:   64 48 8b 14 25 00 00 00 00  mov    rdx,QWORD PTR fs:0x0
4005fb:   48 c7 c0 fc ff ff ff        mov    rax,0xfffffffffffffffc
400602:   48 01 d0                    add    rax,rdx
400605:   5d                          pop    rbp
400606:   c3                          ret

比较一下 __thread版本,它没有这个额外的包装:

00000000004005b0 <main>:
main():
4005b0:   55                          push   rbp
4005b1:   48 89 e5                    mov    rbp,rsp
4005b4:   48 c7 c0 fc ff ff ff        mov    rax,0xfffffffffffffffc // line 6
4005bb:   64 8b 00                    mov    eax,DWORD PTR fs:[rax]
4005be:   8d 50 25                    lea    edx,[rax+0x25]
4005c1:   48 c7 c0 fc ff ff ff        mov    rax,0xfffffffffffffffc
4005c8:   64 89 10                    mov    DWORD PTR fs:[rax],edx
4005cb:   48 c7 c0 fc ff ff ff        mov    rax,0xfffffffffffffffc // line 7
4005d2:   64 8b 00                    mov    eax,DWORD PTR fs:[rax]
4005d5:   89 c2                       mov    edx,eax
4005d7:   83 e2 0b                    and    edx,0xb
4005da:   48 c7 c0 fc ff ff ff        mov    rax,0xfffffffffffffffc
4005e1:   64 89 10                    mov    DWORD PTR fs:[rax],edx
4005e4:   48 c7 c0 fc ff ff ff        mov    rax,0xfffffffffffffffc // line 8
4005eb:   64 8b 00                    mov    eax,DWORD PTR fs:[rax]
4005ee:   89 c2                       mov    edx,eax
4005f0:   83 f2 03                    xor    edx,0x3
4005f3:   48 c7 c0 fc ff ff ff        mov    rax,0xfffffffffffffffc
4005fa:   64 89 10                    mov    DWORD PTR fs:[rax],edx
4005fd:   b8 00 00 00 00              mov    eax,0x0                // line 9
400602:   5d                          pop    rbp
400603:   c3                          ret

不过,在 thread_local的每个用例中都不需要这个包装器。 只有在下列情况下才会生成包装器:

  • 它是 没有函数-local,

    1. 它是 extern(上面显示的示例) ,或者
    2. 该类型有一个非平凡的析构函数(__thread变量不允许这样做) ,或者
    3. Type 变量由非常量表达式初始化(__thread变量也不允许使用非常量表达式)。

在所有其他用例中,它的行为与 __thread相同。这意味着,除非您有一些 extern __thread变量,否则您可以将所有 __thread替换为 thread_local,而不会造成任何性能损失。


* : 我使用 -O0编译,因为内联函数将使函数边界不那么可见。即使我们调到 -O3,那些初始化检查仍然保留。

如果变量是在当前 TU 中定义的,内联程序将负责开销。我希望 thread _ local 的大多数用法都是这样。

对于外部变量,如果程序员可以确定在非定义 TU 中使用变量不需要触发动态初始化(或者因为变量是静态初始化的,或者在定义 TU 中使用变量将在另一个 TU 中使用之前执行) ,他们可以使用-fno-extern-tls-init 选项避免这种开销。

C + + 11 thread _ local 具有与 _ _ thread 说明符相同的运行时效果(__thread不是 C 标准的一部分; thread_local是 C + + 标准的一部分)

它取决于 TLS 变量(用 __thread说明符声明)的声明位置。

  • 如果 TLS 变量是在可执行文件中声明的,那么访问速度很快
  • 如果 TLS 变量是在共享库代码中声明的(使用 -fPIC编译器选项编译) ,并且指定了 -ftls-model=initial-exec编译器选项,那么访问是快速的; 然而以下限制适用: 共享库不能通过 dlopen/dlsym (动态加载)加载,使用库的唯一方法是在编译期间链接到它(链接器选项 -l<libraryname>)
  • 如果 TLS 变量是在一个共享库(-fPIC编译器选项集)中声明的,那么访问非常缓慢,因为一般的动态 TLS 模型是假定的——这里每次访问一个 TLS 变量都会导致对 _tls_get_addr()的调用; 这是默认情况,因为你不受共享库使用方式的限制。

资料来源: Ulrich Drepper 用于线程本地存储的 ELF 处理 Https://www.akkadia.org/drepper/tls.pdf 本文还列出了为受支持的目标平台生成的代码。