我正试图建立一种直觉,即如何编写有效的代码,以最大程度地降低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)是否会影响此处的优化?
答案 0 :(得分:1)
我会尽力回答您的问题。
编译器可能会将MyClass对象对齐8个以上,特别是如果它们在静态内存中,则x和y [0]可能位于同一高速缓存行中。大多数编译器将对齐大对象而不是对齐小对象。
如果MyClass对象在本地声明,它将存储在堆栈中。在这种情况下,整个对象很可能在L1缓存中。
z [0]可能由硬件预取,但可能还不够早。
由于前五行是独立的,因此它们可能会无序执行。这意味着一行上的任何高速缓存未命中都不会减慢下一行的速度。
您是正确的,因为* pq =可以防止乱序执行,因为(通常)编译器不知道* pq是否是某些其他变量的别名。
'a'的加载速度不一定比'q'快。例如,如果它们都在二级缓存中,则它们将同样快速地加载。它不取决于距离,而是取决于它们自上次接触以来的时间。如果两者都在主RAM中并且q距离很远,则TLB丢失或页面边界当然可能会影响获取时间。
如果b与a处于同一缓存行,它将保持缓存状态,但是直到* pq的地址被解析并且发现b上没有别名时,您才能访问b。
如果我们假设数据缓存是瓶颈,而代码缓存不是瓶颈,那么内联calc函数就没有什么区别。