我正在制作蒙特卡罗模拟代码,每个类大约有一个cpp文件。
虽然为每对相邻粒子调用执行checkOverlap
函数,但这对执行时间非常重要。
我注意到,当我注释掉该函数时,与我立即返回时相比,我的执行时间差别很大。在进一步调查时我发现了这一点
类方法的评估速度取决于它的调用类。
目前我有一个类似于这个伪代码的类布局
class CollisionLogic{
public:
bool testCall(){
return false;
}
bool checkOverlap(void particle1, void particle2){
//do something...
return value;
}
};
class InformationContainer{
public:
bool testCall(){
return false;
}
CollisionLogic CL;
};
在主代码中执行以下操作
InformationContainer IC;
checkCollisionOn( particle P )
{
for( 'each neighbouring particle P_n' )
{
if( IC.CL.checkOverlap(P,P_n) )
return true;
/* Marker */
}
}
出于测试目的,checkOverlap
会将false
作为第一次调用返回。然后帧的评估需要953ms。如果我将标记注释替换为IC.CL.testCall()
,则时间会增加到1232毫秒,如果被IC.testCall()
替换,则会进一步增加到1305毫秒。
由于两个函数完全相同,我假设我可以排除cpu计算时间。所以我的问题是:导致这种情况发生的原因是什么?我需要做些什么来阻止它呢?
谢谢!
Q / A编译:
我将每个代码文件编译成一个带有标志'-O3 -std = gnu ++ 11'的目标文件,然后用相同的标志将它们链接在一起。
Ps:我发现了几个关于不同c ++速度问题的问题解释。 [1,2,3,4]但不是这样的人。
[1] Speed of C program execution
[2] Why are elementwise additions much faster in separate loops than in a combined loop?
[3] c++ speed comparison iterator vs index
[4] Why are elementwise additions much faster in separate loops than in a combined loop?
答案 0 :(得分:1)
这个答案假设您的程序包含的代码多于您发布的代码,并且处理框架执行的代码足以从处理器的至少最低级指令缓存中删除某些页面。
在这种情况下,我会假设执行时间的差异与指令缓存局部性有关。从第一级缓存中获取代码的速度非常快,较高级别的缓存逐渐变慢但速度更快,因此不太可能逐出缓存行。在现代桌面处理器上访问主内存需要数百个周期,因为内存延迟变得非常快,而内存延迟只会非常缓慢地减少。
在你打电话之前你叫CollisionLogic :: checkOverlap,它可能比SignalContainer :: testCall更接近CollisionLogic :: testCall。也许它甚至在同一个缓存行上,或者处理器在推测时获取的缓存行上(控制缓存的逻辑在高性能处理器上非常复杂)。因此,处理器可以快速获取InformationContainer :: testCall的指令。
您可以检查调试器中不同方法的指令地址,以检查此假设。
特别是当CollisionLogic和InformationContainer在不同的源文件中时,他们的方法最终可能会相互删除。
我不知道在二进制文件的代码部分中保持函数彼此接近的可移植和可维护方法。
<强>内联强>
我用于较小的,非常“热”的方法和函数的一个解决这个问题的方法是使它们在标题中使用定义的内联函数,并让它们在调用函数中内联。除非编译器决定不进行内联函数(它可能由于各种原因而执行,例如函数太大)。这不仅可以消除调用开销,还可以保证特定调用的缓存局部性。
但你必须衡量这是否会改善实际表现。太多内联会增加整体代码大小并导致相反的效果,因为这意味着经常一起访问的代码“工作集”可能不再适合特定的缓存级别并导致更多的chache未命中。
影响二进制文件中的代码位置
我还看到了一个旧的高性能项目,原作者确保常用代码位于相同的.cpp文件中。该程序运行得非常快。缺点是代码非常难以阅读和维护,并且只要程序的不相关部分发生变化,性能就会很容易退化。如果你必须以可维护性换取速度,我不推荐这样的方法。
如果您愿意了解链接器脚本,可以编写一个将特定的常用功能放在一起的脚本(您可能必须使用编译器特定的属性将它们分配给代码中的特定部分)。
无论您选择哪种方法来改进这一点,您都必须确定哪些函数实际上有助于执行时间(通过分析和基准测试实验性更改),查看调用图和调用计数,并执行大量操作进行基准测试以检查更改的实际性能影响。