因此,对于下面的代码,我看到了一个奇怪的内联/优化人工制品。
我很好奇,在一些我不欣赏的假设场景中,它们是否是“必要的”?
Godbolt:https://godbolt.org/z/M8PW1obE7
#include <cstdint>
#include <stdio.h>
struct ThreadStateLogger
{
static thread_local struct Instance
{
char TLS_byteLoc[3] {' ', 0, 0};
uint8_t TLS_numBytes {1};
// 4 wasted bytes here...
char* TLS_byteLocPtr {TLS_byteLoc};
void Log(char v)
{
TLS_byteLocPtr[0] = v;
}
void Log(char v1, char v2)
{
TLS_byteLocPtr[0] = v1;
if (TLS_numBytes>1)
TLS_byteLocPtr[1] = v2;
}
} instance;
static void Log(char v1, char v2)
{
instance.Log(v1, v2);
}
// static void Log(char v1, char v2)
// {
// instance.TLS_byteLocPtr[0] = v1;
// if (instance.TLS_numBytes>1)
// instance.TLS_byteLocPtr[1] = v2;
// }
};
extern ThreadStateLogger theThreadStateLogger;
int main()
{
ThreadStateLogger::Log('a', 'b');
// printf("Hello world");
ThreadStateLogger::Log('c', 'd');
return 0;
}
整个主要实现都与-O3内联,这正是我想要的:-)
因此,第一个Log()调用似乎正确地检查了这个TLS是否需要分配,然后使用__tls_get_addr@PLT获得转换后的地址,这一切都很好。
第二个Log()调用显然也会检查对象是否需要初始化,但随后会使用第一个调用(rbx)中的缓存地址!所以如果它初始化了,那可能是错的?
下面是godbolt上clang 16的输出,它与gcc相当-相同的重新初始化测试和缓存地址,但它比我现在使用-fPIC的clang 10好一些。https://godbolt.org/z/M8PW1obE7
main: # @main
push rbx
cmp qword ptr [rip + _ZTHN17ThreadStateLogger8instanceE@GOTPCREL], 0
je .LBB0_2
call TLS init function for ThreadStateLogger::instance@PLT
.LBB0_2:
data16
lea rdi, [rip + ThreadStateLogger::instance@TLSGD]
data16
data16
rex64
call __tls_get_addr@PLT
mov rbx, rax
mov rax, qword ptr [rax + 8]
mov byte ptr [rax], 97
cmp byte ptr [rbx + 3], 2
jae .LBB0_3
cmp qword ptr [rip + _ZTHN17ThreadStateLogger8instanceE@GOTPCREL], 0
jne .LBB0_5
.LBB0_6:
mov rax, qword ptr [rbx + 8]
mov byte ptr [rax], 99
cmp byte ptr [rbx + 3], 2
jae .LBB0_7
.LBB0_8:
xor eax, eax
pop rbx
ret
.LBB0_3:
mov rax, qword ptr [rbx + 8]
mov byte ptr [rax + 1], 98
cmp qword ptr [rip + _ZTHN17ThreadStateLogger8instanceE@GOTPCREL], 0
je .LBB0_6
.LBB0_5:
call TLS init function for ThreadStateLogger::instance@PLT
mov rax, qword ptr [rbx + 8]
mov byte ptr [rax], 99
cmp byte ptr [rbx + 3], 2
jb .LBB0_8
.LBB0_7:
mov rax, qword ptr [rbx + 8]
mov byte ptr [rax + 1], 100
xor eax, eax
pop rbx
ret
- 编辑 *
删除printf -?添加了更多的初始化检查,但是__tls_get_addr仍然缓存在rbx中。
- 这看起来像是在至少1个代码路径中优化与TLS_numBytes的比较。我想这是因为我在两个调用之间没有复杂性,所以这看起来是一个有效的优化,但在实践中没有用处。
因此,运行时成本只是重复检查TLS-singleton-initialised标志,我们知道该标志已设置。还有代码膨胀,这很烦人。
- tldr*
所以提醒一下这个问题:为什么重复初始化检查/调用?如果这是必要的,那么为什么不需要重新生成地址?或者这只是一个没有人想到要做的优化?有没有办法通过改变代码模式来获得更好的优化?我有几个去到这里,正如你所看到的。
当然,我需要一个具有这种行为的日志类的功能,并且使用一个类来封装3个TLS变量意味着它们都是一起构建的,而不是一次一个。
1条答案
按热度按时间igsr9ssn1#
LLVM和GCC可能无法优化这一点。去虚拟化、线程本地存储初始化和其他语言特性可以在函数中进行本地优化,但是一旦它们被内联到另一个函数中,就没有进一步的潜力了。
当我们查看LLVM IR时,这个问题就变得很明显了:
在所有优化通过后,此函数变为:
重要的是这个函数包含:
call void @TLS init function for ThreadStateLogger::instance()
(断开)call void @_ZTHN17ThreadStateLogger8instanceE()
(损坏)它不是一个神奇的函数,编译器无法知道调用它一次会使以后的调用变得不必要。函数调用位于从可变全局内存读取的分支中:
并且没有IR级别的信息,该存储器必须由我们的
call void @_ZTHN17ThreadStateLogger8instanceE()
设置为!= 0
。如果随后将
ThreadStateLogger::Log(char)
内联到main
中,则包含此函数调用的两个分支不会相互废弃,您将看到call TLS init function for ThreadStateLogger::instance@PLT
的两个示例那
__tls_get_addr
为什么要优化?__tls_get_addr
是@llvm.threadlocal.address.p0
,它可能会受到对_ZTHN17ThreadStateLogger8instanceE
函数的“随机”调用所没有的优化。在
main
中执行 EarlyCSEPass 时,该调用将被消除。参见compiler explorer with LLVM Optimization Pipeline。