<背景>

我现在确实需要优化 C++ 代码。我正在编写一个分子模拟库,我需要添加一个新功能。我过去已经尝试添加此功能,但随后我使用了在嵌套循环中调用的虚拟函数。我对此有不好的感觉,第一次实施证明这是一个坏主意。然而,这对于测试这个概念来说是可以的。

</背景>

现在我需要这个功能尽可能快(如果没有汇编代码或 GPU 计算,这仍然必须是 C++ 并且可读性更强)。现在我对模板和类策略有了更多的了解(来自 Alexandrescu 的优秀书籍),我认为编译时代码生成可能是解决方案。

不过,我需要在执行之前测试设计 巨大的 将其实施到图书馆的工作。问题是测试这一新功能效率的最佳方法。

显然我需要打开优化,因为如果没有这个 g++(可能还有其他编译器)会在目标代码中保留一些不必要的操作。我还需要在基准测试中大量使用新功能,因为 1e-3 秒的增量可以区分好设计和坏设计(该功能在实际程序中将被调用一百万次)。

问题是 g++ 在优化时有时“太聪明”,如果它认为从未使用过计算结果,则可以删除整个循环。我在查看输出汇编代码时已经看到过一次。

如果我向标准输出添加一些打印,编译器将被迫在循环中进行计算,但我可能主要对 iostream 实现进行基准测试。

那么我该怎么做 正确的 从库中提取的小特征的基准?相关问题:这样做是正确的方法吗 体外 在一个小单元上进行测试还是我需要整个上下文?

感谢您的建议!


似乎有几种策略,从允许微调的特定于编译器的选项到应该适用于每个编译器的更通用的解决方案,例如 volatile 或者 extern.

我想我会尝试所有这些。非常感谢您的所有回答!

有帮助吗?

解决方案

如果要强制的任何的编译器不放弃的结果,结果都写入到一个volatile对象。该操作不能被优化时,根据定义

template<typename T> void sink(T const& t) {
   volatile T sinkhole = t;
}

没有iostream的开销,只是具有留在所产生的代码的副本。 现在,如果你从很多操作收集结果,最好不要放弃他们一个接一个。这些副本还可以添加一些开销。取而代之的是,以某种方式收集在一个单一的非挥发性物(所以需要所有单独的结果)的所有结果,然后分配该结果对象到一个易失性。例如。如果您的个人行动都会产生字符串,您可以通过添加所有的char值一起模1 << 32强制评估。这几乎没有增加任何开销;琴弦可能会在缓存中。该加法的结果随后将被分配到的挥发性,以便在每个刺每个字符实际上必须进行计算,没有捷径允许的。

其他提示

除非你有一个的真正的积极的编译器(可能发生的),我建议计算校验和(只需添加多种组合的结果),并输出校验。

除此之外,你可能想看看生成的汇编代码运行的基准,以便您可以直观地验证任何循环实际上是在运行前。

编译器只允许消除代码分支,可以不会发生。只要不能排除一个分支应该被执行,也不会消除。只要有一些数据相关的某个地方,代码将在那里和将运行。编译器不是太聪明了哪些程序方面将不会运行而且不要试图估算,因为这是一个NP问题,几乎可计算的。他们有一些简单的检查,如if (0),但仅此而已。

我的愚见是,你可能通过一些其他的问题打早些时候,如C / C ++计算布尔表达式的方式。

但不管怎么说,因为这是关于速度的测试,你可以检查东西被调用自己 - 不带返回值的测试,一旦运行它,然后其他时间。或静态变量递增。在测试结束时,打印出生成的数。结果将是相等的。

要回答你的问题有关体外测试:是的,做到这一点。如果你的程序是这样的时间关键,做到这一点。在另一方面,你的描述暗示了一个不同的问题:如果你的增量是在1E-3秒的时间内,那么这听起来像的计算复杂性问题,因为问题的方法必须被称为非常非常经常(为几个运行,1E-3秒neglectible)。

在问题领域你是建模听起来很复杂,数据集可能是巨大的。这样的事情总是一个有趣的工作。请确保你绝对正确的数据结构和算法第一,虽然和微优化你之后想。 所以,我想说一下全范围内第一个; - )

出于好奇,什么是您所计算的问题?

您有很多的优化,为您编辑的控制。 -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的。

如果这对您来说是可能的,您可以尝试将代码拆分为:

  • 您要测试的库已在所有优化打开的情况下编译
  • 一个测试程序,动态链接库,优化关闭

否则,您可以为具有优化属性的测试函数指定不同的优化级别(看起来您正在使用 gcc...)(请参阅 http://gcc.gnu.org/onlinedocs/gcc/Function-Attributes.html#Function-Attributes).

您可以创建一个什么也不做单独的CPP文件的虚拟功能,但需要作为参数无论是你的计算结果的类型。然后,你可以调用该函数与计算的结果,迫使gcc生成的中间代码,唯一刑罚是调用函数(除非你把它叫做的很多不应该的结果扭曲的成本! )。

#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;
}

修改:你可以做的就是简单地使用数据以某种虚假的方式功能运行,您的基准测试外后最容易的事情。像,

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;

有时适用于我在这些情况下什么是隐藏的体外在函数内部测试和通过的易失性指针传递的基准数据集。这告诉它不能崩溃之后写入这些指针(因为他们可能的例如的内存映射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的工作,但它往往不 - 看汇编输出是肯定的。还记得挥发性来衬托一些编译器优化(它禁止编译器保持指针的内容寄存器和力量写入按照程序顺序),所以这一点,如果你使用它的只有值得信赖最后写入了数据。

在这样的一般体外试验是非常有用的,只要你记住,这是不是故事的全部。我通常测试我的新的数学程序,在这样的隔离,这样我可以快速迭代的只是我在一致的数据算法的高速缓存和流水线的特点。

试管分析这样在“现实世界”运行它之间的差异意味着你将得到广泛变化的输入数据集(有时是最好的情况下,有时最坏的情况下,有时病变),缓存会在一些未知状态在进入功能,你可能有其他线程敲打总线上;所以你应该在这个函数运行一些基准的在体内的还有,当你完成。

我不知道,如果GCC也有类似的功能,但与VC ++,你可以使用:

#pragma optimize

,以选择性地打开/关闭优化。如果GCC也有类似的功能,你可以建立与全面优化,只是关闭它在必要时,以确保您的代码被调用。

不期望的优化的只是一个小例子:

#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;
}

如果你从注释代码“双COORDS [500] [3]” for循环将产生完全相同的汇编代码(只是试图使用g ++ 4.3.2)的端部。我知道这个例子太简单了,我没能表现出一个简单的“坐标”结构的一个std ::向量这一行为。

不过,我觉得这个例子还表明,某些优化能够在基准引入错误,我想避免这样的一些惊喜库中引入新的代码时。这很容易想象,新的环境可能会阻止一些优化,并导致非常低效的库。

同样也要具有虚函数的应用(但我没有在这里证明这一点)。用于上下文,其中静态链接会做的工作,我非常有信心,体面的编译器应该消除额外的间接调用虚拟函数。我可以尝试在一个循环这一呼吁并得出结论:调用虚函数是没有这样一个大问题。 然后,我会打电话给它的一千次百强上下文当编译器无法猜测会是怎样的指针的确切类型和具有运行时间增加了20%...

在启动时,从文件中读取。在代码,说,如果(输入== “X”)的cout << result_of_benchmark;

,编译器将不能够消除计算,并且如果可以确保输入不是“X”,则不会基准了iostream。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top