我想知道是否有任何英特尔专家可以告诉我STD和STA在英特尔Skylake内核方面的区别。
在英特尔优化指南中,有一张图片描述了英特尔酷睿的“超标量端口”。
这是PDF。图片在第40页上。
。
这是第78页的另一张图片,该图片描述了“存储地址”和“存储数据”:
使用存储的数据地址准备存储转发和存储退出逻辑。
准备存储转发和存储退出逻辑以及要存储的数据。
考虑到Skylake可以在每个时钟周期执行一次#1 3x,但是在每个时钟周期只能执行一次#2,我很好奇这两者之间的区别。
在我看来,将存储转发到数据地址是“自然的”。但是我无法理解何时进行数据存储转发(又名:STD /端口4)。是否有任何组装/优化专家可以帮助我准确了解STD和STA之间的区别?
写合并缓冲区一直是Intel CPU的功能,至少可以追溯到Pentium 4甚至更早。基本思想是这些高速缓存行大小的缓冲区将写操作收集到同一高速缓存行中,因此可以将它们作为一个单元进行处理。作为它们对软件性能的影响的一个示例,如果您不编写完整的缓存行,则可能会遇到性能下降的情况。
例如,在《Intel 64和IA-32体系结构优化参考手册》中,“ 3.6.10写合并”部分以以下说明(加了重点)开头:
写合并(WC)通过两种方式提高性能:
•在对第一级缓存的写入未命中时,它允许在从缓存/内存层次结构的更深层读取所有权(RFO)之前对该同一个缓存行进行多个存储。然后读取剩余的行,将尚未写入的字节与返回行中的未修改字节合并。
•写合并允许将多个写组合在一起,并作为一个单元在高速缓存层次结构中进一步写出。这样可以节省端口和总线流量。节省流量对于避免部分写入未缓存的内存特别重要。
有六个写合并缓冲区(在奔腾4和Intel Xeon处理器上,CPUID签名的族编码为15,模型编码为3;有8个写合并缓冲区)。这些缓冲区中的两个可以写出更高的缓存级别,并释放出来以供其他写未命中。保证只有四个写合并缓冲区可同时使用。写合并适用于存储器类型WC;它不适用于内存类型UC。
Intel Core Duo和Intel Core Solo处理器的每个处理器内核中都有六个写合并缓冲区。基于英特尔酷睿微体系结构的处理器在每个内核中都有八个写合并缓冲区。从英特尔微体系结构代码名称Nehalem开始,有10个缓冲区可用于写合并。
写合并缓冲区用于所有存储器类型的存储。它们对于写入未缓存的内存特别重要...
我的问题是,在使用普通存储区(非临时存储区以外的其他存储区,即您所使用的存储区)时,写合并是否适用于WB内存区域(即您在用户程序中使用99.99%的时间的“普通”内存)正在使用99.99%的时间)。
上面的文字很难准确解释,因为自Core Duo时代以来没有更新过。您有说写梳理的部分“适用于WC存储器,但不适用于UC”,但是当然不包括所有其他类型,例如WB。后来,您发现“ [WC对于写入未缓存的内存特别重要”,这似乎与“不适用于UC部分”相矛盾。
那么,现代英特尔芯片上用于正常存储到WB存储器的写合并缓冲区是否有效?
由于等待全局状态的全局同步几乎会一直不必要地停止所有执行,因此多处理器系统会无序地执行“实际”内存操作(那些操作会影响最终执行,而不仅仅是推测执行)。另一方面,从每个L1高速缓存开始,从允许的行为角度来看,内存系统似乎是完全同步,一致且平坦的(允许语义)。显然,时间取决于缓存的大小和行为。
因此,在一个CPU上,一个极端被称为“寄存器”,根据定义,它们是私有的,而在另一个极端上,则存在共享的内存。令人遗憾的是,在具有特殊命名或寻址模式的寄存器的微不足道的空间之外,存储器始终是全局的,共享的和全局同步的,并且实际上完全受制于所有限制,即使该存储器用作未命名的寄存器也是如此。其目的是存储比少数寄存器中容纳的数据更多的数据,而不会被其他线程检查(除非使用ptrace进行调试,因为ptrace显然会停止,停止,序列化并存储执行的完整可观察状态)。
在现代计算机(现代=可以合理地支持C ++和Java的计算机)上,情况总是如此吗?
专用L1高速缓存为什么不为那些仅由特定内核使用的存储单元提供类似寄存器的语义?高速缓存必须跟踪共享的内存,无论如何。当需要对内存操作进行严格的全局排序时,不必暂停此类本地数据的内存操作,因为没有其他内核在观察它,并且如果需要,缓存可以暂停此类外部访问。高速缓存将只需要知道哪些存储单元是私有的(不可全局读取),直到出现混乱的操作停顿为止,这将使之保持一致(高速缓存可能需要一种方法来请求核心对操作进行序列化并发布一致的状态在记忆中)。
是否所有CPU都停滞不前并同步篱笆或同步操作上的所有内存访问?
内存可以用作几乎不受限制的寄存器资源吗?
memory cpu-architecture cpu-registers memory-barriers cpu-cache
我以为我了解L1D写未命中是如何处理的,但仔细想想却让我感到困惑。
这是一个汇编语言片段:
;rdi contains some valid 64-bytes aligned pointer
;rsi contains some data
mov [rdi], rsi
mov [rdi + 0x40], rsi
mov [rdi + 0x20], rsi
Run Code Online (Sandbox Code Playgroud)
假设[rdi]和[rdi + 0x40]行在 l1d 中不处于 Exclusive 或 Modified 状态。然后我可以想象以下动作序列:
mov [rdi], rsi 退休。mov [rdi], rsi尝试将数据写入 l1d。RFO 启动,数据放入 WC 缓冲区。mov [rdi + 0x40], rsi退休(mov [rdi], rsi已经退休,所以有可能)mov [rdi + 0x40], rsi 为连续的缓存行启动 RFO,数据被放入 WC 缓冲区。mov [rdi + 0x20], rsi退休(mov [rdi …假设我们有一个处理器,它有两个内核(C0 和 C1)和一个从k最初由 C0 拥有的地址开始的缓存线。如果 C1 在第 8 字节槽上发出一条存储指令k,是否会影响在 C1 上执行的后续指令的吞吐量?
intel优化手册有以下一段
当一条指令将数据写入内存位置 [...] 时,处理器会确保包含该内存位置的行位于其 L1d 缓存中 [...]。如果缓存线不存在,它会使用 RFO 请求 [...] RFO 从下一级获取数据,并在指令退出后存储数据。因此,存储延迟通常不会影响存储指令本身
参考以下代码,
// core c0
foo();
line(k)->at(i)->store(kConstant, std::memory_order_release);
bar();
baz();
Run Code Online (Sandbox Code Playgroud)
从英特尔手动使得报价我认为在上面的代码,代码的执行将看起来像是商店基本上是一个空操作,和结束之间会不会影响延迟foo()和开始bar()。相比之下,对于下面的代码,
// core c0
foo();
bar(line(k)->at(i)->load(std::memory_order_acquire));
baz();
Run Code Online (Sandbox Code Playgroud)
结束foo()和开始之间的延迟bar()会受到加载的影响,因为以下代码将加载结果作为依赖项。
这个问题主要与英特尔处理器(在 Broadwell 系列或更新版本中)如何在上述情况下工作有关。此外,特别是关于如何将类似于上述的 C++ 代码编译为这些处理器的汇编。
我正在阅读 Cortex M4 TRM 以了解指令执行周期。但是,那里有一些令人困惑的描述
STR Rx,[Ry,#imm]是总是一个周期,这是因为地址生成在初始循环中执行,并且所述数据存储在同一时间作为下一个指令正在执行执行。
如果存储到
write buffer,并且write buffer已满或未启用,则下一条指令将延迟,直到存储完成。
如果存储不是到
write buffer,例如代码段,并且该事务停止,则只有在完成之前执行另一个加载或存储操作时才会感受到对计时的影响
LDR可以通过跟随LDR和进行流水线化STR,但STR不能通过跟随指令进行流水线化。其他指令在
STR使用寄存器偏移后无法流水线化。STR 只能在跟随 LDR 时被流水线化,但在存储之后什么都不能流水线化。由于写缓冲区的原因,即使停止STR通常也只需要两个周期
更具体的让我困惑的是:
一季度。1和2似乎相互冲突,STR实际需要多少个周期,1 还是 2?(虽然我的实验显示为 1)
Q2。2表示如果存储通过write buffer并且不可用,它仍然会停止流水线,但是如果存储绕过它,流水线可能只在加载/存储指令跟随时停止。闻起来write buffer只会让事情变得更糟。这是有悖常理的。
Q3。3表示STR不能与后续指令流水线化,但是2 …
与我之前的问题类似,请考虑以下代码
-- Initially --
std::atomic<int> x{0};
std::atomic<int> y{0};
-- Thread 1 --
x.store(1, std::memory_order_release);
-- Thread 2 --
y.store(2, std::memory_order_release);
-- Thread 3 --
int r1 = x.load(std::memory_order_acquire); // x first
int r2 = y.load(std::memory_order_acquire);
-- Thread 4 --
int r3 = y.load(std::memory_order_acquire); // y first
int r4 = x.load(std::memory_order_acquire);
Run Code Online (Sandbox Code Playgroud)
是怪异的结果 r1==1, r2==0,并r3==2, r4==0有可能在C ++ 11内存模型下,这种情况下?如果我要全部替换std::memory_order_acq_rel成该std::memory_order_relaxed怎么办?
在x86上,这样的结果似乎是被禁止的,请参见此SO问题,但我一般是在询问C ++ 11内存模型。
奖励问题:
我们都同意,与std::memory_order_seq_cst该怪异的结果不会在C ++ 11被允许。现在,赫伯·萨特(Herb Sutter)在他著名的- …
在阅读一致性模型(即 x86 TSO)时,作者通常会使用包含大量 CPU、相关存储缓冲区和私有缓存的模型。
如果我的理解是正确的,存储缓冲区可以被描述为队列,CPU 可以在其中放置他们想要提交到内存的任何存储指令。因此,顾名思义,它们是store缓冲区。
但是当我阅读这些论文时,他们倾向于谈论加载和存储的交互,诸如“稍后加载可以通过较早存储”之类的陈述有点令人困惑,因为它们几乎似乎在谈论存储缓冲区将同时具有加载和存储,当它没有时 - 对吗?
所以也必须有一个他们没有(至少明确地)谈论的负载存储。另外,这两者必须以某种方式同步,所以两者都知道什么时候可以从内存加载并提交到内存——或者我是否遗漏了什么?
任何人都可以对此有所了解吗?
编辑:
让我们看一下“内存一致性和缓存一致性入门”中的一段:
为了理解 TSO 中原子 RMW 的实现,我们将 RMW 视为紧跟在存储之后的负载。由于 TSO 的排序规则,RMW 的负载部分无法通过较早的负载。乍一看,RMW 的加载部分可能会传递写入缓冲区中较早的存储,但这是不合法的。如果 RMW 的加载部分通过较早的存储,则 RMW 的存储部分也必须通过较早的存储,因为 RMW 是原子对。但是由于在 TSO 中不允许 store 相互传递,因此 RMW 的负载部分也不能通过较早的 store
进一步来说,
由于 TSO 的排序规则,RMW 的负载部分无法通过较早的负载。乍一看,RMW 的加载部分可能会传递写入缓冲区中较早的存储
所以他们指的是在写入缓冲区中相互交叉的负载/存储(我认为这与存储缓冲区相同?)
谢谢
std::atomic_uint64_t writing_ {0};
std::atomic_uint64_t reading_ {0};
std::array<type, size> storage_ {};
bool try_enqueue(type t) noexcept
{
const std::uint64_t writing {
writing_.load(std::memory_order::memory_order_relaxed)};
const auto last_read {reading_.load(std::memory_order::memory_order_relaxed)};
if (writing - last_read < size) {
storage_.at(writing & (size - 1)) = t;
writing_.store(writing + 1, std::memory_order::memory_order_release);
return true;
}
else
return false;
}
Run Code Online (Sandbox Code Playgroud)
据我所知,在上面的代码中,如果条件评估为false,则任何线程都不可能观察到对共享存储的写入。在条件排序之后不能将某操作视为之前发生的操作是否正确?还是我完全误解了这一点,实际上是否可能发生这种事情(也许通过推测执行?)?
更具体地说,处理器可以推测性地执行写入操作(当条件最终将为假时),另一个线程观察到已发生写入操作,然后第一个线程丢弃推测性写入操作吗?
(注:这是单一生产者单一消费者)
据我了解,当 CPU 推测性地执行一段代码时,它会在切换到推测性分支之前“备份”寄存器状态,以便如果预测结果错误(使分支无用)——寄存器状态将是安全恢复,而不会破坏“状态”。
所以,我的问题是:推测执行的 CPU 分支是否可以包含访问 RAM 的操作码?
我的意思是,访问 RAM 不是“原子”操作——如果数据当前不在 CPU 缓存中,那么从内存中读取一个简单的操作码可能会导致实际的 RAM 访问,这可能会变成一个非常耗时的操作,从 CPU 的角度来看。
如果在推测分支中确实允许这种访问,它是否仅用于读取操作?因为,我只能假设,如果一个分支被丢弃并执行“回滚”,根据它的大小恢复写操作可能会变得非常缓慢和棘手。而且,可以肯定的是,至少在某种程度上支持读/写操作,因为寄存器本身,在某些 CPU 上,据我所知,物理上位于 CPU 缓存上。
所以,也许更精确的表述是:推测执行的一段代码有什么限制?
concurrency ×4
assembly ×3
c++ ×3
cpu-cache ×3
performance ×3
x86 ×3
intel ×2
memory-model ×2
arm ×1
c++11 ×1
cortex-m ×1
cpu ×1
lock-free ×1
memory ×1
optimization ×1
rfo ×1
stdatomic ×1
x86-64 ×1