我一直在阅读Ulrich Drepper的“ 每个程序员应该知道什么内存 ”和第3.3.2节“caching效果的度量” (页面中间部分),这给我的印象是访问结构的任何成员会导致整个结构被拉进CPUcaching。
它是否正确? 如果是这样,硬件如何知道这些结构的布局? 还是由编译器生成的代码以某种方式强制整个结构被加载?
或者是由于结构分布在更多内存页面导致的TLB未命中,使用更大结构的速度减慢?
Drepper使用的示例结构是:
struct l { struct l *n; long int pad[NPAD]; };
其中, sizeof(l)
由NPAD
确定等于NPAD
或31,得到NPAD
和248字节的结构,并假定高速caching行是64字节和4k页。
只要迭代链表,就会随着结构的增长而显着变慢,尽pipe除了指针之外没有别的东西被实际访问。
硬件完全不知道结构。 但是,确实缓存中的硬件负载在实际访问的字节周围有一些字节。 这是因为缓存行有一个大小。 它不能以逐字节访问的方式工作,而是一次处理16个字节的数据。
在订购结构的成员时必须小心,以便经常使用的成员彼此接近。 例如,如果你有以下的结构:
struct S { int foo; char name[64]; int bar; };
如果成员变量foo和bar经常被使用,硬件将会缓存foo周围的字节,当你访问bar时,它将不得不加载围绕bar的字节。 即使foo周围的这些字节从不使用。 现在重写你的结构如下:
struct S { int foo; int bar; char name[64]; };
当你使用foo的时候,硬件会在缓存中加载foo的字节。 当你使用bar时,bar将已经在缓存中,因为bar包含在foo的字节中。 CPU不必等待栏位在缓存中。
答案是 :访问一个单一的结构成员不会拉高速缓存中的整个结构,但将结构的其他成员拉入缓存。
硬件不知道结构的布局,只是将被访问的成员周围的一些字节加载到缓存中。 是的,从较大的结构放缓是因为他们将分布在更多的缓存行。
访问一个struct成员不会比访问内存中的任何其他区域更多的性能损失。 实际上,如果您访问同一区域中的多个结构成员,可能会提高性能,因为其他成员可能会被第一次访问缓存。
通常,L1高速缓存使用虚拟地址 ,如果访问struct
成员,则特定数量的字节会进入高速缓存(一个高速缓存行 ,大小通常在8到512字节之间)。 由于所有struct
成员在内存中并排排列,因此整个结构进入缓存的机会有点大(取决于sizeof(struct your_struct)
)…
虽然CPU可以愉快地处理小至一个字节的加载和存储,但是缓存只处理“缓存行”大小的数据。 在计算机体系结构的教科书中,这也被称为“块大小”。
在大多数系统上,这是32或64字节。 它可能不同于一个CPU,甚至有时从一个高速缓存到另一个高速缓存。
此外,一些CPU执行预测性预取; 这意味着如果您按顺序访问cacheline 5和6,它将尝试加载cacheline 7而不要求它。
“只要迭代链表,就会随着结构的增长而慢得多,即使指针实际上没有被访问。
NPAD = 0,每个缓存行包含8个列表节点,所以你可以看到为什么这是最快的。
在NPAD = 7,15,31的情况下,每个列表节点只需要加载一个缓存行,并且您可能期望它们都是相同的速度 – 每个节点只有一个缓存未命中。 但现代内存管理员将会进行投机缓存。 如果它有多余的容量(这可能是因为使用现代内存,它可以并行执行多次读取),那么它将开始加载内存到您正在使用的内存附近。 虽然它是一个链表,但如果你用任何明显的方式构造它,那么你很有可能依次访问内存。 因此,列表节点越接近内存,缓存越有可能成为您已经拥有的内容。
在最糟糕的情况下,当你使用它的时候你的内存被从交换中拉出来,你的程序将受到磁盘I / O的限制。 通过列表进度的速度可能完全取决于每个页面有多少个节点,并且您可能会看到花费的时间与节点的大小成正比,最高可达4k。 但是我还没有尝试过,操作系统会随着交换机巧妙地进行交换,就像MMU对主内存很聪明一样,所以不一定那么简单。