问题标签 [false-sharing]
For questions regarding programming in ECMAScript (JavaScript/JS) and its various dialects/implementations (excluding ActionScript). Note JavaScript is NOT the same as Java! Please include all relevant tags on your question; e.g., [node.js], [jquery], [json], [reactjs], [angular], [ember.js], [vue.js], [typescript], [svelte], etc.
c++ - 为什么错误共享仍然影响非原子,但远低于原子?
考虑以下证明虚假共享存在的示例:
一个线程以a
1 为增量递增,另一个线程以 1 递增b
。使用 MSVC编译增量lock xadd
,即使结果未使用。
对于a
和分开的结构,几秒钟内累积的值大约是for 的b
十倍。not_shared_t
shared_t
到目前为止的预期结果:单独的缓存线在 L1d 缓存中保持热,增加lock xadd
吞吐量瓶颈,错误共享是对缓存线的性能灾难。(编者注:lock inc
启用优化时使用更高版本的 MSVC。这可能会扩大竞争与非竞争之间的差距。)
现在我using type = std::atomic<std::int64_t>;
用普通代替std::int64_t
(非原子增量编译为inc QWORD PTR [rcx]
。循环中的原子负载恰好阻止编译器将计数器保留在寄存器中,直到循环退出。)
达到的计数not_shared_t
仍然大于 for shared_t
,但现在不到两倍。
为什么非原子情况在性能上如此接近?
这是完成最小可重现示例的程序的其余部分。(也在带有 MSVC 的 Godbolt 上,准备编译/运行)
java - 错误共享并将元素添加到队列
给定两个线程a
并b
从共享队列中读取。如果a
修改了队列并向其中添加了一个元素,这是否意味着b
它会因为队列已被修改而导致缓存未命中?
如在队列末尾更改某些内容(猜测它是一个内存块)会导致缓存行无效并强制它们重新读取吗?
提前致谢。
c++ - 当 2 个线程共享同一个物理内核时,具有错误共享的 volatile 增量在发布时比在调试时运行得慢
我正在尝试测试虚假共享对性能的影响。测试代码如下:
结果大多如下:
我的 CPU 是intel core i7-9750h
. “core0”和“core1”是物理内核,“core2”和“core3”等也是。MSVC 14.24 被用作编译器。
由于有大量后台任务,记录的时间是几次运行中最好成绩的近似值。我认为这很公平,因为结果可以清楚地分组,0.1s~0.3s 的误差不会影响这样的划分。
Test2 很容易解释。x
和在y
不同的缓存行中一样,在 2 个物理内核上运行可以获得 2 倍的性能提升(在此可以忽略在单个内核上运行 2 个线程时的上下文切换成本),并且在具有 SMT 的一个内核上运行效率低于 2物理内核,受coffee-lake的吞吐量限制(相信Ryzen可以做得更好),并且比时间多线程更有效。似乎 64 位模式在这里更有效。
但是 test1 的结果让我很困惑。首先,在调试模式下,0-2、0-3 和 0-5 比 0-0 慢,这是有道理的。我对此进行了解释,因为某些数据被反复从 L1 移动到 L3 和 L3 到 L1,因为缓存必须在 2 个内核之间保持一致,而在单个内核上运行时它始终保持在 L1 中。但是这个理论与 0-1 对总是最慢的事实相冲突。从技术上讲,两个线程应该共享相同的 L1 缓存。0-1 的运行速度应该是 0-0 的 2 倍。
其次,在释放模式下,0-2、0-3、0-5 比 0-0 快,这反驳了上述理论。
最后,0-1 在 64 位和 32 位模式下的运行速度release
都比在debug
64 位和 32 位模式下要慢。这是我最不能理解的。我阅读了生成的汇编代码,没有发现任何有用的东西。
c++ - clang 和 gcc 不实现 std::hardware_{constructive, destroyive}_interference_size 的原因是什么?
我知道答案可能是他们没有优先考虑它,但这真的感觉像是故意遗漏,他们已经有很多 C++20 核心语言/库特性,而这个 C++17 特性仍然没有实现。
事实上,根据这张表,这是 clang 和 gcc 都没有实现的唯一 C++17 库功能。
c - 理解错误共享和内存对齐
在 github 的这个虚假共享测试中,一个数组被定义为int array[100]
. 它说
bad_index = 1
good_index = 99
。然后它创建两个线程并执行以下操作:
- 虚假共享:thread_1 更新
A[0]
,thread_2 更新A[bad_index]
- 无虚假共享:thread_1 更新
A[0]
,thread_2 更新A[good_index]
使用虚假共享,操作速度会慢 2 倍以上。我的问题是为什么索引1
不好而索引99
好?
c++ - 与 alignas 的错误共享预防被打破
我不习惯在互联网上发布任何问题,所以如果我做错了什么,请告诉我。
简而言之
如何正确防止 CPU 缓存线大小为 64 字节的 64 位架构上的错误共享?
C++ 'alignas' 关键字和简单字节数组(例如:char[64])的使用如何影响多线程效率?
语境
在对Single Consumer Single Producer Queue进行非常有效的实现时,我在对代码进行基准测试时遇到了来自 GCC 编译器的不合逻辑的行为。
全文
我希望有人有必要的知识来解释发生了什么。
我目前在 Arch linux 上使用 GCC 10.2.0 及其 C++ 20 实现。我的笔记本电脑是配备 i7-7500U 处理器的联想 T470S。
让我从数据结构开始:
以下数据结构在我的系统上推送/弹出时获得了快速且稳定的 20ns。
但是,仅使用以下成员更改对齐方式会使基准不稳定并给出 20 到 30ns 之间的时间。
最后,当我尝试这种配置时,我更加迷失了,结果在 40 到 55ns 之间。
这次我让队列推送/弹出在 40 到 55ns 之间振荡。
在这一点上我很迷茫,因为我不知道我应该在哪里寻找答案。到目前为止,C++ 内存布局对我来说非常直观,但我意识到我仍然错过了非常重要的知识,以便更好地处理高频多线程。
最少的代码示例
如果你想编译整个代码来自己测试,这里是需要的几个文件:
SPSCQueue.hpp:
基准,使用google benchmark。bench_SPSCQueue.cpp:
java - 虚假共享和易变
美好的一天,我最近发现了 Java 8 中引入的一个名为Contended的注解。从这个邮件列表中,我了解到什么是虚假共享以及注释如何允许对象或字段分配整个缓存行。
经过一番研究,我发现如果两个内核存储相同的缓存行并且其中一个修改它,那么第二个内核必须从主存储器重新读取整行。https://en.wikipedia.org/wiki/MESI_protocol。但我仍然不清楚为什么硬件会强制 CPU 重新读取它。我的意思是这就是为什么我们在 Java 中确实有一个 volatile 关键字,对吧?如果变量被声明为 volatile,那么线程将从缓存中跳过该变量,并始终从主内存读取/写入它。如果硬件在每次写入后强制 cpu 重新读取缓存行,那么在多线程应用程序中如何可能出现数据不一致?
提前致谢
x86 - 在检查错误共享时,为什么使用同级线程运行时比使用独立线程运行时有更多的 L1d 缓存未命中
(我知道过去有人问过一些相关的问题,但我找不到关于 L1d 缓存未命中和超线程/SMT 的问题。)
在阅读了几天关于虚假共享、MESI/MOESI 缓存一致性协议等一些超级有趣的东西之后,我决定用 C 编写一个小的“基准”(见下文),以测试虚假共享的实际效果。
我基本上有一个包含 8 个双精度数的数组,因此它适合一个缓存行和两个递增相邻数组位置的线程。
在这一点上,我应该声明我正在使用 Ryzen 5 3600,其拓扑结构可以在这里看到。
我创建了两个线程,然后将它们固定在两个不同的逻辑核心上,每个核心都访问并更新它自己的数组位置,即线程 A 更新数组 [2] 和线程 B 更新数组 [3] 。
当我使用属于同一内核的硬件线程#0和#6运行代码时(如拓扑图所示)共享 L1d 缓存,执行时间约为 5 秒。
当我使用没有任何共同缓存的线程#0和#11时,大约需要 9.5 秒才能完成。该时间差是预期的,因为在这种情况下,“缓存线乒乓球”正在进行。
但是,这让我感到困扰,当我使用 Threads #0和#11时,L1d 缓存未命中少于使用 Threads #0和#6运行。
我的猜测是,当使用没有公共缓存的线程#0和#11时,当一个线程更新共享缓存行的内容时,根据 MESI/MOESI 协议,另一个核心中的缓存行会失效。因此,即使正在进行乒乓球,也不会发生太多缓存未命中(与使用线程#0和#6运行时相比),只是在内核之间发生了一堆无效和缓存行块传输。
那么,当使用具有公共 L1d 缓存的线程 #0 和 #6 时,为什么会有更多的缓存未命中?
(线程#0和#6也有公共的 L2 缓存,但我认为它在这里没有任何重要性,因为当缓存行失效时,它必须从主内存(MESI)或另一个核心的缓存(MOESI),因此 L2 似乎不可能拥有所需的数据,但也被要求提供)。
当然,当一个线程写入 L1d 缓存行时,缓存行会变得“脏”,但这有什么关系呢?驻留在同一物理核心上的其他线程不应该没有问题读取新的“脏”值吗?
TLDR:在测试 False Sharing时,使用两个同级线程(属于同一物理内核的线程)时,L1d 缓存未命中率大约是使用属于两个不同物理内核中的线程时的3 倍。(2.34% 对 0.75% 的未命中率,3.96 亿对 1.18 亿的绝对未命中数)。为什么会这样?
(L1d 缓存未命中等所有统计数据都是使用 Linux 中的 perf 工具测量的。)
另外,次要问题,为什么兄弟线程在 ID 6 数字中配对?即线程 0 的兄弟是线程 6。线程 i 的兄弟是线程 i+6。这有什么帮助吗?我在 Intel 和 AMD CPU 中都注意到了这一点。
我对计算机体系结构非常感兴趣,我还在学习,所以上面的一些可能是错误的,很抱歉。
所以,这是我的代码。只需创建两个线程,将它们绑定到特定的逻辑核心,然后访问相邻的缓存行位置。
我正在使用 GCC 10.2.0 编译为gcc -pthread p.c -o p
perf record ./p --cpu=0,6
然后在分别使用线程 0,6 和 0,11 时使用 --cpu=0,11运行或相同的东西。
然后在另一种情况下运行perf stat -d ./p --cpu=0,6
或与 --cpu=0,11 相同
使用线程0和6运行:
使用线程0和11运行:
c - 我不知道为什么在 pthread 子例程中更改变量访问/存储类型会大幅提高性能
我是多线程编程的新手,我知道如果你不小心就会有一些奇怪的副作用,但我没想到会对我编写的代码感到困惑。我正在写我认为是线程的明显开始/测试:只是总结 0 到 x 之间的数字(当然https://www.reddit.com/r/mathmemes/comments/gq36wb/nn12/但我想做的更多的是练习如何使用线程,而不是如何使该程序尽可能快)。我使用函数调用来创建基于系统上硬编码内核数的线程,以及定义处理器是否具有多线程功能的“布尔值”。我将工作或多或少均匀地分配到每个线程中,因此每个线程在一个范围之间求和,理论上,如果所有线程都设法一起工作,我可以执行 numcores*normal_computation,这确实令人兴奋,令我惊讶的是,它或多或少符合我的预期;直到我做了一些调整。
在继续之前,我认为一些代码会有所帮助:
这些是我在基本代码中使用的预处理器定义:
我使用这个结构将参数传递给我的面向线程的函数:
这是制作线程的函数:
这只是求和的“正常”实现,只是为了比较:
这是线程运行的函数,它有“两个实现”,一个出于某种原因快速实现,一个出于某种原因慢速实现(这是我所困惑的):额外说明:我使用预处理器指令只是为了让 SLOWER 和 FASTER 版本更容易编译。
这是我的主要内容:
这是我的编译(我确保将优化级别设置为0,以避免编译器完全优化出愚蠢的求和程序,毕竟我想学习如何使用线程!!!):
以下是结果/差异(注意,使用 GCC 生成的代码也有相同的副作用):
为什么使用额外的堆栈定义变量比取消引用传入的结构指针快得多!
我试图自己找到这个问题的答案。我最终做了一些测试,从我的 SumUpTo() 函数中实现了相同的基本/朴素求和算法,唯一的区别是它正在处理的数据间接。
结果如下:
测试产生了我或多或少预期的值。因此,我推断它必须是这个想法之上的东西。
只是为了添加更多信息,我正在运行 Linux,特别是 Mint 发行版。
我的处理器信息如下:
如果您想自己编译代码,或者查看为我的特定实例生成的程序集,请查看:https ://github.com/spaceface102/Weird_Threads 主要源代码是“countV2.c”,以防万一丢失的。感谢您的帮助!