我正在使用依赖thread_local
的第三方库。这导致我的程序重复调用__tls_init()
,即使在某些周期的每次迭代中(我还没有检查所有这些),尽管thread_local
变量已被无条件初始化相同的功能(实际上,在整个程序的开始附近)。
我__tls_init()
上x86_64
的第一条说明是
cmpb $0, %fs:__tls_guard@tpoff
je .L530
ret
.L530:
pushq %rbp
pushq %rbx
subq (some stack space), %rsp
movb $1, %fs:__tls_guard@tpoff
所以第一次按每个线程调用它时,%fs:__tls_guard@tpoff
的值设置为1
,并且进一步调用会立即返回。但是,这意味着每次访问call
变量时thread_local
的所有开销都是正确的吗?
请注意,这是一个静态链接(实际上是生成!)函数,因此编译器"知道"它从这个条件开始,完全可以想象流分析发现没有必要多次调用这个函数。但它没有。
是否有可能摆脱多余的call __tls_init
指令,或者至少停止编译器在时间关键部分发出它们?
实际编译的示例情况:( - O3)
pushq %r13
movq %rdi, %r13
pushq %r12
pushq %rbp
pushq %rbx
movq %rsi, %rbx
subq $88, %rsp
call __tls_init // always gets called
movq (%rbx), %rdi
call <some local function>
movq 8(%rax), %r12
subq (%rax), %r12
movq %rax, %rbp
sarq $4, %r12
cmpq $1, %r12
jbe .L6512
leaq -2(%r12), %rax
movq $0, (%rsp)
leaq 48(%rsp), %rbx
movq %rax, 8(%rsp)
.L6506:
call __tls_init // needless and called potentially very many times!
movq %rsp, %rsi
movq %rsp, %rdi
addq $8, %rbx
call <some other local function>
movq %rax, -8(%rbx)
leaq 80(%rsp), %rax
cmpq %rbx, %rax
jne .L6506 // cycle
更新:上述源代码过于复杂。这是一个MWE:
void external(int);
struct X {
volatile int a; // to prevent optimizing to a constexpr
X() { a = 5; } // to enforce calling a c-tor for thread_local
void f() { external(a); } // to prevent disregarding the value of a
};
thread_local X x;
void f() {
x.f();
for(int j = 0; j < 10; j++)
x.f(); // x is totally initialized now
}
如果您在编译器资源管理器(link to this particular example)中看到使用最大优化设置进行分析,您会注意到在每次重复中冗余地检查fs:__tls_guard@tpoff
与0
相同的现象将<1>放在之后的循环,即在标签.L4
中(假设输出将保持不变),即使在这个超简单的情况下内联__tls_init
。
虽然这个问题是关于G ++的,但CLang(see in Compiler Explorer)使这更加明显。
可以说外部函数调用可能会覆盖此示例中的存储值。但那会有什么保证?如果是这样,它也可能不尊重调用约定。在这些方面,编译器只需要假设它会很好用。此外,我上面的主代码和单个翻译单元中没有外部函数,只是相当大(在MWE等小例子中,编译器将检测并删除无关的测试,显示它必须以某种方式可能。)
答案 0 :(得分:3)
我不知道是否有任何编译器选项可以消除tls调用,但是您可以通过在函数中使用指向TLS对象的指针来优化您的特定代码:
void f() {
auto ptr = &x;
ptr->f();
for(int j = 0; j < 10; j++)
ptr->f();
}