< backgound>
我真的需要优化C ++代码。我正在为分子模拟编写一个库,我需要添加一个新功能。我过去曾尝试添加此功能,但之后我使用了嵌套循环中调用的虚函数。我对此感到不满,第一次实施证明这是一个坏主意。然而,这对于测试这个概念是可以的。
< /背景>
现在我需要这个功能尽可能快(没有汇编代码或GPU计算,这仍然必须是C ++,更可读而不是更少)。 现在我对模板和类策略有了更多了解(来自Alexandrescu的优秀书籍),我认为编译时代码生成可能是解决方案。
但是我需要先测试设计,然后再将巨大的工作实现到库中。问题是关于测试这一新功能效率的最佳方法。
显然我需要改进优化,因为如果没有这个g ++(也可能是其他编译器)会在目标代码中保留一些不必要的操作。我还需要大量使用基准测试中的新功能,因为1e-3秒的增量可以区分好的和坏的设计(在真实程序中这个功能将被称为百万倍)。 p>
问题是g ++在优化时有时“太聪明”,如果考虑到从未使用计算结果,可以删除整个循环。在查看输出汇编代码时,我已经看过了。
如果我向stdout添加一些打印,那么编译器将被强制在循环中进行计算,但我可能主要是对iostream实现进行基准测试。
那么如何对从库中提取的小特征进行正确基准测试呢? 相关问题:在一个小单元上进行这种体外测试是正确的方法还是我需要整个上下文?
感谢您的建议!
似乎有几种策略,从特定于编译器的选项,允许微调到更适用于volatile
或extern
等每个编译器的更通用的解决方案。
我想我会尝试所有这些。 非常感谢您的所有答案!
答案 0 :(得分:5)
如果要强制任何编译器不丢弃结果,请将结果写入易失性对象。根据定义,该操作无法优化。
template<typename T> void sink(T const& t) {
volatile T sinkhole = t;
}
没有iostream开销,只是必须保留在生成的代码中的副本。 现在,如果你从很多操作中收集结果,最好不要逐个丢弃它们。这些副本仍然可以增加一些开销。相反,以某种方式将所有结果收集在一个非易失性对象中(因此需要所有单独的结果),然后将该结果对象分配给volatile。例如。如果您的各个操作都生成字符串,则可以通过将所有字符值一起加到modulo 1&lt;&lt; 32来强制进行评估。这几乎不增加任何开销;字符串可能会在缓存中。添加的结果随后将被分配为易失性,因此每个sting中的每个char实际上必须被计算,不允许有快捷方式。
答案 1 :(得分:1)
除非您有真正积极的编译器(可能发生),否则我建议计算校验和(只需将所有结果一起添加)并输出校验和。
除此之外,您可能希望在运行任何基准测试之前查看生成的汇编代码,以便可以直观地验证是否实际运行了任何循环。
答案 2 :(得分:1)
只允许编译器消除不可能发生的代码分支。只要它不能排除应该执行分支,它就不会消除它。只要某处存在某些数据依赖关系,代码就会存在并将运行。编译器不太聪明地估计程序的哪些方面不会运行而且不会尝试,因为这是一个NP问题并且难以计算。他们有一些简单的检查,例如if (0)
,但就是这样。
我的拙见是你之前可能遇到过其他一些问题,例如C / C ++评估布尔表达式的方式。
但无论如何,因为这是对速度的测试,你可以检查事情是否为自己调用 - 运行一次没有,然后另一次测试返回值。或者增加静态变量。在测试结束时,打印出生成的数字。结果将是平等的。
回答有关体外测试的问题:是的,这样做。如果您的应用对时间要求很高,请执行此操作。另一方面,你的描述暗示了一个不同的问题:如果你的增量在1e-3秒的时间范围内,那么这听起来像计算复杂性的问题,因为所讨论的方法必须经常被调用(对于几次运行,1e-3秒是可以忽略不计的。)
您正在建模的问题域非常复杂,数据集可能很大。这些事情总是很有意思。尽管如此,请确保您绝对拥有正确的数据结构和算法,并在此之后进行微观优化。 所以,我首先要看整个背景。; - )
出于好奇,你在计算的问题是什么?
答案 3 :(得分:1)
您可以对编译的优化进行大量控制。 -O1,-O2等只是一堆交换机的别名。
从手册页
-O2 turns on all optimization flags specified by -O. It also turns
on the following optimization flags: -fthread-jumps -falign-func‐
tions -falign-jumps -falign-loops -falign-labels -fcaller-saves
-fcrossjumping -fcse-follow-jumps -fcse-skip-blocks
-fdelete-null-pointer-checks -fexpensive-optimizations -fgcse
-fgcse-lm -foptimize-sibling-calls -fpeephole2 -fregmove -fre‐
order-blocks -freorder-functions -frerun-cse-after-loop
-fsched-interblock -fsched-spec -fschedule-insns -fsched‐
ule-insns2 -fstrict-aliasing -fstrict-overflow -ftree-pre
-ftree-vrp
您可以调整并使用此命令来帮助您缩小要调查的选项范围。
...
Alternatively you can discover which binary optimizations are
enabled by -O3 by using:
gcc -c -Q -O3 --help=optimizers > /tmp/O3-opts
gcc -c -Q -O2 --help=optimizers > /tmp/O2-opts
diff /tmp/O2-opts /tmp/O3-opts Φ grep enabled
一旦找到了culpret优化,就不需要cout了。
答案 4 :(得分:1)
如果可以,您可以尝试将代码拆分为:
否则,您可以为具有optimize属性的测试函数指定不同的优化级别(看起来您正在使用gcc ...)(请参阅http://gcc.gnu.org/onlinedocs/gcc/Function-Attributes.html#Function-Attributes)。
答案 5 :(得分:1)
您可以在单独的cpp文件中创建一个虚拟函数,该文件不执行任何操作,但可以将计算结果的类型作为参数。然后你可以用你的计算结果调用那个函数,强制gcc生成中间代码,唯一的代价就是调用函数的代价(除非你称之为批次,否则不应该扭曲你的结果! )。
答案 6 :(得分:1)
#include <iostream>
// Mark coords as extern.
// Compiler is now NOT allowed to optimise away coords
// This it can not remove the loop where you initialise it.
// This is because the code could be used by another compilation unit
extern double coords[500][3];
double coords[500][3];
int main()
{
//perform a simple initialization of all coordinates:
for (int i=0; i<500; ++i)
{
coords[i][0] = 3.23;
coords[i][1] = 1.345;
coords[i][2] = 123.998;
}
std::cout << "hello world !"<< std::endl;
return 0;
}
答案 7 :(得分:1)
编辑:您可以做的最简单的事情就是在函数运行之后以及基准之外以虚假的方式使用数据。像,
StartBenchmarking(); // ie, read a performance counter
for (int i=0; i<500; ++i)
{
coords[i][0] = 3.23;
coords[i][1] = 1.345;
coords[i][2] = 123.998;
}
StopBenchmarking(); // what comes after this won't go into the timer
// this is just to force the compiler to use coords
double foo;
for (int j = 0 ; j < 500 ; ++j )
{
foo += coords[j][0] + coords[j][1] + coords[j][2];
}
cout << foo;
在这些情况下,有时对我有用的是隐藏函数内的体外测试,并通过 volatile 指针传递基准数据集。这告诉编译器它不能折叠对这些指针的后续写入(因为它们可能是例如内存映射的I / O)。所以,
void test1( volatile double *coords )
{
//perform a simple initialization of all coordinates:
for (int i=0; i<1500; i+=3)
{
coords[i+0] = 3.23;
coords[i+1] = 1.345;
coords[i+2] = 123.998;
}
}
出于某种原因,我还没想到它在MSVC中并不总是有效,但它经常会这样 - 看看装配输出是否确定。还记得 volatile 会阻止一些编译器优化(它禁止编译器将指针的内容保存在寄存器中并强制写入以程序顺序发生)所以这只是值得信赖的,如果你将它用于最终写出数据。
一般来说,这样的体外测试非常有用,只要你记住它不是整个故事。我通常会像这样单独测试我的新数学例程,以便我可以快速迭代我的算法在一致数据上的缓存和管道特征。
像这样的测试管分析和在“现实世界”中运行它的区别意味着你会得到各种各样的输入数据集(有时是最好的情况,有时是最坏的情况,有时是病态的),缓存将是一些未知的进入功能状态,你可能有其他线程在公共汽车上敲打;因此,当你完成时,你应该在这个函数 in vivo 上运行一些基准测试。
答案 8 :(得分:0)
我不知道GCC是否有类似功能,但使用VC ++可以使用:
#pragma optimize
选择性地打开/关闭优化。如果GCC具有类似的功能,您可以使用完全优化进行构建,并在必要时将其关闭,以确保您的代码被调用。
答案 9 :(得分:0)
只是一个不需要的优化的小例子:
#include <vector>
#include <iostream>
using namespace std;
int main()
{
double coords[500][3];
//perform a simple initialization of all coordinates:
for (int i=0; i<500; ++i)
{
coords[i][0] = 3.23;
coords[i][1] = 1.345;
coords[i][2] = 123.998;
}
cout << "hello world !"<< endl;
return 0;
}
如果您将代码从“double coords [500] [3]”注释到for循环的末尾,它将生成完全相同的汇编代码(只是尝试使用g ++ 4.3.2)。我知道这个例子太简单了,我无法用简单的“坐标”结构的std :: vector来显示这种行为。
但是我认为这个例子仍然表明一些优化可能会在基准测试中引入错误,我想在库中引入新代码时避免这种意外。很容易想象新的上下文可能会阻止一些优化并导致库非常低效。
同样适用于虚拟功能(但我不在此证明)。在静态链接可以完成工作的上下文中使用我非常有信心,正常的编译器应该消除虚函数的额外间接调用。我可以在一个循环中尝试这个调用,并得出结论,调用虚函数并不是一件大事。 然后我会在一个上下文中调用它数十万次,编译器无法猜出指针的确切类型,并且运行时间增加了20%......
答案 10 :(得分:0)
启动时,从文件中读取。在你的代码中,比如if(input ==“x”)cout&lt;&lt; result_of_benchmark;
编译器将无法消除计算,如果确保输入不是“x”,则不会对iostream进行基准测试。