在配置文件引导优化后嵌套 for 循环更快,但缓存未命中率更高

Posted

技术标签:

【中文标题】在配置文件引导优化后嵌套 for 循环更快,但缓存未命中率更高【英文标题】:nested for loop faster after profile guided optimization but with higher cache misses 【发布时间】:2014-04-09 20:44:14 【问题描述】:

我有一个程序,其核心是一个二维数组,格式为 a

std::vector<std::vector< int > > grid

还有一个简单的双 for 循环,有点像这样:

for(int i=1; i<N-1; ++i)
    for(int j=1; j<N-1; ++j)
        sum += grid[i][j-1] + grid[i][j+1] + grid[i-1][j] + grid[i+1][j] + grid[i][j]*some_float;

使用g++ -O3,它运行得非常快,但为了进一步优化,我使用 callgrind 进行了分析,发现 L1 缓存未命中率约为 37%,而 LL 未命中率为 33%,考虑到随机性质,这虽然很多但并不令人惊讶的计算。所以我做了一个配置文件引导优化

g++ -fprofile-generate -O3 ...
./program
g++ -fprofile-use -O3 ...

程序运行速度提高了大约 48%!但令人费解的是:缓存未命中甚至增加了! L1 数据缓存未命中率为 40%,LL 相同。

怎么可能?循环中没有可以优化预测的条件,并且缓存未命中率更高。但它更快。

编辑:好的,这里是 sscce:http://pastebin.com/fLgskdQG。在不同的运行时使用 N。编译通过

g++ -O3 -std=c++11 -sscce.cpp

在 linux 下的 gcc 4.8.1 上。

使用上述命令进行配置文件引导优化。 Callgrind 的东西是用 g++ -g 开关和valgrind --tool=callgrind --simulate-cache=yes ./sscce

【问题讨论】:

哇。我不知道你可以使用 gcc 进行配置文件引导优化。 更快的代码会给预取器带来困难。 PGO 可以提高分支预测,而不是 L1 数据缓存命中率,但是如何在简单循环上提高这一点很混乱。通过发布汇编代码鼓励回答。 请发帖SSCCE。 我在 2002 年左右在德国,当时 gcc 首次引入了配置文件引导优化。这并不是那么棒,但即使在那时,有些案例也有很大的改进。所以它并不完全是新的...... :) 你没有具体提到你一直在使用哪个版本的 gcc... 【参考方案1】:

我注意到使用或不使用 PGO 生成的汇编代码之间只有一个显着差异。如果没有 PGO,sum 变量会从寄存器溢出到内存,每次内部循环迭代一次。这种将变量写入内存并将其加载回理论上可能会大大减慢速度。幸运的是,现代处理器通过存储到加载转发对其进行了优化,因此减速并没有那么大。英特尔的优化手册仍然不建议将浮点变量溢出到内存中,尤其是当它们通过长延迟运算(如浮点乘法)计算时。

这里真正令人费解的是为什么 GCC 需要 PGO 来避免将寄存器溢出到内存中。未使用的浮点寄存器已经足够了,即使没有 PGO 编译器也可以从单个源文件中获取正确优化所需的所有信息...

这些不必要的加载/存储操作不仅解释了为什么 PGO 代码更快,还解释了为什么它会增加缓存未命中的百分比。如果没有 PGO,寄存器总是溢出到内存中的同一位置,因此这种额外的内存访问会增加内存访问次数和缓存命中次数,但不会改变缓存未命中次数。使用 PGO,我们的内存访问更少,但缓存未命中的数量相同,因此它们的百分比增加了。

【讨论】:

我又做了一些修修补补,发现这件事非常不稳定,操作系统和编译器版本之间的性能变化。但你的评论可能解释了这一点。谢谢! @Basti 您是否尝试过使用(row * columnCount) + column 的单个向量和地址单元格。这样,整个网格将存储在单个内存块中,而不是每行的多个碎片块中。

以上是关于在配置文件引导优化后嵌套 for 循环更快,但缓存未命中率更高的主要内容,如果未能解决你的问题,请参考以下文章

更快的理解js中循环嵌套

什么是更快的嵌套循环或多个循环? [关闭]

Python - 比 2 个嵌套 for 循环更快的东西

SQL,While循环,递归存储过程或游标中哪个更快?

在 Python 中优化 for 循环以更快地工作

有没有更快的方法来添加数组? [关闭]