微信搜索:编程笔记本
微信搜索:编程笔记本
微信搜索:编程笔记本
点击上方蓝字关注我,我们一起学编程
欢迎小伙伴们分享、转载、私信、赞赏
最近在学习一些内存管理的内容,发现一个比较有趣的现象,在这里跟大家分享一下。
首先,我们来看一段对二维数组的求和程序:
/* sum_by_row.c */
#include <stdio.h>
#define N 20480
int arr[N][N] = {1};
int main()
{
int sum = 0;
for (int i = 0; i < N; ++i) {
for (int j = 0; j < N; ++j) {
sum += arr[i][j];
}
}
return sum;
}
可以看到,上面的程序是按行求和的,还有一种方式是按列求和:
/* sum_by_col.c */
#include <stdio.h>
#define N 20480
int arr[N][N] = {1};
int main()
{
int sum = 0;
for (int i = 0; i < N; ++i) {
for (int j = 0; j < N; ++j) {
sum += arr[j][i];
}
}
return sum;
}
从朴素的直觉来看,这两种方式应该没什么区别。下面我们就来运行一下,并用 time
进行计时。
微信搜索:编程笔记本
微信搜索:编程笔记本
微信搜索:编程笔记本
➜ $ gcc sum_by_row.c -o sum_by_row
➜ $ time ./sum_by_row
./sum_by_row 0.93s user 0.43s system 99% cpu 1.358 total
➜ $ gcc sum_by_col.c -o sum_by_col
➜ $ time ./sum_by_col
./sum_by_col 2.36s user 0.08s system 97% cpu 2.500 total
从运行结果来看,按行求和的程序执行时间是 0.93 秒,按列求和的程序执行时间是 2.36 秒。为什么会有这么大的差别呢?是不是很反直觉?
朴素的解释是这样的:
- 数组的数据是存放在物理内存中的,当我们访问数组元素时,需要进行一系列的转换与查找工作。
- 当我们访问
arr[i][j]
时,需要先找到该元素与实际的物理地址之间的映射关系,然后根据这个映射关系,前往特定的物理地址中访存数据。 - 物理内存的某个地方存放着所有的这种映射,地址转换缓存中也保存着一小部分的映射。
- 从地址转换缓存中查找映射关系会比从物理内存中查找映射关系快得多。
- 在访存数据时,会首先在地址转换缓存中查找映射关系,若查不到再去物理内存中查找。
有了这些先验知识,我们上面两个程序段的差异就不难理解了。我们知道,数组在内存中是连续存储的,其存储顺序为:
arr[0][0], arr[0][1], ... arr[0][N-1],
arr[1][0], arr[1][1], ... arr[1][N-1],
... ...
arr[N-1][0], arr[N-1][1], ... arr[N-1][N-1]
可以看到,arr[0][0]
距离 arr[0][1]
更近,arr[0][0]
距离 arr[1][0]
更远。
在内存管理中有一个分页的概念,就是会将一段连续的内存数据放在一页上。读取一个数据时会将一整页的数据全部读出。
当我们读取完 arr[0][0]
后,再读取 arr[0][1]
时,由于它和 arr[0][0]
在一页上,会发现这个元素到物理地址的映射关系已经存在地址转换缓存中了(缓存命中),所以访问这个元素就是非常快。
反之,arr[0][0]
后,再读取 arr[1][0]
时,由于它和 arr[0][0]
相距很远,所以他们不在一页上,其映射关系也不在地址转换缓存中,所以我们就需要从物理内存中查找这个映射关系,所以访问就会慢很多。
看完了上面这个例子,我们知道,一个良好的程序应该要好好利用局部性原理,提高缓存命中率。
微信搜索:编程笔记本
微信搜索:编程笔记本
微信搜索:编程笔记本
本博客可能随时删除或隐藏,请关注微信公众号,获取永久内容。