这篇文章还是很深的,很多看不懂,可能要以后回头再看了。这里摘几个简单一点的。
1. 指令预取
根据这篇文章描述,现代cpu架构中,最重要的是指令预取和缓存的使用。
如果cpu在读取连续地址的数据时,能很好的利用指令预取。有一个矩阵乘法的例子。
最初的代码是:
for (i = 0; i < N; ++i)
for (j = 0; j < N; ++j)
for (k = 0; k < N; ++k)
res[i][j] += mul1[i][k] * mul2[k][j];
这里mul2的地址不是顺序读取的。因此做一下修改:
double tmp[N][N];
for (i = 0; i < N; ++i)
for (j = 0; j < N; ++j)
tmp[i][j] = mul2[j][i];
for (i = 0; i < N; ++i)
for (j = 0; j < N; ++j)
for (k = 0; k < N; ++k)
res[i][j] += mul1[i][k] * tmp[j][k];
这段代码比上面的多了部分拷贝,但比上面的代码快75%!
2.CPU缓存
缓存是另外一个重要的优化手段。现代的内存有两种内存,静态内存和动态内存,静态内存可以做到速度接近cpu但价格昂贵,不能做得很大。所以一般是用静态内存作为cpu cache。最小并且最快的是L1 cache, 然后是L2, L3。一般从内存读取到cpu都会先到cache中找,找不到再从下一级cache甚至内存去找。读取在cache中找不到称为cache miss。
cache line是cache的基本单位,读取时都是以这个为单位读取的,在linux上可以用sysconf (_SC_LEVEL1_DCACHE_LINESIZE)来获得。
因此对于一个大的结构,建议是让意义练习紧密的部分放在一起,这样一个字段读取进去之后相邻的字段往往也在cache里面了。
其它建议是:最重要的字段放在开头,尽量让字段读取按照结构成员的顺序来(可能是为了指令预取)。
除了数据,指令也可以被缓存。可执行程序越小越容易使用缓存。
3.多线程优化
3.1 并发优化
在加载的时候尽量不要让各个数据结构使用单独的cache line, 如果一个数据结构大小不足以填满一个cache line,可以使用padding来填满。
比如定义一个结构
struct data
{
int bar;
int offset;
};
可以定义一个cachedata来把data包起来:
template<class T>
struct cachedata
{
T value;
char padding[CLSIZE- sizeof(T)];
};
这里CLSIZE是L1 cache line的大小,可以在程序中用define指定。