关于多核CPU或多处理器系统中使用的高速缓存存储器,我有几个问题.(虽然与编程没有直接关系,但是当一个人为多核处理器/多处理器系统编写软件时会产生很多反响,因此在这里问!)
在多处理器系统或多核处理器(Intel Quad Core,Core two Duo等......)中,每个cpu核心/处理器都有自己的缓存(数据和程序缓存)吗?
一个处理器/核心可以访问彼此的高速缓存,因为如果允许它们访问彼此的高速缓存,那么我认为可能存在较少的高速缓存未命中,如果特定处理器高速缓存没有一些数据但是其他一些处理器的缓存可能有它,从而避免从内存读入第一个处理器的缓存?这个假设是否有效且真实?
允许任何处理器访问其他处理器的高速缓冲存储器会有任何问题吗?
假设我有两个线程来操纵全局变量x.每个线程(或我认为的每个核心)都有一个缓存副本x.
现在说Thread A执行以下说明:
set x to 5
some other instruction
Run Code Online (Sandbox Code Playgroud)
现在set x to 5执行时,缓存的值x将设置为5,这将导致缓存一致性协议使用新值来操作和更新其他核心的缓存x.
现在我的问题是:什么时候x实际设置5在Thread A缓存中,其他内核的缓存在some other instruction执行之前是否会更新?或者应该使用内存屏障来确保?:
set x to 5
memory barrier
some other instruction
Run Code Online (Sandbox Code Playgroud)
注意:假设指令是按顺序执行的,也假设set x to 5执行时,5会立即放入线程A的缓存中(因此指令不会放在队列中或稍后要执行的内容).
我发现了一条来自 的评论crossbeam。
从 Intel 的 Sandy Bridge 开始,空间预取器现在一次提取成对的 64 字节缓存线,因此我们必须对齐到 128 字节而不是 64。
资料来源:
我在英特尔的手册中没有找到这样的说法。但直到最新的提交,folly仍然使用 128 字节填充,这让我很有说服力。所以我开始编写代码来看看是否可以观察到这种行为。这是我的代码。
#include <thread>
int counter[1024]{};
void update(int idx) {
for (int j = 0; j < 100000000; j++) ++counter[idx];
}
int main() {
std::thread t1(update, 0);
std::thread t2(update, 1);
std::thread t3(update, 2);
std::thread t4(update, 3);
t1.join();
t2.join();
t3.join();
t4.join();
}
Run Code Online (Sandbox Code Playgroud)
我的CPU是锐龙3700X。当索引为0、1、2、3时,大约需要 1.2 秒才能完成。当索引为0, 16, 32,时 …
这个问题专门针对现代x86-64缓存一致性架构 - 我很欣赏其他CPU的答案可能会有所不同.
如果我写入内存,MESI协议要求首先将缓存行读入缓存,然后在缓存中进行修改(将值写入缓存行,然后将其标记为脏).在较旧的写入微架构中,这将触发高速缓存行被刷新,在写回期间,被刷新的高速缓存行可能会延迟一段时间,并且一些写入组合可能在两种机制下发生(更可能是回写) .我知道这与访问相同缓存行数据的其他核心如何交互 - 缓存监听等.
我的问题是,如果商店恰好匹配缓存中已有的值,如果没有单个位被翻转,那么任何英特尔微架构都会注意到这一点并且不将该行标记为脏,从而可能将该行标记为独占,以及在某些时候跟随的回写内存开销?
当我向更多的循环进行矢量化时,我的矢量化操作组合基元不会明确地检查值的变化,并且在CPU/ALU中这样做似乎很浪费,但我想知道底层缓存电路是否可以在没有显式编码的情况下完成(例如,商店微操作或缓存逻辑本身).由于跨多个内核的共享内存带宽变得更加成为资源瓶颈,这似乎是一种越来越有用的优化(例如,重复调整相同的内存缓冲区 - 如果它们已经存在,我们不会重新读取RAM中的值在缓存中,但强制写回相同的值似乎很浪费).回写缓存本身就是对这类问题的承认.
我可以礼貌地要求阻止"在理论上"或"它确实无关紧要"的答案 - 我知道记忆模型是如何工作的,我正在寻找的是关于如何写出相同价值的硬性事实(而不是避免一个商店)将影响内存总线的争用你可以安全地假设是一台运行多个工作负载的机器几乎总是受内存带宽限制.另一方面,解释为什么芯片不这样做的确切原因(我悲观地假设他们没有这样做)将具有启发性......
更新: 这里的预期线路上的一些答案https://softwareengineering.stackexchange.com/questions/302705/are-there-cpus-that-perform-this-possible-l1-cache-write-optimization但仍然很多推测"它必须很难,因为它没有完成",并说如何在主CPU核心中这样做会很昂贵(但我仍然想知道为什么它不能成为实际缓存逻辑本身的一部分).
我已阅读维基百科页面关于无序执行和推测性的exectution.
我不能理解的是相似之处和不同之处.在我看来,当推测执行没有确定条件的值时,它会使用无序执行.
当我阅读Meltdown和Spectre的论文并做了进一步的研究时,出现了混乱.它在陈述消融纸即熔毁是基于乱序执行,而其他一些资源,包括对维基页面sepeculative执行状态消融是基于推测执行.
我想对此有所澄清.
考虑N线程执行一些具有小结果值的异步任务,如double或int64_t.因此,8结果值可以适合单个CPU缓存行.N等于CPU核心数.
一方面,如果我只是分配一个N项目数组,每个a double或者int64_t,那么8 线程将共享一个CPU缓存行,这似乎效率低下.
另一方面,如果我为每个double/ 分配一个完整的缓存行int64_t,接收器线程将必须获取N缓存行,每个缓存行由不同的CPU核心(1除外)写入.
那么这种情况是否有效的解决方案?CPU是x86-64.C++中的解决方案是首选.
澄清1:线程启动/退出开销不大,因为使用了线程池.所以它主要是关键部分的同步.
澄清2:并行批次具有依赖性.主线程只能在收集并处理上一批次的结果后才能启动下一批并行计算.因为前一批次的结果用作下一批次的一些参数.
ARM允许重新排序加载后续存储,以便以下伪代码:
// CPU 0 | // CPU 1
temp0 = x; | temp1 = y;
y = 1; | x = 1;
可以导致temp0 == temp1 == 1(并且,这在实践中也是可观察到的).我无法理解这是怎么发生的; 似乎有序提交会阻止它(这是我的理解,它存在于几乎所有的OOO处理器中).我的理由是"在提交之前,负载必须具有其值,它在存储之前提交,并且在提交之前,存储的值不会对其他处理器可见."
我猜我的一个假设肯定是错的,并且必须遵循下列之一:
说明不需要提交一路有序.稍后的存储可以安全地提交并在之前的加载之前变得可见,只要在存储提交核心时可以保证先前的加载(以及所有中间指令)不会触发异常,并且加载的地址是保证与商店不同.
负载可以在其值已知之前提交.我不知道如何实现这一点.
商店在提交之前可以显示.也许某个内存缓冲区允许将存储转发到另一个线程的加载,即使负载先前已加入?
还有别的吗?
有许多假设的微体系结构特征可以解释这种行为,但我最好的是那些实际存在于现代弱有序CPU中的那些.
想象一下,我想让一个主线程和一个辅助线程作为两个超线程在同一物理核心上运行(可能通过强制它们的亲和力来大致确保这一点)。
主线程将执行重要的高 IPC、CPU 密集型工作。除了定期更新主线程将定期读取的共享时间戳值之外,辅助线程不应该执行任何操作。更新频率是可配置的,但可以快至 100 MHz 或更高。如此快速的更新或多或少排除了基于睡眠的方法,因为阻塞睡眠太慢,无法在 10 纳秒 (100 MHz) 周期内睡眠/唤醒。
所以我想要忙碌的等待。然而,繁忙等待应该对主线程尽可能友好:使用尽可能少的执行资源,从而给主线程增加尽可能少的开销。
我想这个想法将是一个长延迟指令,不使用很多资源,就像pause并且也有一个固定且已知的延迟。这将使我们能够校准“睡眠”周期,因此甚至不需要读取时钟(如果想要更新周期,P我们只需发出P/L这些指令以进行校准的忙睡眠。但pause不满足后一个标准,因为它的延迟各不相同很多1 .
第二种选择是使用长延迟指令,即使延迟未知,并且在每条指令之后执行一个rdtsc或一些其他时钟读取方法(clock_gettime等)来查看我们实际睡了多长时间。看起来它可能会大大减慢主线程的速度。
还有更好的选择吗?
1还有pause一些关于防止推测性内存访问的特定语义,这可能有利于也可能没有利于这个同级线程场景,因为我实际上并不处于自旋等待循环中。
我已经制作了ac/c ++程序(printf和的混合std::)来了解不同的缓存性能.我想并行化一个计算大块内存的进程.我必须在相同的内存位置上进行多次计算,因此我会在结果上写入结果,覆盖源数据.当第一个微积分完成后,我再做一个以前的结果.
I've guessed if I have two threads, one making the first calculus, and the other the second, I would improve performance because each thread does half the work, thus making the process twice as fast. I've read how caches work, so I know if this isn't done well, it may be even worse, so I've write a small program to measure everything.
(See below for machine topology, CPU type and flags and source code.) …
关于std::atomic,C++11 标准规定,存储到原子变量将在“合理的时间内”对该变量的加载变得可见。
从 29.3p13 开始:
\n\n\n\n\n实现应该使原子存储在合理的时间内对原子加载可见。
\n
然而,我很想知道在处理基于 MESI 缓存一致性协议(x86、x86-64、ARM 等)的特定 CPU 架构时实际会发生什么。
\n\n如果我对 MESI 协议的理解是正确的,那么一个核心总是会立即读取先前写入/正在由另一个核心写入的值,可能是通过窥探它。(因为写入值意味着发出 RFO 请求,这反过来会使其他缓存行无效)
\n\n这是否意味着当一个线程 A 将一个值存储到 an 中时std::atomic,另一个连续对该原子进行加载的线程 B 实际上总是会观察到 A 在 MESI 架构上写入的新值?(假设没有其他线程正在对该原子执行操作)
\xe2\x80\x9csuccessively\xe2\x80\x9d 我的意思是在线程 A 发出原子存储之后。(修改顺序已更新)
\nx86 ×6
cpu-cache ×5
c++ ×4
performance ×2
x86-64 ×2
arm ×1
assembly ×1
concurrency ×1
intel ×1
memory-model ×1
mesi ×1
optimization ×1
rust ×1
stdatomic ×1