7

<背景>

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

</背景>

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

但是,在将设计实现到库中之前,我需要对其进行测试。问题是关于测试此新功能效率的最佳方法。

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

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

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

那么如何对从库中提取的小特征进行正确的基准测试呢?相关问题:在一个小单元上进行这种体外测试是正确的方法还是我需要整个背景?

感谢您的建议!


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

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

4

11 回答 11

6

如果要强制任何编译器不丢弃结果,请让它将结果写入 volatile 对象。根据定义,该操作无法优化。

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

没有 iostream 开销,只是必须保留在生成的代码中的副本。现在,如果您要从大量操作中收集结果,最好不要将它们一一丢弃。这些副本仍然会增加一些开销。相反,以某种方式将所有结果收集到一个非易失性对象中(因此需要所有单独的结果),然后将该结果对象分配给易失性对象。例如,如果您的各个操作都产生字符串,您可以通过将所有 char 值加在一起以模 1<<32 来强制评估。这几乎不会增加任何开销;字符串可能会在缓存中。添加的结果随后将分配给 volatile,因此每个字符串中的每个字符实际上都必须进行计算,不允许使用快捷方式。

于 2009-01-13T15:09:17.333 回答
1

除非你有一个非常激进的编译器(可能发生),否则我建议计算一个校验和(只需将所有结果加在一起)并输出校验和。

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

于 2009-01-12T14:51:35.090 回答
1

编译器只允许消除不可能发生的代码分支。只要它不能排除一个分支应该被执行,它就不会消除它。只要某处存在一些数据依赖关系,代码就会存在并运行。编译器不太聪明地估计程序的哪些方面不会运行并且不要尝试,因为这是一个 NP 问题并且几乎不可计算。他们有一些简单的检查,例如 for if (0),但仅此而已。

我的拙见是,您之前可能遇到过其他问题,例如 C/C++ 评估布尔表达式的方式。

但无论如何,由于这是关于速度测试,您可以检查是否为自己调用 - 运行一次而不运行它,然后再次测试返回值。或者增加一个静态变量。在测试结束时,打印出生成的数字。结果将是平等的。

要回答您关于体外测试的问题:是的,请这样做。如果您的应用程序对时间如此紧迫,请执行此操作。另一方面,您的描述暗示了一个不同的问题:如果您的增量在 1e-3 秒的时间范围内,那么这听起来像是一个计算复杂性的问题,因为必须非常非常频繁地调用所讨论的方法(对于几次运行,1e-3 秒可以忽略不计)。

您正在建模的问题域听起来非常复杂,并且数据集可能很大。这样的事情总是一个有趣的努力。不过,请确保您首先绝对拥有正确的数据结构和算法,然后再对您想要的一切进行微优化。所以,我想说先看看整个上下文。;-)

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

于 2009-01-12T15:10:03.277 回答
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

一旦你找到了罪魁祸首优化,你就不需要 cout 了。

于 2009-01-12T15:10:07.177 回答
1

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

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

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

于 2009-01-12T15:11:01.900 回答
1

您可以在单独的 cpp 文件中创建一个不执行任何操作的虚拟函数,但将计算结果的类型作为参数。然后您可以使用计算结果调用该函数,强制 gcc 生成中间代码,唯一的惩罚是调用函数的成本(除非您经常调用它,否则它不应该扭曲您的结果)。

于 2009-01-12T15:35:55.580 回答
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;
}
于 2009-01-12T17:01:03.690 回答
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;

在这些情况下,有时对我有用的是将体外测试隐藏在函数中,并通过易失性指针传递基准数据集。这告诉编译器它不能折叠对这些指针的后续写入(因为它们可能是例如内存映射 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会阻止一些编译器优化(它禁止编译器将指针的内容保存在寄存器中并强制写入按程序顺序进行),因此只有在将它用于数据的最终写出时才值得信赖。

一般来说,这样的体外测试非常有用,只要您记住这不是全部。我通常像这样孤立地测试我的新数学例程,以便我可以快速迭代算法的缓存和管道特性,以获取一致的数据。

像这样的试管分析与在“现实世界”中运行它之间的区别意味着您将获得变化很大的输入数据集(有时是最好的情况,有时是最坏的情况,有时是病态的),缓存将在进入时处于某种未知状态该函数,并且您可能有其他线程在总线上碰撞;所以你应该在完成后在vivo中运行这个函数的一些基准测试。

于 2009-01-12T21:10:54.950 回答
0

我不知道 GCC 是否有类似的功能,但使用 VC++ 你可以使用:

#pragma optimize

选择性地打开/关闭优化。如果 GCC 具有类似的功能,您可以构建完全优化并在必要时将其关闭以确保您的代码被调用。

于 2009-01-12T15:16:32.827 回答
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% 的上下文中调用它十万次......

于 2009-01-12T16:18:35.180 回答
0

在启动时,从文件中读取。在你的代码中,说 if(input == "x") cout<< result_of_benchmark;

编译器将无法消除计算,如果您确保输入不是“x”,您将不会对 iostream 进行基准测试。

于 2009-01-12T19:21:16.997 回答