thread_local int* tls = nullptr;
// using libcontext to jump stack.
void jump_stack();
void* test() {
// before jump_stack, assume we are at thread 1.
int *cur_tls = tls;
jump_stack();
// after jump stack, we are at thread 2.
// we need to reload tls.
cur_tls = tls;
}
OSX:达尔文内核版本22.1.0(苹果M1芯片)
铃声:苹果铃声版本14.0.0(铃声-1400.0.29.202)
x一个一个一个一个x一个一个二个x
在jump_stack
之前,tls
已经缓存到[sp, #16]
中,在jump_stack
之后,则将[sp, #16]
重新加载到cur_tls
中,其中tls
属于thread 1
而不是thread 2
。
是否有任何clang选项禁用此优化以重新加载thread_local变量始终属于当前线程。
1条答案
按热度按时间w6lpcovy1#
所有3个主要的编译器(msvc,gcc,clang)都像你的例子一样优化tls访问,基于执行线程永远不会改变的假设。
实际情况比看起来还要糟糕--由于内联和CSE,tls访问还可以跨函数调用边界进行优化。
要使其工作,您需要纤程安全的线程本地存储。
(i.e. TLS访问需要在每次访问索引时重新评估索引)
不幸的是,MSVC是目前唯一一个提供官方方法来对
/GT
compiler switch执行此操作的编译器。GCC和Clang没有提供任何官方途径来获得这种行为,从他们的问题来看,也不打算这样做:
分辨率:WONTFIX
自2014年以来未解决
你也不是第一个遇到这些问题的人;许多其他使用可以在线程之间切换的协程/纤程的项目也遇到了同样的问题。
仅举几例:
gcc和clang解决方案
针对gcc & clang的建议解决方法是使用noinline函数来 Package 对线程局部变量的访问,例如:
godbolt
noinline
防止编译器直接内联函数asm volatile("");
是必需的,因为这两个函数都没有任何副作用,并且作为一个特殊的副作用来防止编译器优化掉对该函数的调用。(参见gcc noinline docs)这显然会大大降低tls访问的速度(现在每次访问都需要一个额外的函数调用,并且每次都需要重新计算tls索引)但至少它可以正常工作。
(qemu有一个简洁的宏)
不过请注意,这只会解决您自己的线程局部变量的问题。
大多数实现也在内部使用线程局部变量(例如
errno
、pthread_self()
、std::this_thread::get_id()
等),这些变量会遇到相同的tls缓存问题。(这也可能导致竞争条件,例如,如果一个线程试图写入另一个线程的TLS索引
errno
...)不幸的是,对于这些线程局部变量没有解决办法(因为它们隐藏在库代码中),所以不幸的是,您只能自己解决这些问题(至少在clang和gcc上)。
未来
在C20中,我们得到了native coroutine support,它也使得线程之间的切换变得简单。
所以更多的用户在使用原生C协程时遇到了这个问题--对于那些clang在trunk中实现了修复的用户:
然而,此修复仅适用于本机C协程;它不适用于libcontext、boost.context等(至少目前是这样;也许我们将来会得到一些函数属性来处理这个问题)
因此,如果你能够切换到原生C协程,那么这可能是一个潜在的解决方案。
小协程示例:godbolt
-O0
编译时:godbolt(正确输出-3个不同线程ID):
-O2
中,我们看到了原始错误:godbolt(错误输出-三次相同的线程ID):
-O2
时,修复正在起作用:godbolt(正确输出-3个不同线程ID):