我正在读一本关于操作系统的书,它给出了一些我最常理解的C语言示例。我现在看的例子显示了两个几乎相同的代码片段,它们将在一个虚构的系统上运行......
int i, j;
int [128] [128] data;
for (j = 0; j < 128; j++)
for (i = 0; i < 128; i++)
data [i] [j] = 0;
第二段代码
int i, j;
int [128] [128] data;
for (i = 0; i < 128; i++)
for (j = 0; j < 128; j++)
data [i] [j] = 0;
在这个特定的系统中,第一部分代码会导致16k页面错误,而第二部分只导致128页。
如果这是一个愚蠢的问题,我很抱歉,但在我使用.NET的经历中,我一直都没有意识到内存。我只是创建一个变量,它在某个地方,但我不知道在哪里,我不在乎。
我的问题是,.NET如何与这个虚构系统中的这些C示例进行比较(页面大小为128个字,数组的每一行占用一整页。在第一个示例中,我们在第1页设置了一个int,然后是第2页上的一个int,等等......第二个例子设置了第1页的所有整数,然后是第2页的所有整数,等等......)
此外,虽然我认为我理解为什么代码会产生不同级别的分页,但是我能用它做些什么呢?页面的大小是否取决于操作系统?作为一般的经验法则,尽可能连续地访问数组中的内存是否会被带走?
答案 0 :(得分:7)
虚拟操作系统可用于演示原理,但我知道实际操作系统并非如此。如果页面在使用后立即被取消映射,则只能获得16K页面错误。虽然技术上可能会发生这种情况,但机器必须处于主要负载之下,迫切需要RAM来支持一组正在运行的进程。那时你只是不再关心perf,无论如何机器都会慢下来爬行。技术术语是“thrashing”。
在这段代码中你还没有提到更重要的事情。 1级CPU缓存在访问数组元素的速度方面起着重要作用。在第一次访问时从内存中填充高速缓存行(通常为64字节)。在下一个内存地址访问下一个元素非常便宜,数据已经在缓存中。访问外部索引首先更改的数组是非常昂贵的,它需要另一个高速缓存行填充,这可能需要数百个周期,最坏的情况。由于驱逐包含阵列数据的现有缓存行的风险很大,因此第一级缓存不是很大。要做到这一点,需要了解运行时如何在内存中对数组元素进行排序。
答案 1 :(得分:2)
你的两个样本是相同的,但大概你想要的是:
int i, j;
int [128] [128] data;
for (i = 0; i < 128; i++)
for (j = 0; j < 128; j++)
data [i] [j] = 0; // <--- i,j
第二段代码
int i, j;
int [128] [128] data;
for (i = 0; i < 128; i++)
for (j = 0; j < 128; j++)
data [j] [i] = 0; // <--- j,i
假设页面大小为128 * sizeof(int):
在一种情况下,您将按顺序迭代数组。在这种情况下,最坏情况的页面错误数是pages = 1中数组的大小。
在另一种情况下,您将在每次迭代时跳转到新页面,因此在最坏的情况下,您可能会在循环的每次迭代中出现页面错误。
.NET中的情况完全相同。
答案 2 :(得分:0)
您获得的页面错误数量取决于您访问的页面数量,这些页面不在内存中。
堆栈上的页面更有可能在内存中,除非这是第一次使用堆栈达到这个程度。
假设您的页面大小与许多系统上的页面大小相同,并且没有任何页面在内存中,您将访问128 * 128 * 4字节或64 KB,即16页。注意:如果此结构不在页面边界上开始,则该结构可能跨越17页,在这种情况下,第一页将在内存中。
只要您访问每个页面,访问它们的方式或顺序都无关紧要。
可能是您在谈论缓存未命中,而您访问数据的顺序可能会有所不同。在这种情况下,最好沿最后一个索引逐步访问数据。即对于a[i][j]
,您希望内循环使用j
但是如果您的缓存足够大,这可能无关紧要。对于不适合快速缓存的真正大型结构,它可以有很大的不同。