【问题标题】:Optimization of CPU pipelining and cache access优化 CPU 流水线和缓存访问
【发布时间】:2019-08-28 05:44:15
【问题描述】:

我正在尝试建立关于如何编写有效代码以最小化 CPI(每条指令周期)和最小化缓存未命中和后端绑定性能的直觉。我想了解数据局部性和流水线如何交互。

我知道这些事情很多都依赖于特定的硬件,无法肯定地回答。尽管如此,我仍然希望对“典型”台式计算机上“可能”发生的事情有一些合理的指导,使用用 gcc 或 icpc 和 -O2 等通用编译器编译的程序。

考虑以下(人为的)代码。这段代码的目的是设置不同的场景来说明问题。假设一个高速缓存行是 64 字节。 (编辑)-为了澄清,让我们假设在执行 calc 时这些变量都不在任何级别的缓存中。响应正确地指出,如果已经缓存了任何内容,则会影响结果。

class MyClass {
public:
    MyClass() {};
    inline void calc(const double in);
private:
    double x,y[10],z[32],a,b;
};

inline void MyClass:calc(const double in) 
{
    x = 5 + in;
    y[0] = 10 + in;
    z[0] = 25 + in;
    a = 50 + in;
    q = 100 + in;//q is a variable from global scope that is not already in the cache
    *pq = 200 + in;//*pq is a pointer from global scope that is not already in the cache
    q2 = 300 + in;//q2 is a variable from global scope that is not already in the cache
    b = 400 + in;
    cout << x << ", " << y[0] << ", " << z[0] << ", " << a << ", " << q << ", " << *pq << ", " << q2 << "," << b;
}

当 calc 运行时,x 和 y[0] 可能在同一个缓存行上,所以 y[0] 会被缓存命中? z[0] 在下一个高速缓存行。但是,它可能会受益于“下一个缓存行”预取并且也是缓存命中? a 是几个缓存行,然后 q 是来自全局范围的变量,位于内存中的某个远程位置。即使 a 是 z[0] 的几条高速缓存行,我们是否应该期望它比 q 更快地加载到处理器中?是否会在更高级别的缓存中进行某种预取,以防止 a 成为完全缓存未命中? q 肯定需要从主内存中提取,因为它来自内存中的远程位置。 *pq 和 q 也需要自己从主内存中提取。

所以我的期望是会发生这样的事情:y[0] 将加载 L1 缓存命中,z[0] 可能加载 L1 或 L2 缓存命中,a 可能是也可能不是 L2 缓存命中,并且 q 肯定是缓存未命中。如果 q 太远以至于它也会导致 TLB 缓存未命中怎么办?那么它会更慢吗?我对这一切的理解正确吗?

流水线如何影响这一点?处理器可以流水线化一系列内存加载,在前一行代码完成之前将 q 从主内存带入高速缓存。因此,在实践中,我们是否会观察到使用位于内存中远程位置的变量 q 的速度减慢?

请注意,calc 是内联的,因此它的指令可能构成调用它的函数中更大的操作链的一部分,我认为这有助于流水线操作。

变量 *pq 如何影响流水线?编译器不知道 *pq 是指向 q2 还是指向 b 的指针。这会影响流水线的功效吗?

最后,我们到达 b。它与 a 位于同一缓存行上。自从我们上次使用 a 以来,我们不得不做几件事,但希望它仍然在 L1 缓存中并且命中?同样,使用指针 *pq(可能指向 b)会影响这里的优化吗?

【问题讨论】:

  • xy[0] 在同一缓存行上。没有这样的保证。 MyClass 的对象通常不与缓存边界对齐(扩展对齐要求可能会强制这样做)。无论如何,我建议阅读What Every Programmer Should Know About Memory
  • x 和 y[0] 上的公平点。我应该说它们“可能”在同一个缓存行上。另外,感谢您对阅读的建议。事实上,我昨天读了那篇文章。这个问题的动机是我试图综合我所读到的内容,确认我的理解是正确的,并澄清挥之不去的问题。
  • 很公平:)。问题是您一次提出“百万”个问题,而 IMO 中的许多问题无法简单地回答(或者,您可以就这些主题撰写长篇研究文章)。例如,您不能对如何应用硬件预取做出任何假设。它不仅依赖于架构,而且在相同机器代码的执行之间可能存在很大差异。您唯一能做的就是分析您的程序,例如使用硬件计数器。
  • "CPI(每条指令的周期数)" - 我认为你倒退了。人们通常希望最大化 IPC(每时钟指令);-)
  • @JesperJuhl - 感谢您指出错字。我将其更改为“最小化”CPI。

标签: c++ caching cpu hpc cpu-cache


【解决方案1】:

我会尽力回答你的问题。

编译器可能会将 MyClass 对象对齐超过 8 个,尤其是当它们位于静态内存中时,因此 x 和 y[0] 很可能位于同一缓存行中。大多数编译器将对齐大对象而不是小对象。

如果 MyClass 对象在本地声明,它将存储在堆栈中。在这种情况下,很可能整个对象都在 L1 缓存中。

z[0] 可能被硬件预取,但可能还不够早。

前五行可能会乱序执行,因为它们是独立的。这意味着一行上的任何缓存未命中都不会减慢下一行的速度。

*pq = something 可以防止乱序执行,因为(在一般情况下)编译器不知道 *pq 是否是其他一些变量的别名。

'a' 的加载速度不一定比 'q' 快。例如,如果它们都在二级缓存中,它们的加载速度将相同。它不取决于距离,而是取决于他们最后一次接触的时间。如果 TLB 未命中或页面边界都在主 RAM 中并且 q 很远,那么当然可能会影响获取时间。

如果b和a在同一个缓存行,b将保持缓存,但是直到*pq的地址被解析并且发现不在b上的别名,你才能访问b。

如果我们假设数据缓存是瓶颈,而代码缓存不是,那么内联 calc 函数在这里没有区别。

【讨论】:

  • 谢谢!这很有用。我特别喜欢你关于乱序执行的观点。我想要求澄清一下。我的意图是在执行 calc 之前,这些变量都不在任何级别的缓存中。因此,在这些条件下,问题是:(1)由于它(适度)接近 z,潜在的加载速度会更快吗? (2) 如果整个程序使用大量内存并且 q 导致 TLB 未命中,q 是否可能加载非常慢?(3) 编译器是否能够通过预取到管道中来部分或完全缓解这些延迟?
  • @pah52。 (1) no 'a' 不会因为接近而加载得更快。 (2) 是的,TLB 未命中会使 q 加载变慢。 (3) 不,编译器通常不会插入预取指令。
  • 谢谢!如果 TLB 未命中,我猜 'a' 的加载速度可能比 q 快,但有趣的是,否则没关系。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-07-16
  • 1970-01-01
  • 1970-01-01
  • 2016-02-14
  • 2013-06-14
  • 1970-01-01
  • 2017-02-19
相关资源
最近更新 更多