关于c ++:编码实践,使编译器/优化器能够制作更快的程序

Coding Practices which enable the compiler/optimizer to make a faster program

许多年前,C编译器并不特别聪明。作为一个解决方法,K&R发明了register关键字,以提示编译器,最好将这个变量保存在内部寄存器中。他们还制作了第三个操作符来帮助生成更好的代码。

随着时间的推移,编译器逐渐成熟。它们变得非常聪明,因为它们的流分析允许它们对寄存器中要保存的值做出比您可能做的更好的决策。register关键字变得不重要。

由于别名问题,对于某些类型的操作,Fortran可能比C快。理论上,通过仔细编码,可以绕过这个限制,使优化器能够生成更快的代码。

哪些编码实践可以使编译器/优化器生成更快的代码?

  • 如果能识别出你使用的平台和编译器,我们将不胜感激。
  • 为什么这项技术看起来有效?
  • 鼓励使用示例代码。

这是一个相关的问题

[编辑]这个问题不是要分析和优化的整个过程。假设程序编写正确,经过充分优化编译,测试并投入生产。您的代码中可能有一些构造禁止优化器尽其所能做最好的工作。您可以做些什么来重构,以消除这些限制,并允许优化器生成更快的代码?

[编辑]偏移相关链接


以下是帮助编译器创建快速代码的编码实践:任何语言、任何平台、任何编译器、任何问题:

不要使用任何聪明的技巧来强制甚至鼓励编译器按照您认为最好的方式在内存中布局变量(包括缓存和寄存器)。首先编写一个正确且可维护的程序。

接下来,分析您的代码。

然后,并且只有在那时,您才可能想开始研究告诉编译器如何使用内存的效果。一次做一个改变,并测量它的影响。

期待失望,并必须非常努力地工作,为小的性能改进。对于诸如Fortran和C这样的成熟语言,现代编译器非常非常好。如果你读了一个关于"技巧"的描述,以便从代码中获得更好的性能,记住编译器编写者也读过它,如果值得的话,可能实现了它。他们可能首先写下了你读到的东西。


写入局部变量而不是输出参数!这对于消除混叠减速有很大帮助。例如,如果您的代码看起来像

1
2
3
4
5
6
7
void DoSomething(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut)
{
    for (int i=0; i<numFoo, i++)
    {
         barOut.munge(foo1, foo2[i]);
    }
}

编译器不知道foo1!=巴洛克,因此每次必须通过循环重新加载foo1。它也不能读取foo2[i]直到写入barout完成。你可以开始用限制性的指针来捣乱,但这样做同样有效(而且更清楚):

1
2
3
4
5
6
7
8
9
void DoSomethingFaster(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut)
{
    Foo barTemp = barOut;
    for (int i=0; i<numFoo, i++)
    {
         barTemp.munge(foo1, foo2[i]);
    }
    barOut = barTemp;
}

这听起来很愚蠢,但是编译器可以更聪明地处理局部变量,因为它不可能在内存中与任何参数重叠。这可以帮助您避免可怕的负载命中存储(由FrancisBoivin在本文中提到)。


遍历内存的顺序会对性能产生深远的影响,编译器并没有很好地解决这一问题。如果您关心性能,那么在编写代码时必须认真考虑缓存位置问题。例如,C中的二维数组是以行主格式分配的。以列主格式遍历数组往往会使您有更多的缓存未命中,并使您的程序比处理器绑定的内存绑定更多:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define N 1000000;
int matrix[N][N] = { ... };

//awesomely fast
long sum = 0;
for(int i = 0; i < N; i++){
  for(int j = 0; j < N; j++){
    sum += matrix[i][j];
  }
}

//painfully slow
long sum = 0;
for(int i = 0; i < N; i++){
  for(int j = 0; j < N; j++){
    sum += matrix[j][i];
  }
}


通用优化

这里是我最喜欢的一些优化。我实际上通过使用这些工具增加了执行时间并减少了程序大小。好的。将小函数声明为inline或宏

对函数(或方法)的每个调用都会产生开销,例如将变量推送到堆栈上。一些函数在返回时也可能产生开销。效率低下的函数或方法在其内容中的语句数比组合开销少。无论是作为#define宏还是inline函数,这些都是很好的内联候选函数。(是的,我知道inline只是一个建议,但在这种情况下,我认为它是对编译器的提醒。)好的。删除死代码和冗余代码

如果代码没有被使用或者对程序的结果没有贡献,那么就去掉它。好的。简化算法设计

我曾经写下一个程序正在计算的代数方程,然后简化代数表达式,从程序中删除了大量的汇编代码和执行时间。简化代数表达式的实现比原函数占用更少的空间和时间。好的。循环展开

每个循环都有一个递增和终止检查的开销。要获得性能因数的估计值,请计算开销中的指令数(最少3个:递增、检查、转到循环的开始),然后除以循环内的语句数。数字越低越好。好的。

编辑:提供循环展开的示例之前:好的。

1
2
3
4
5
unsigned int sum = 0;
for (size_t i; i < BYTES_TO_CHECKSUM; ++i)
{
    sum += *buffer++;
}

展开后:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
unsigned int sum = 0;
size_t i = 0;
**const size_t STATEMENTS_PER_LOOP = 8;**
for (i = 0; i < BYTES_TO_CHECKSUM; **i = i / STATEMENTS_PER_LOOP**)
{
    sum += *buffer++; // 1
    sum += *buffer++; // 2
    sum += *buffer++; // 3
    sum += *buffer++; // 4
    sum += *buffer++; // 5
    sum += *buffer++; // 6
    sum += *buffer++; // 7
    sum += *buffer++; // 8
}
// Handle the remainder:
for (; i < BYTES_TO_CHECKSUM; ++i)
{
    sum += *buffer++;
}

在这个优势中,第二个好处是:在处理器必须重新加载指令缓存之前,会执行更多的语句。好的。

当我展开一个循环到32个语句时,我得到了令人惊讶的结果。这是一个瓶颈,因为程序必须计算2GB文件上的校验和。这种优化与块读取相结合,将性能从1小时提高到5分钟。循环展开在汇编语言中也提供了出色的性能,我的memcpy比编译器的memcpy快得多。——T.M.好的。if报表的减少

处理器讨厌分支或跳转,因为它强制处理器重新加载其指令队列。好的。布尔运算(编辑:应用代码格式到代码片段,添加示例)

if语句转换为布尔赋值。一些处理器可以有条件地执行指令而不进行分支:好的。

1
2
3
bool status = true;
status = status && /* first test */;
status = status && /* second test */;

如果statusfalse的话,逻辑与运算符(&&的短路会阻止测试的执行。好的。

例子:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct Reader_Interface
{
  virtual bool  write(unsigned int value) = 0;
};

struct Rectangle
{
  unsigned int origin_x;
  unsigned int origin_y;
  unsigned int height;
  unsigned int width;

  bool  write(Reader_Interface * p_reader)
  {
    bool status = false;
    if (p_reader)
    {
       status = p_reader->write(origin_x);
       status = status && p_reader->write(origin_y);
       status = status && p_reader->write(height);
       status = status && p_reader->write(width);
    }
    return status;
};

循环外的因子变量分配

如果变量是在循环中动态创建的,请将创建/分配移动到循环之前。在大多数情况下,不需要在每次迭代期间分配变量。好的。循环外的因子常量表达式

如果计算值或变量值不依赖于循环索引,请将其移出循环(在循环之前)。好的。块中的输入输出

以大块(块)形式读取和写入数据。越大越好。例如,一次读取一个八位字节的效率低于一次读取1024个八位字节的效率。例子:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static const char  Menu_Text[] ="
"

   "1) Print
"

   "2) Insert new customer
"

   "3) Destroy
"

   "4) Launch Nasal Demons
"

   "Enter selection: ";
static const size_t Menu_Text_Length = sizeof(Menu_Text) - sizeof('\0');
//...
std::cout.write(Menu_Text, Menu_Text_Length);

这项技术的效率可以直观地显示出来。-)好的。不使用printf族作为常量数据

常量数据可以使用块写入输出。格式化写入将浪费时间扫描文本以格式化字符或处理格式化命令。请参见上面的代码示例。好的。格式化到内存,然后写入

使用多个sprintf格式化到char数组,然后使用fwrite。这还允许将数据布局分为"常量部分"和变量部分。想想邮件合并。好的。将常量文本(字符串文本)声明为static const

当变量在没有static的情况下声明时,一些编译器可以在堆栈上分配空间并从ROM复制数据。这是两个不必要的操作。这可以通过使用static前缀来修复。好的。最后,像编译器那样的代码

有时,编译器可以比一个复杂的版本更好地优化几个小语句。此外,编写代码来帮助编译器优化也有帮助。如果我希望编译器使用特殊的块传输指令,我将编写看起来应该使用特殊指令的代码。好的。好啊。


优化器并不能真正控制程序的性能。使用适当的算法和结构以及配置文件、配置文件、配置文件。

也就是说,您不应该从另一个文件中的一个文件对一个小函数进行内部循环,因为这样会阻止它被内联。

如果可能,避免使用变量的地址。请求指针不是"自由的",因为它意味着变量需要保存在内存中。如果避免指针,甚至数组也可以保存在寄存器中——这对于矢量化是必要的。

这就引出了下一点,请阅读^$@手册!如果在这里撒一个__restrict__,在那里撒一个__attribute__( __aligned__ ),gcc就可以向量化普通的C代码。如果您想从优化器中得到非常具体的东西,您可能必须是具体的。


在大多数现代处理器上,最大的瓶颈是内存。

别名:加载命中存储在一个紧密的循环中可能是毁灭性的。如果您正在读取一个内存位置并写入另一个内存位置,并且知道它们是不相交的,那么在函数参数上小心地放置一个alias关键字确实可以帮助编译器生成更快的代码。但是,如果内存区域确实重叠,并且您使用了"alias",那么您将进入一个具有未定义行为的良好调试会话!

缓存未命中:不确定如何帮助编译器,因为它主要是算法,但有预取内存的内部函数。

另外,不要试图将浮点值转换为in t,反之亦然,因为它们使用不同的寄存器,从一种类型转换为另一种类型意味着调用实际的转换指令,将值写入内存,并将其读取回正确的寄存器集。


在代码中尽可能多地使用常量正确性。它允许编译器更好地优化。

在本文档中,有很多其他优化提示:cpp优化(不过是一个有点老的文档)

亮点:

  • 使用构造函数初始化列表
  • 使用前缀运算符
  • 使用显式构造函数
  • 内联函数
  • 避免临时物品
  • 了解虚拟功能的成本
  • 通过引用参数返回对象
  • 考虑按类分配
  • 考虑stl容器分配器
  • "空成员"优化


人们编写的绝大多数代码都是I/O绑定的(我相信我在过去30年里为钱编写的所有代码都是如此绑定的),所以大多数人的乐观主义者的活动都是学术性的。

然而,我会提醒人们,为了优化代码,你必须告诉编译器优化它-很多人(包括我当我忘记)后,这里的C++基准是没有意义的,如果没有优化器启用。


尝试尽可能多地使用静态单次分配进行编程。SSA与大多数函数式编程语言的结果完全相同,这也是大多数编译器将代码转换为进行优化的原因,因为它更易于使用。通过这样做,编译器可能会感到困惑的地方就会暴露出来。它还使除最差的寄存器分配器外的所有寄存器分配器都能像最好的寄存器分配器一样工作,并且允许您更容易地进行调试,因为几乎不必考虑变量从何处获取值,因为它只分配了一个位置。避免使用全局变量。

当通过引用或指针处理数据时,将其拉入局部变量,完成您的工作,然后将其复制回去。(除非你有充分的理由不这么做)

在进行数学或逻辑运算时,使用大多数处理器提供给您的几乎免费的与0的比较。几乎总是会得到一个==0和<0的标志,从中可以很容易地得到3个条件:

1
2
3
4
5
6
7
8
x= f();
if(!x){
   a();
} else if (x<0){
   b();
} else {
   c();
}

几乎总是比测试其他常量便宜。

另一个技巧是使用减法来消除一个范围内的比较测试。

1
2
3
4
5
6
7
#define FOO_MIN 8
#define FOO_MAX 199
int good_foo(int foo) {
    unsigned int bar = foo-FOO_MIN;
    int rc = ((FOO_MAX-FOO_MIN) < bar) ? 1 : 0;
    return rc;
}

这通常可以避免对布尔表达式进行短路的语言中的跳转,并避免编译器必须尝试找出如何处理保留得出第一次比较的结果,同时进行第二次比较,然后将它们组合起来。这看起来可能会耗尽一个额外的寄存器,但它几乎从来没有这样做过。通常你不再需要foo了,如果你这样做,rc还没有使用,所以它可以去那里。

当使用C(strcpy,memcpy,…)中的字符串函数时,请记住它们返回的是什么——目的地!通过"忘记"指向目的地的指针副本并从这些函数的返回中获取它,通常可以得到更好的代码。

永远不要忽略返回与上次调用的函数完全相同的内容的机会。编译器不太擅长收集:

1
2
3
4
5
6
7
8
9
10
11
foo_t * make_foo(int a, int b, int c) {
        foo_t * x = malloc(sizeof(foo));
        if (!x) {
             // return NULL;
             return x; // x is NULL, already in the register used for returns, so duh
        }
        x->a= a;
        x->b = b;
        x->c = c;
        return x;
}

当然,如果只有一个返回点,就可以反转上面的逻辑。

(我后来回忆起的技巧)

最好尽可能将函数声明为静态函数。如果编译器能够证明自己已经考虑到特定函数的每个调用方,那么它就可以以优化的名义打破该函数的调用约定。编译器通常可以避免将参数移动到寄存器或堆栈位置,而这些寄存器或堆栈位置通常是被调用函数期望其参数所在的位置(必须同时偏离被调用函数和所有调用方的位置才能执行此操作)。编译器还可以经常利用知道被调用函数需要什么内存和寄存器的优势,避免生成代码来保存寄存器中的变量值或被调用函数不干扰的内存位置。当对函数的调用很少时,这种方法尤其有效。这在很大程度上得益于内联代码,但实际上没有内联。


我写了一个优化的C编译器,这里有一些非常有用的东西需要考虑:

  • 使大多数函数保持静态。这使得过程间的常量传播和别名分析可以完成它的工作,否则编译器需要假定函数可以从翻译单元外部调用,参数值完全未知。如果您查看著名的开放源码库,它们都将函数标记为静态的,但真正需要外部的函数除外。

  • 如果使用全局变量,则尽可能将其标记为静态变量和常量。如果初始化一次(只读),最好使用初始值设定项列表,如static const int val[]=1,2,3,4,否则编译器可能不会发现变量实际上是已初始化的常量,并且将无法用常量替换变量中的加载。

  • 不要在循环内部使用goto,大多数编译器将不再识别循环,并且不会应用任何最重要的优化。

  • 只有在必要时才使用指针参数,并在可能时将其标记为限制。这对别名分析有很大帮助,因为程序员保证没有别名(过程间别名分析通常非常原始)。非常小的结构对象应按值传递,而不是按引用传递。

  • 尽可能使用数组而不是指针,尤其是在循环(a[i])内。数组通常为别名分析提供更多的信息,经过一些优化之后,仍然会生成相同的代码(如果好奇,请搜索降低循环强度)。这也增加了应用循环不变代码运动的机会。

  • 尝试在循环调用外部提升到没有副作用的大型函数或外部函数(不依赖于当前循环迭代)。在许多情况下,小函数是内联的或转换为易于提升的内部函数,但对于编译器来说,大函数在实际情况下似乎有副作用。外部函数的副作用是完全未知的,除了标准库中的一些函数,这些函数有时由一些使循环不变的代码运动成为可能。

  • 在编写具有多个条件的测试时,最有可能的条件放在第一位。如果(a b c)应该是如果(b a c),如果b比其他的更可能是真的。编译器通常不知道条件的可能值,也不知道哪些分支被占用得更多(它们可以通过使用概要信息来知道,但很少有程序员使用它)。

  • 使用一个开关比做一个类似if(a_b_……γz)。首先检查您的编译器是否自动执行此操作,有些是自动执行的,如果使用if-though,则更易于阅读。


  • 在嵌入式系统和用C/C++编写的代码中,尽量避免动态内存分配。我这样做的主要原因不一定是性能,但这个经验法则确实具有性能影响。

    在某些平台(如VxWorks)中,用于管理堆的算法速度非常慢。更糟糕的是,从调用malloc返回所需的时间高度依赖于堆的当前状态。因此,任何调用malloc的函数都会受到性能影响,这不容易解释。如果堆仍然是干净的,那么对性能的影响可能很小,但是在该设备运行一段时间之后,堆可能会变得碎片化。调用将花费更长的时间,并且您无法轻松计算性能随时间的推移将如何降低。您不能真正生成更差的案例估计。在这种情况下,优化器也不能为您提供任何帮助。更糟糕的是,如果堆变得过于分散,调用将开始完全失败。解决方案是使用内存池(如glib片)而不是堆。如果您正确地进行分配调用,那么它将更快、更具确定性。


    一个愚蠢的小提示,但它可以为您节省一些微乎其微的速度和代码。

    总是以相同的顺序传递函数参数。

    如果有调用f_2的f_1(x,y,z),则将f_2声明为f_2(x,y,z)。不要将其声明为F_2(x,z,y)。

    其原因是C/C++平台ABI(AKA调用约定)承诺在特定寄存器和堆栈位置传递参数。当参数已经在正确的寄存器中时,就不必移动它们了。

    在阅读被反汇编的代码时,我看到一些可笑的寄存器出现了混乱,因为人们没有遵循这条规则。


    我在上面的列表中没有看到两种编码技术:

    通过将代码作为唯一源写入来绕过链接器

    虽然单独的编译对于编译时间来说真的很好,但是说到优化时却很糟糕。基本上编译器不能超越编译单元进行优化,即链接器保留域。

    但是,如果你设计好你的程序,你也可以通过一个独特的公共源代码来编译它。也就是说,不要编译Unit1.c和Unit2.c,而是链接这两个对象,编译只包含Unit1.c和Unit2.c的所有.c。这样,您将从所有编译器优化中受益。

    这很像写标题只在C++程序中(甚至更容易在C中完成)。

    如果您从一开始就编写程序来启用它,这项技术就足够简单了,但是您还必须知道它会改变C语义的一部分,并且您可能会遇到一些问题,如静态变量或宏冲突。对于大多数程序来说,很容易克服出现的小问题。还要注意,作为一个独特的源代码进行编译的速度要慢得多,可能会占用大量的内存(通常不是现代系统的问题)。

    使用这个简单的技术,我碰巧使一些程序的速度快了十倍!

    像register关键字一样,这个技巧也很快就会过时。编译器开始支持通过链接器进行优化gcc:link-time优化。

    在循环中分离原子任务

    这一个更棘手。它是关于算法设计和优化器管理缓存和寄存器分配的方式之间的交互。通常程序必须循环访问某些数据结构,并对每个项执行一些操作。通常,所执行的操作可以在两个逻辑上独立的任务之间进行拆分。如果是这种情况,您可以编写完全相同的程序,在同一边界上有两个循环,执行完全相同的任务。在某些情况下,以这种方式写入它可能比唯一循环更快(细节更复杂,但一种解释是,在简单的任务情况下,所有变量都可以保存在处理器寄存器中,而在更复杂的情况下,则不可能,并且某些寄存器必须写入内存并在以后读取,并且成本高于加法。NAL流量控制)。

    小心这一个(使用或不使用这个技巧的配置文件性能),就像使用寄存器一样,它可能会比改进的性能提供更少的性能。


    大多数现代编译器应该很好地加速尾部递归,因为函数调用可以优化。

    例子:

    1
    2
    3
    4
    5
    6
    7
    int fac2(int x, int cur) {
      if (x == 1) return cur;
      return fac2(x - 1, cur * x);
    }
    int fac(int x) {
      return fac2(x, 1);
    }

    当然,这个例子没有任何边界检查。

    后期编辑

    虽然我对代码没有直接的了解,但很明显,在SQL Server上使用CTE的要求是专门设计的,这样它就可以通过尾部递归进行优化。


    不要一遍又一遍地做同样的工作!

    我看到的一个常见的反模式是这样的:

    1
    2
    3
    4
    5
    6
    7
    8
    void Function()
    {
       MySingleton::GetInstance()->GetAggregatedObject()->DoSomething();
       MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingElse();
       MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingCool();
       MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingReallyNeat();
       MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingYetAgain();
    }

    编译器实际上必须一直调用所有这些函数。假设您,程序员,知道聚合对象在这些调用过程中不会发生变化,因为爱所有神圣的事物…

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    void Function()
    {
       MySingleton* s = MySingleton::GetInstance();
       AggregatedObject* ao = s->GetAggregatedObject();
       ao->DoSomething();
       ao->DoSomethingElse();
       ao->DoSomethingCool();
       ao->DoSomethingReallyNeat();
       ao->DoSomethingYetAgain();
    }

    在单例getter的情况下,调用可能不太昂贵,但它肯定是一个成本(通常,"检查对象是否已创建,如果没有,则创建它,然后返回它")。这个吸气剂链越复杂,我们就越浪费时间。


    实际上,我在sqlite中看到过这一点,他们声称这会导致性能提高约5%:将所有代码放在一个文件中,或者使用预处理器来实现这一点。这样优化器就可以访问整个程序,并可以进行更多的过程间优化。


    我从@msalters-comment中学到了一种简单的技巧,它允许编译器在根据某些条件返回不同的对象时执行复制省略:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // before
    BigObject a, b;
    if(condition)
      return a;
    else
      return b;

    // after
    BigObject a, b;
    if(condition)
      swap(a,b);
    return a;

  • 对所有变量声明使用尽可能多的本地范围。

  • 尽可能使用const

  • 不要使用注册,除非你计划同时使用和不使用注册。

  • 其中的前2个,尤其是1个帮助优化器分析代码。它将特别有助于它对寄存器中要保存的变量做出正确的选择。

    盲目地使用register关键字可能会有助于优化,但在查看程序集输出或配置文件之前,很难知道什么才是重要的。

    从代码中获得良好的性能还有其他重要的事情;例如,设计数据结构以最大限度地提高缓存一致性。但问题是关于优化器。


    我想起了我曾经遇到的一些事情,其中的症状只是我们的内存不足,但结果是性能大大提高(以及内存占用的大幅减少)。

    在这种情况下,问题是我们使用的软件进行了大量的小分配。比如,在这里分配四个字节,在那里分配六个字节等等,很多小对象也在8-12字节的范围内运行。问题不在于程序需要大量的小东西,而是它单独分配了大量的小东西,这使得每个分配都膨胀到32字节(在这个特定的平台上)。

    解决方案的一部分是将Alexandrescu样式的小对象池组合在一起,但对其进行扩展,以便能够分配小对象数组以及单个项。这对性能也有很大的帮助,因为在同一时间缓存中可以容纳更多的项目。

    解决方案的另一部分是用一个SSO(小字符串优化)字符串替换手工管理的char*成员的大量使用。最小分配为32字节,我构建了一个字符串类,在char*后面有一个嵌入的28个字符缓冲区,因此95%的字符串不需要进行额外的分配(然后我手动用这个新类替换了库中char*的几乎每一个外观,这很有趣。这也帮助了大量的内存碎片,从而增加了其他指向对象的引用位置,同样也提高了性能。


    将数据与本机/自然边界对齐。


    这是我的第二条优化建议。正如我的第一条建议一样,这是通用的,而不是特定于语言或处理器的。

    仔细阅读编译器手册并理解它告诉你的。尽量使用编译器。

    我同意其他一两个受访者的观点,他们认为选择正确的算法对于从程序中榨取性能至关重要。除此之外,在使用编译器时的回报率(在代码执行改进中衡量)远远高于调整代码时的回报率。

    是的,编译器编写者不是来自一个编码巨人的种族,编译器包含错误,根据手册和编译器理论,什么应该使事情更快,有时会使事情更慢。这就是为什么你必须一步一步来衡量前后的性能。

    是的,最终,您可能会面临编译器标志的组合爆炸,因此您需要有一个或两个脚本来使用各种编译器标志运行make,在大型集群上对作业进行排队并收集运行时统计信息。如果只是你和一台PC上的Visual Studio,那么在你尝试了足够多的编译器标志组合之前,你就会失去兴趣。

    当做

    作记号

    当我第一次拿起一段代码时,我通常可以在一两天内通过修改编译器标志获得1.4倍的性能(即新版本的代码在旧版本的1/1.4或1/2时间内运行)。当然,这可能是对我工作的大部分代码的科学家缺乏编译器的理解,而不是我优秀的表现的一种评论。如果将编译器标志设置为max(很少是-o3),则需要花费数月的努力才能获得另一个系数1.05或1.1。


    如果您有反复调用的小函数,那么我过去通过将它们作为"静态内联"放在头中获得了很大的收益。IX86上的函数调用非常昂贵。

    使用显式堆栈以非递归方式重新实现递归函数也可以获得很多好处,但是您实际上处于开发时间与收益的关系中。


    当dec推出它的alpha处理器时,有一个建议将一个函数的参数数保持在7以下,因为编译器总是试图在寄存器中自动放置6个参数。


    你在这里得到了很好的答案,但是他们认为你的程序在开始时非常接近最佳状态,你说

    Assume that the program has been
    written correctly, compiled with full
    optimization, tested and put into
    production.

    根据我的经验,一个程序可能是正确的,但这并不意味着它接近最佳状态。要达到这一点需要额外的工作。

    如果我能举个例子,这个答案显示了一个完美合理的程序是如何通过宏优化使其速度提高40倍以上的。大加速不能像最初写的那样在每一个程序中完成,但是在许多程序中(非常小的程序除外),根据我的经验,它可以。

    完成后,微观优化(热点)可以给你一个很好的回报。


    我使用英特尔编译器。在Windows和Linux上。

    当或多或少地完成后,我分析代码。然后抓住热点,尝试修改代码,让编译器做得更好。

    如果代码是计算代码,并且包含大量循环,那么在"英特尔编译器"中的矢量化报告非常有用,请在"帮助"中查找"矢量化报告"。

    所以,主要的想法是——润色性能关键的代码。至于其余的——优先是正确的和可维护的——简短的功能,一年后就可以理解清楚的代码。


    我在C++中使用的一个优化是创建一个什么都不做的构造函数。为了使对象处于工作状态,必须手动调用init()。

    在我需要这些类的大向量的情况下,这是有好处的。

    我调用reserve()来为向量分配空间,但构造函数实际上并没有触及对象所在的内存页面。所以我花了一些地址空间,但实际上并没有消耗大量的物理内存。我避免了与相关的建设成本相关联的页面错误。

    当我生成填充向量的对象时,我使用init()设置它们。这限制了我的总页面错误,并避免了在填充向量时需要调整其大小()。


    为了提高性能,首先要注意编写维护代码——组件化的、松散耦合的等,因此当您必须隔离一个部件来重写、优化或简单地进行概要分析时,您可以不费吹灰之力就完成它。

    优化器对程序的性能有一定的帮助。


    我所做的一件事是,尝试在用户可能希望程序延迟一点的地方保持昂贵的操作。总体性能与响应有关,但并不完全相同,而且在许多情况下,响应是性能中更重要的部分。

    上一次我真的需要改进整体性能时,我留意了次优算法,并寻找可能存在缓存问题的地方。我先对性能进行了分析和测量,然后在每次更改之后再次对性能进行了分析和测量。后来公司倒闭了,但不管怎样,这是一项有趣而有指导意义的工作。


    我在80年代从COBOL隐约记得的一件事是,有一些链接器选项允许您影响函数链接在一起的顺序。这允许您(可能)增加代码位置。

    也有同样的想法。如果想知道使用该模式是否可以实现可能的优化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    for (some silly loop)
    if (something)
        if (somthing else)
            if (somthing else)
                if (somthing else)
                    /* This is the normal expected case */
                else error 4
            else error 3
        else error 2
    else error 1

    for head和ifs可以放入缓存块中,这在理论上可以导致更快的循环执行。

    我想ELSE是相似的,可以在某种程度上优化。

    评论?我在做梦吗?


    将小的和/或经常调用的函数放在源文件的顶部。这使得编译器更容易找到内联的机会。


    我一直怀疑,但从来没有证明声明数组使其拥有2的幂(作为元素数),这样在查找单个元素时,优化器就可以通过将一个乘移位替换为若干位来降低强度。


    让优化器完成它的工作。

    说真的。不要试图胜过优化器。它是由才华横溢的人设计的,比你更有经验。