我可以通过将变量声明为线程局部变量来避免缓存一致性检查吗?
Can I avoid cache consistency checks by declaring variables as thread-local?
我正在阅读有关 CPU 如何在多线程应用程序中保持缓存一致性的信息。一个核心缓存中的写入将其标记为脏,所有其他核心必须小心不要从主内存中读取该段,因为主内存副本不是最新的。
我编写的许多应用程序 工作方式类似于 actor 系统,可变性仅限于单个线程上的局部变量。我通常不会将它们标记为 "thread local" 除非我有这样做的语义原因。
但是,我是否错过了优化机会?是否将一个变量显式标记为线程局部变量,而不是仅仅以这种方式使用它,是否通知硬件它不必检查一致性,因为该变量永远不会对其他线程可见,即使在原则上也是如此?
编辑: 作为表达同一事物的更高层次的方式,我是否应该期望通过使用像 Akka 这样的正式演员系统而不是仅仅坚持我的 类 中的演员范式?一个正式的 actor 系统增加了严格性、跨计算机扩展的能力以及可能的一些开销,但它是否也有助于低级细节,比如让线程跳过对已知非共享的缓存数据的一致性检查?
是否通过标记数据来做到这一点 "thread local"?
至少在 java 中,所有方法局部变量都是非共享的,因此仅限于一个线程。所以没有理由以任何特殊方式标记它们。
只要你避开false sharing, you're fine. i.e. make sure that the static data used by one thread isn't in the same cache line as static data used by another thread. You could look for this by checking the memory order machine-clears perf event.
如果你发现你的程序有一些虚假共享,你可以重新安排声明的顺序(因为编译器通常倾向于按照声明的顺序存储东西),或者用链接器部分来选择事物在静态存储中的分组方式。结构或数组也可以保证内存布局。
TL;DR:避免将两个变量放在同一个缓存行(通常是 64B),如果这些变量将被不同的线程使用。当然,将 同时从同一线程修改的事物组合在一起。
线程局部变量解决了不同的问题。他们让同一个函数根据调用它的线程访问不同的静态变量。这是传递指针的替代方法。
它们仍然像其他 static
/ 全局变量一样存储在内存中。您可以确定没有虚假共享,但有更便宜的方法可以避免这种情况。
线程局部变量和 "normal" 全局变量之间的区别在于它们的寻址方式。它不是通过绝对地址访问它们,而是线程本地存储块的偏移量。
在 x86 上,这是通过段覆盖前缀完成的。例如mov rax, QWORD PTR fs:0x28
从线程本地存储块内的字节 0x28 加载(因为每个线程的 fs
段寄存器都加载了它自己的 TLS 块的偏移量)。
所以 TLS 不是免费的。如果你不需要它,请不要使用它。 不过,它可能比四处传递指针更便宜。
无法让硬件跳过缓存一致性检查,因为硬件没有任何 TLS 概念。只有存储和加载 to/from 内存,以及 the ordering guarantees provided by the ISA。由于 TLS 只是为不同调用者使用不同地址获得相同功能的技巧,因此实现 TLS 时的软件错误可能会导致存储到相同地址。硬件不会让有缺陷的软件以这种方式破坏其缓存一致性,因为它可能会破坏特权分离。
在弱序架构上,memory_order_consume
(理论上)是一种安排线程间数据依赖关系的方法,这样其他线程只需要等待写入共享数据,而不是写入线程私有数据。
然而,这对于编译器来说很难安全可靠地正确执行,因此他们目前将 mo_consume 实现为更强大的 mo_acquire。我写了 ,其中包含一堆指向内存排序内容的链接,并提到了 C++11 memory_order_consume。
很难标准化,因为不同的架构有 different rules for what operations carry a dependency。我假设一些代码库有一些利用依赖排序的手写汇编。 AFAIK,手写 asm 是利用依赖排序避免弱排序 ISA 上的内存屏障指令的唯一方法。 (例如,在生产者-消费者模型中,或在需要的不仅仅是无序原子存储的无锁算法中。)
我正在阅读有关 CPU 如何在多线程应用程序中保持缓存一致性的信息。一个核心缓存中的写入将其标记为脏,所有其他核心必须小心不要从主内存中读取该段,因为主内存副本不是最新的。
我编写的许多应用程序 工作方式类似于 actor 系统,可变性仅限于单个线程上的局部变量。我通常不会将它们标记为 "thread local" 除非我有这样做的语义原因。
但是,我是否错过了优化机会?是否将一个变量显式标记为线程局部变量,而不是仅仅以这种方式使用它,是否通知硬件它不必检查一致性,因为该变量永远不会对其他线程可见,即使在原则上也是如此?
编辑: 作为表达同一事物的更高层次的方式,我是否应该期望通过使用像 Akka 这样的正式演员系统而不是仅仅坚持我的 类 中的演员范式?一个正式的 actor 系统增加了严格性、跨计算机扩展的能力以及可能的一些开销,但它是否也有助于低级细节,比如让线程跳过对已知非共享的缓存数据的一致性检查?
是否通过标记数据来做到这一点 "thread local"?
至少在 java 中,所有方法局部变量都是非共享的,因此仅限于一个线程。所以没有理由以任何特殊方式标记它们。
只要你避开false sharing, you're fine. i.e. make sure that the static data used by one thread isn't in the same cache line as static data used by another thread. You could look for this by checking the memory order machine-clears perf event.
如果你发现你的程序有一些虚假共享,你可以重新安排声明的顺序(因为编译器通常倾向于按照声明的顺序存储东西),或者用链接器部分来选择事物在静态存储中的分组方式。结构或数组也可以保证内存布局。
TL;DR:避免将两个变量放在同一个缓存行(通常是 64B),如果这些变量将被不同的线程使用。当然,将 同时从同一线程修改的事物组合在一起。
线程局部变量解决了不同的问题。他们让同一个函数根据调用它的线程访问不同的静态变量。这是传递指针的替代方法。
它们仍然像其他 static
/ 全局变量一样存储在内存中。您可以确定没有虚假共享,但有更便宜的方法可以避免这种情况。
线程局部变量和 "normal" 全局变量之间的区别在于它们的寻址方式。它不是通过绝对地址访问它们,而是线程本地存储块的偏移量。
在 x86 上,这是通过段覆盖前缀完成的。例如mov rax, QWORD PTR fs:0x28
从线程本地存储块内的字节 0x28 加载(因为每个线程的 fs
段寄存器都加载了它自己的 TLS 块的偏移量)。
所以 TLS 不是免费的。如果你不需要它,请不要使用它。 不过,它可能比四处传递指针更便宜。
无法让硬件跳过缓存一致性检查,因为硬件没有任何 TLS 概念。只有存储和加载 to/from 内存,以及 the ordering guarantees provided by the ISA。由于 TLS 只是为不同调用者使用不同地址获得相同功能的技巧,因此实现 TLS 时的软件错误可能会导致存储到相同地址。硬件不会让有缺陷的软件以这种方式破坏其缓存一致性,因为它可能会破坏特权分离。
在弱序架构上,memory_order_consume
(理论上)是一种安排线程间数据依赖关系的方法,这样其他线程只需要等待写入共享数据,而不是写入线程私有数据。
然而,这对于编译器来说很难安全可靠地正确执行,因此他们目前将 mo_consume 实现为更强大的 mo_acquire。我写了
很难标准化,因为不同的架构有 different rules for what operations carry a dependency。我假设一些代码库有一些利用依赖排序的手写汇编。 AFAIK,手写 asm 是利用依赖排序避免弱排序 ISA 上的内存屏障指令的唯一方法。 (例如,在生产者-消费者模型中,或在需要的不仅仅是无序原子存储的无锁算法中。)