c++ 如何禁用thread_local变量的clang表达式消除

eulz3vhy  于 2023-03-05  发布在  其他
关注(0)|答案(1)|浏览(103)
    • bounty将在5天后过期**。回答此问题可获得+100的声誉奖励。Ds.Hale正在寻找来自声誉良好的来源的答案
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变量始终属于当前线程。

w6lpcovy

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

thread_local int* tls = nullptr;

[[gnu::noinline]] int* getTls() {
    asm volatile("");
    return tls;
}

[[gnu::noinline]] void setTls(int* val) {
    asm volatile("");
    tls = val;
}
  • noinline防止编译器直接内联函数
  • asm volatile("");是必需的,因为这两个函数都没有任何副作用,并且作为一个特殊的副作用来防止编译器优化掉对该函数的调用。(参见gcc noinline docs)

这显然会大大降低tls访问的速度(现在每次访问都需要一个额外的函数调用,并且每次都需要重新计算tls索引)但至少它可以正常工作。
(qemu有一个简洁的宏)
不过请注意,这只会解决您自己的线程局部变量的问题。
大多数实现也在内部使用线程局部变量(例如errnopthread_self()std::this_thread::get_id()等),这些变量会遇到相同的tls缓存问题。
(这也可能导致竞争条件,例如,如果一个线程试图写入另一个线程的TLS索引errno ...)
不幸的是,对于这些线程局部变量没有解决办法(因为它们隐藏在库代码中),所以不幸的是,您只能自己解决这些问题(至少在clang和gcc上)。

未来

在C20中,我们得到了native coroutine support,它也使得线程之间的切换变得简单。
所以更多的用户在使用原生C
协程时遇到了这个问题--对于那些clang在trunk中实现了修复的用户:

然而,此修复仅适用于本机C协程;它不适用于libcontext、boost.context等(至少目前是这样;也许我们将来会得到一些函数属性来处理这个问题)
因此,如果你能够切换到原生C
协程,那么这可能是一个潜在的解决方案。
小协程示例:godbolt

#include <coroutine>
#include <iostream>
#include <thread>
 
auto switch_to_new_thread()
{
    struct awaitable
    {
        bool await_ready() {
            return false;
        }
        void await_suspend(std::coroutine_handle<> h) {
            std::thread([h] { h.resume(); }).detach();
        }
        void await_resume() {
        }
    };

    return awaitable{};
}
 
struct task
{
    struct promise_type
    {
        task get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() {}
    };
};

task my_coroutine() {
    std::cout << "Running on thread "
              << std::this_thread::get_id()
              << std::endl;

    co_await switch_to_new_thread();

    std::cout << "Running on thread "
              << std::this_thread::get_id()
              << std::endl;

    co_await switch_to_new_thread();

    std::cout << "Running on thread "
              << std::this_thread::get_id()
              << std::endl;
}

int main() {
    my_coroutine();
    
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return 0;
}
  • 使用clang 15和-O0编译时:godbolt

(正确输出-3个不同线程ID):

Running on thread 139806754031424
Running on thread 139806754027264
Running on thread 139806745634560
  • 在clang 15 -O2中,我们看到了原始错误:godbolt

(错误输出-三次相同的线程ID):

Running on thread 140037315024704
Running on thread 140037315024704
Running on thread 140037315024704
  • 使用clang trunk -O2时,修复正在起作用:godbolt

(正确输出-3个不同线程ID):

Running on thread 140633090672448
Running on thread 140633090668288
Running on thread 140633082275584

相关问题