【问题标题】:Can I avoid cache consistency checks by declaring variables as thread-local?我可以通过将变量声明为线程本地来避免缓存一致性检查吗?
【发布时间】:2016-05-28 14:10:18
【问题描述】:

我正在阅读有关 CPU 如何在多线程应用程序中保持其缓存一致性的信息。写入一个内核的缓存会将其标记为脏,所有其他内核必须小心不要从主内存读取该段,因为主内存副本不是最新的。

我编写的许多应用程序一个actor系统一样工作,其可变性仅限于单个线程上的局部变量。我通常不会将它们标记为“线程本地”,除非我有这样做的语义原因。

但是,我是否错过了优化机会?是否将变量显式标记为线程本地,而不是仅以这种方式使用它,是否会通知硬件它不必检查一致性,因为该变量永远不会对其他线程可见,即使在原则上也是如此?

编辑:作为表达同一事物的更高级别的方式,我是否应该期望通过使用正式的演员系统(如 Akka)来获得性能提升,而不是仅仅在我的课程中坚持演员范式?正式的参与者系统增加了严格性、跨计算机扩展的能力以及可能的一些开销,但它是否也有助于低级细节,例如让线程跳过对已知非共享的缓存数据的一致性检查?

是否通过将数据标记为“线程本地”来做到这一点?

【问题讨论】:

    标签: actor micro-optimization thread-local


    【解决方案1】:

    只要避免false sharing,就可以了。即确保static data used by one thread isn't in the same cache line as static data used by another thread.您可以通过查看memory order machine-clears perf event 来查找此内容。

    如果你发现你的程序有一些错误共享,你可以重新安排声明的顺序(因为编译器通常倾向于按照声明的顺序存储东西),或者用链接器部分做一些事情选择事物在静态存储中的分组方式。结构体或数组也可以保证内存布局。

    TL;DR:避免将两个变量放在同一缓存行(通常为 64B)中,如果这些变量将被不同的线程使用。当然,将同一线程中同时修改的内容组合在一起。


    线程局部变量解决了不同的问题。它们让同一个函数根据调用它的线程访问不同的静态变量。这是传递指针的替代方法。

    它们仍然像其他static / 全局变量一样存储在内存中。您可以确定没有虚假分享,但有更便宜的方法可以避免这种情况。

    线程局部变量和“普通”全局变量之间的区别在于它们的处理方式。它不仅仅是通过绝对地址访问它们,而是从线程本地存储块的偏移量。

    在 x86 上,这是通过段覆盖前缀完成的。例如mov rax, QWORD PTR fs:0x28 从线程本地存储块内的字节 0x28 加载(因为每个线程的 fs 段寄存器加载了其自己的 TLS 块的偏移量)。

    所以 TLS 不是免费的。如果您不需要它,请不要使用它。不过,它可能比传递指针更便宜。


    没有办法让硬件跳过缓存一致性检查,因为硬件没有任何 TLS 概念。只有存储和加载到/从内存,以及the ordering guarantees provided by the ISA。由于 TLS 只是获得相同功能以将不同地址用于不同调用者的技巧,因此实现 TLS 中的软件错误可能导致存储到相同地址。硬件不会让有缺陷的软件以这种方式破坏其缓存一致性,因为它可能会破坏权限分离。

    在弱序架构上,memory_order_consume(理论上)是一种安排线程间数据依赖关系的方法,这样只有写入共享数据必须由其他线程等待,而不是写入线程私有数据.

    但是,编译器很难安全可靠地正确处理,因此他们目前将 mo_consume 实现为更强大的 mo_acquire。我写了a really long and rambling answer a while ago,里面有一堆内存排序的链接,还提到了C++11 memory_order_consume。

    很难标准化,因为不同的架构都有different rules for what operations carry a dependency。我假设一些代码库有一些利用依赖顺序的手写 asm。 AFAIK,手写 asm 是利用依赖排序来避免弱排序 ISA 上的内存屏障指令的唯一方法。 (例如,在生产者-消费者模型中,或者在需要的不仅仅是无序原子存储的无锁算法中。)

    【讨论】:

    • 谢谢!我一直在使用线程局部变量来解决这个不同的问题(因此很少使用它们,因为我通常不会以需要它的方式构建应用程序——主要是在扩展第三方代码时作为一种解决方法)。我只是想知道它是否旨在作为对硬件的关于不能共享的指令。显然不是。
    • @JimPivarski:我添加了一个关于弱排序架构的部分,以及尽可能避免让线程只等待来自其他线程的实际共享存储。硬件非常擅长在不改变可观察结果的情况下尽可能多地进行无序操作,因此当线程根本不交互(没有真或假共享)时,它们不会互相减慢速度,除非通过竞争内存带宽和共享的最后一级缓存。
    【解决方案2】:

    至少在 java 中,所有方法局部变量都是非共享的,因此仅限于一个线程。所以没有理由用任何特殊的方式标记它们。

    【讨论】:

    • 很高兴知道。在任何语言中,函数中的局部变量都应该是非共享的。但是跨函数调用持续存在的数据(例如类的成员)呢?
    • 好吧,一般来说,如果你不做任何特别的事情,比如volatilesyncrhonized 或其他任何东西,那么你可以在java 中获得最快的访问权限。因为那时编译器和 JVM 都没有做任何特别的事情。
    • 只有你想要一些特别的东西,比如原子性或线程间可见性,然后你要求这个,然后你支付性能。
    猜你喜欢
    • 2018-07-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-11-16
    • 1970-01-01
    • 2012-09-24
    • 2014-08-09
    相关资源
    最近更新 更多