优化CPU流水线和缓存访问

时间:2019-04-06 14:59:27

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

我正试图建立一种直觉,即如何编写有效的代码,以最大程度地降低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缓存命中,L2缓存命中或可能不是,和q肯定是缓存未命中。如果q离得太远而又导致TLB缓存未命中怎么办?那会更慢吗?我对这一切的理解正确吗?

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

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

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

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

1 个答案:

答案 0 :(得分:1)

我会尽力回答您的问题。

编译器可能会将MyClass对象对齐8个以上,特别是如果它们在静态内存中,则x和y [0]可能位于同一高速缓存行中。大多数编译器将对齐大对象而不是对齐小对象。

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

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

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

您是正确的,因为* pq =可以防止乱序执行,因为(通常)编译器不知道* pq是否是某些其他变量的别名。

'a'的加载速度不一定比'q'快。例如,如果它们都在二级缓存中,则它们将同样快速地加载。它不取决于距离,而是取决于它们自上次接触以来的时间。如果两者都在主RAM中并且q距离很远,则TLB丢失或页面边界当然可能会影响获取时间。

如果b与a处于同一缓存行,它将保持缓存状态,但是直到* pq的地址被解析并且发现b上没有别名时,您才能访问b。

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