本篇针对矩阵转置中的不同写内存方式(行连续与列连续)分别进行测试,并通过抓包验证造成性能差异的原因。
其中,对1024*1024尺寸的二维矩阵转置,执行耗时情况如下:
写列连续 2.68945 ms
写行连续 1.5498 ms
先上结论:缓存一致性算法在读写内存的实现细节不同,写内存实现更加复杂。在实现类似矩阵转置功能,即内存读写方面存在映射关系且无法保证内存访问都是连续操作情况下,可以尝试使用优先保证写内存连续,读内存跳跃的策略来保证性能。
矩阵转置代码实现
由于矩阵转置的逻辑功能是将矩阵行列对调,同时矩阵在内存中通常是按照行内存连续的方式存储的。因此,原始矩阵和目标矩阵之间转换的过程中,一定存在一个矩阵的内存访问时不连续的。
为了让程序执行时间足够长,使执行时间足够稳定,同时便于抓包分析,重复执行代码1024次。
矩阵尺寸相关宏定义
#define NROW 1024
#define NCOL 1024
#define NSLICE NROW*NCOL
#define REPEAT 1024
写内存行连续的代码实现
void TargetRowSeqTranspose(unsigned char* pSource, unsigned char* pTarget)
{
//Target 连续访问行
//TargetRowSeqTranspose Time (ms) 1.5498
clock_t begin = clock();
for (int i = 0; i < REPEAT; ++i)
{
for (int irow = 0; irow < NROW; ++irow)
{
for (int icol = 0; icol < NCOL; ++icol)
{
pTarget[irow * NCOL + icol] = pSource[icol * NROW + irow];
}
}
}
clock_t end = clock();
std::cout << "TargetRowSeqTranspose Time (ms) " << ((float)(end - begin))/(float)REPEAT << std::endl;
}
执行时间
TargetRowSeqTranspose Time (ms) 1.5498
写内存列连续的实现
void TargetColSeqTranspose(unsigned char* pSource, unsigned char* pTarget)
{
//Target 连续访问列
//TargetRowSeqTranspose Time (ms) 2.68945
clock_t begin = clock();
for (int i = 0; i < REPEAT; ++i)
{
for (int irow = 0; irow < NROW; ++irow)
{
for (int icol = 0; icol < NCOL; ++icol)
{
pTarget[icol * NROW + irow] = pSource[irow * NCOL + icol];
}
}
}
clock_t end = clock();
std::cout << "TargetRowSeqTranspose Time (ms) " << ((float)(end - begin)) / (float)REPEAT << std::endl;
}
执行时间
TargetRowSeqTranspose Time (ms) 2.68945
对比两种不同的访问模式,列连续方法的执行时间是行连续的1.7倍。
抓包分析
用VTune对两个方法抓包。结果如下
行连续方法
列连续方法
鉴于本人对缓存更新算法细节了解不够深入,因此无法解释缓存更新的执行过程与导致各级Cache Miss的具体原因。但是从抓包的结果看出行连续访问内存时,指令CPI更低为1.15,同时性能瓶颈在L3的Cache Miss。列连续访问内存时,指令执行平均时间更长,CPI升高到2.02,性能瓶颈点为L1 Cache Miss 与存储延迟。
原因解释
由于两种实现方式对内存的操作是对称的,即TargetRowSeqTranspose函数写内存连续读内存跳跃,TargetColSeqTranspose函数读内存连续写内存跳跃,参考《深入理解计算机系统》中缓存章节,缓存更新算法处理写内存操作比读内存更复杂。其中,读miss时,缓存会依次从低等级存储中寻找并加载包含当前数据的Cache line;在写miss时,首先会依次从低等级缓存中加载数据至当前缓存中,然后对当前缓存行更新数据,并依次向低等级缓存跟新Cache line。因此写内存不连续的情况会比读内存不连续时额外做了写回操作,造成性能下降。通过上面抓取的信息也可以验证这一点,两种实现方式在访问缓存和写内存延迟上存在非常大的差别。
借鉴意义
鉴于本人水平有限,无法从缓存一致性算法的实现细节分析出VTune各级缓存指标表示的性能状态的具体原因,但是分析本例仍然有实际的意义。在实现类似矩阵转置功能,即内存读写存方面存在映射关系且无法保证访问全部是连续操作,可以尝试使用优先保证写内存连续,读内存跳跃的策略,充分利用缓存替换策略,对比是否有性能上的提升。