我在循环中运行4个线程,在循环中我正在评估函数并逐步增加计数器。
while(1) {
int fitness = EnergyFunction::evaluate(sequence);
mutex.lock();
counter++;
mutex.unlock();
}
当我运行这个循环时,就像我在4个运行线程中所说的那样,我每秒得到~20 000 000个评估。
while(1) {
if (dist(mt) == 0) {
sequence[distDim(mt)] = -1;
} else {
sequence[distDim(mt)] = 1;
}
int fitness = EnergyFunction::evaluate(sequence);
mainMTX.lock();
overallGeneration++;
mainMTX.unlock();
}
如果我为序列添加一些随机变异,我每秒得到~13 000 000次评估。
while(1) {
if (dist(mt) == 0) {
sequence[distDim(mt)] = -1;
} else {
sequence[distDim(mt)] = 1;
}
int fitness = EnergyFunction::evaluate(sequence);
mainMTX.lock();
if(fitness < overallFitness)
overallFitness = fitness;
overallGeneration++;
mainMTX.unlock();
}
但是当我添加简单的if语句进行检查时,如果新的适应度小于旧的适应性,如果这是真的,那么用新的适应性替换旧的健身。
但是性能损失是巨大的!现在我每秒得到~20 000次评估。如果我删除随机变异部分,我每秒也会得到~20 000个评估。
变量overallFitness声明为
extern int overallFitness;
我遇到了麻烦,想知道如此大的性能损失是什么问题。比较两个int这样的时间采取操作?
此外,我不相信这与互斥锁定有关。
更新
此性能损失不是因为分支预测,而是编译器忽略了此调用int fitness = EnergyFunction::evaluate(sequence);
。
现在我添加了volatile
,编译器不再忽略该调用。
还要感谢你指出分支机构的错误预测和atomic<int>
,并不了解它们!
因为原子我也删除了互斥部分,所以最终代码如下所示:
while(1) {
sequence[distDim(mt)] = lookup_Table[dist(mt)];
fitness = EnergyFunction::evaluate(sequence);
if(fitness < overallFitness)
overallFitness = fitness;
++overallGeneration;
}
现在我每秒得到~25000次评估。
答案 0 :(得分:31)
您需要运行一个分析器才能到达底层。在Linux上,使用perf
。
我的猜测是EnergyFunction::evaluate()
正在完全优化,因为在第一个示例中,您不使用结果。所以编译器可以丢弃整个事情。您可以尝试将返回值写入volatile
变量,这会强制编译器或链接器不优化调用。加速1000倍绝对不是一个简单的比较。
答案 1 :(得分:11)
实际上有一个原子指令可以将int增加1.因此,智能编译器可能能够完全删除互斥锁,尽管如此,我会感到惊讶。您可以通过查看程序集,或删除互斥锁并将overallGeneration
的类型更改为atomic<int>
来检查它,以检查它仍然有多快。最后一个缓慢的例子不再可能进行这种优化。
此外,如果编译器可以看到evaluate
对全局状态不执行任何操作并且未使用结果,则它可以跳过对evaluate
的整个调用。您可以通过查看程序集或删除对EnergyFunction::evaluate(sequence)
的调用来查看是否是这种情况,并查看时间 - 如果它没有加速,则首先不调用该函数。最后一个慢速示例不再可能进行此优化。您应该能够通过在不同的目标文件(其他cpp或库)中定义函数并禁用链接时间优化来阻止编译器不执行EnergyFunction::evaluate(sequence)
。
这里还有其他效果也会产生性能差异,但是我看不到任何可以解释因子1000差异的其他效果。因子1000通常意味着编译器在之前的测试中被欺骗了,现在改变了它来自作弊。
答案 2 :(得分:7)
我不确定我的答案会给出如此戏剧性的表现下降的解释,但它肯定会对它产生影响。
在第一种情况下,您将分支添加到非关键区域:
if (dist(mt) == 0) {
sequence[distDim(mt)] = -1;
} else {
sequence[distDim(mt)] = 1;
}
在这种情况下,CPU(至少IA)将执行分支预测,并且在分支未命中预测的情况下存在性能损失 - 这是已知的事实。
现在关于第二次添加,您在 critical 区域添加了一个分支:
mainMTX.lock();
if(fitness < overallFitness)
overallFitness = fitness;
overallGeneration++;
mainMTX.unlock();
反过来,除了“未命中预测”惩罚之外,还增加了在该区域中执行的代码量,从而增加了其他线程必须等待mainMTX.unlock();
的概率。
注意强>
请确保将所有全局/共享资源定义为volatile
。否则编译器可能会优化它们(这可能在一开始就解释了如此多的评估)。
如果overallFitness
,它可能不会被优化,因为它被声明为extern
但overallGeneration
可能会被优化掉。如果是这种情况,则可以在 critical 区域中添加“真实”内存访问后解释此性能下降。
<强>注2 强>
我仍然不确定我提供的解释是否可以解释这种显着的性能下降。所以我相信代码中可能会有一些您没有发布的实现细节(例如volatile
)。
修改强>
正如彼得(@Peter) Mark Lakata (@MarkLakata)在单独的答案中所述,我倾向于同意他们,很可能是因为性能下降是在第一种情况下fitness
从未使用过,因此编译器只是将该变量与函数调用一起优化。在第二种情况下使用了fitness
,因此编译器没有优化它。彼得和马克很好!我错过了这一点。
答案 3 :(得分:2)
我意识到这不是问题的答案,而是问题的替代方案。
代码运行时是否使用overallGeneration
?也就是说,它可能用于确定何时停止计算?如果不是,则可以放弃同步全局计数器并在每个线程上有一个计数器,并在计算完成后,将所有每线程计数器总计为总计。类似地,对于overallFitness
,您可以跟踪每个线程的maxFitness
,并在计算结束后选择四个结果中的最大值。
根本没有线程同步会让你100%的CPU使用率。