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

时间:2010-01-15 19:13:08

标签: c++ c performance optimization

许多年前,C编译器并不是特别聪明。作为一种解决方法,K& R发明了寄存器关键字,提示编译器,将这个变量保存在内部寄存器中可能是个好主意。他们还让第三级运营商帮助生成更好的代码。

随着时间的推移,编译器逐渐成熟。他们变得非常聪明,他们的流量分析使他们能够更好地决定寄存器中的值,而不是你可能做的。 register关键字变得不重要了。

由于alias问题,对于某些操作,FORTRAN可能比C更快。从理论上讲,仔细编码可以解决这个限制,使优化器能够生成更快的代码。

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

  • 识别您使用的平台和编译器,将不胜感激。
  • 为什么这项技术似乎有效?
  • 鼓励使用示例代码。

这是related question

[编辑] 此问题与分析和优化的整体流程无关。假设程序编写正确,编译完全优化,测试并投入生产。您的代码中可能有一些构造禁止优化器尽其所能地完成最佳工作。您可以做什么来重构将删除这些禁令,并允许优化器生成更快的代码?

[编辑] Offset related link

32 个答案:

答案 0 :(得分:71)

这是一个编码实践,帮助编译器创建快速代码 - 任何语言,任何平台,任何编译器,任何问题:

使用任何强制或甚至鼓励编译器将变量放在内存(包括缓存和寄存器)中的巧妙技巧。首先编写一个正确且可维护的程序。

接下来,对您的代码进行分析。

然后,只有这样,您可能想要开始研究告诉编译器如何使用内存的效果。一次进行1次更改并衡量其影响。

期望失望并且必须非常努力地进行小的性能改进。成熟语言(如Fortran和C)的现代编译器非常非常好。如果您阅读了一个“技巧”的帐户以获得更好的代码性能,请记住编译器编写者也已阅读过它,如果值得这样做,可能会实现它。他们可能首先写下了你所读到的内容。

答案 1 :(得分:53)

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

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!= barOut,因此每次都要通过循环重新加载foo1。在写入barOut完成之前,它也无法读取foo2 [i]。你可以开始搞乱限制指针,但这样做有效(并且更加清晰):

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

这听起来很愚蠢,但编译器可以更聪明地处理局部变量,因为它不可能在内存中与任何参数重叠。这可以帮助您避免可怕的load-hit-store(Francis Boivin在此主题中提到)。

答案 2 :(得分:47)

您遍历内存的顺序会对性能产生深远的影响,而编译器并不擅长确定并修复它。如果您关心性能,则在编写代码时必须充分考虑缓存局部性问题。例如,C中的二维数组以行主格式分配。以列主格式遍历数组将使您有更多的缓存未命中,并使您的程序比处理器绑定更多的内存限制:

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

答案 3 :(得分:36)

通用优化

这里有一些我最喜欢的优化。通过使用这些,我实际上增加了执行时间并减少了程序大小。

将小函数声明为inline或宏

对函数(或方法)的每次调用都会产生开销,例如将变量推送到堆栈上。某些功能也可能导致返回开销。低效的函数或方法在其内容中的语句少于组合的开销。这些是内联的良好候选者,无论是#define宏还是inline函数。 (是的,我知道inline只是一个建议,但在这种情况下,我认为它是编译器的提醒。)

删除死代码和冗余代码

如果代码未被使用或者对程序的结果没有贡献,请将其删除。

简化算法设计

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

循环展开

每个循环都有递增和终止检查的开销。要获得性能因子的估计,请计算开销中的指令数(最小3:增量,检查,循环开始)并除以循环内的语句数。数字越低越好。

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

unsigned int sum = 0;
for (size_t i; i < BYTES_TO_CHECKSUM; ++i)
{
    sum += *buffer++;
}

展开后:

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语句转换为布尔赋值。某些处理器可以在不分支的情况下有条件地执行指令:

bool status = true;
status = status && /* first test */;
status = status && /* second test */;

如果&&status逻辑AND 运算符(false)的短路会阻止执行测试

示例:

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

循环外的因子变量分配

如果在循环内动态创建变量,则将创建/分配移动到循环之前。在大多数情况下,不需要在每次迭代期间分配变量。

循环外的因子常量表达式

如果计算或变量值不依赖于循环索引,则将其移动到循环之外(之前)。

块中的

I / O

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

static const char  Menu_Text[] = "\n"
    "1) Print\n"
    "2) Insert new customer\n"
    "3) Destroy\n"
    "4) Launch Nasal Demons\n"
    "Enter selection:  ";
static const size_t Menu_Text_Length = sizeof(Menu_Text) - sizeof('\0');
//...
std::cout.write(Menu_Text, Menu_Text_Length);

可以直观地证明该技术的效率。 : - )

不要将printf 系列用于常量数据

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

格式化为内存,然后写入

使用多个char格式化为sprintf数组,然后使用fwrite。这也允许将数据布局分解为“常量部分”和可变部分。想想邮件合并

将常量文本(字符串文字)声明为static const

当声明变量而没有static时,一些编译器可能会在堆栈上分配空间并从ROM复制数据。这是两个不必要的操作。这可以通过使用static前缀来修复。

最后,像编译器这样的代码

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

答案 4 :(得分:26)

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

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

尽可能避免使用变量的地址。要求指针不是“自由”,因为它意味着变量需要保存在内存中。如果你避免使用指针,即使数组也可以保存在寄存器中 - 这对于矢量化很重要。

这导致下一点,阅读^#$ @ manual !如果你在这里撒__restrict__并在那里撒__attribute__( __aligned__ ),GCC可以对普通的C代码进行矢量化。如果你想要一些非常具体的优化器,你可能必须具体。

答案 5 :(得分:18)

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

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

Cache-miss:不确定如何帮助编译器,因为它主要是算法,但有预取内存的内在函数。

另外,不要尝试将浮点值转换为int,反之亦然,因为它们使用不同的寄存器并从一种类型转换为另一种类型意味着调用实际转换指令,将值写入内存并在内存中读取它适当的套准。

答案 6 :(得分:11)

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

在本文档中有大量其他优化提示:CPP optimizations(虽然有点旧文档)

亮点:

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

答案 7 :(得分:11)

人们编写的绝大多数代码都将受到I / O限制(我相信我在过去30年中为所有代码编写的代码都受到了限制),因此对于大多数人来说,优化器的活动将是学术的。

但是,我会提醒人们,为了优化代码,你必须告诉编译器优化它 - 很多人(包括我忘记了)在这里发布C ++基准测试,如果没有启用优化器就没有意义。

答案 8 :(得分:9)

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

  1. 让大部分功能都是静态的。这允许过程间常量传播和别名分析完成其工作,否则编译器需要假设可以从翻译单元外部调用该函数,其中参数的值完全未知。如果你看一下着名的开源库,他们都会将函数标记为静态,除了真正需要extern的函数。

  2. 如果使用全局变量,请尽可能将它们标记为静态和常量。如果它们被初始化一次(只读),最好使用初始化列表,如static const int VAL [] = {1,2,3,4},否则编译器可能不会发现变量实际上是初始化的常量和将无法用常量替换变量中的载荷。

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

  4. 仅在必要时使用指针参数,并在可能的情况下将其标记为限制。这有助于别名分析,因为程序员保证没有别名(过程间别名分析通常非常原始)。非常小的struct对象应该通过值传递,而不是通过引用传递。

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

  6. 尝试在循环调用之外提升大函数或没有副作用的外部函数(不依赖于当前循环迭代)。在很多情况下,小函数被内联或转换为易于提升的内在函数,但是大型函数可能看起来编译器在它们实际上没有副作用时会产生副作用。外部函数的副作用是完全未知的,除了标准库中的某些函数(有时由某些编译器建模),使得循环不变的代码运动成为可能。

  7. 在编写具有多个条件的测试时,最有可能将其放在首位。 if(a || b || c)应该是if(b || a || c),如果 b 比其他更可能是真的。编译器通常不了解条件的可能值以及更多的分支(通过使用配置文件信息可以知道,但很少有程序员使用它)。

  8. 使用开关比执行if(a || b || ... || z)等测试更快。首先检查你的编译器是否自动执行此操作,但是如果 if ,则更容易阅读。

答案 9 :(得分:9)

尝试尽可能使用静态单一分配进行编程。 SSA与大多数函数式编程语言中的结果完全相同,这就是大多数编译器将代码转换为优化的原因,因为它更易于使用。通过这样做,编译器可能会混淆的地方被揭露出来。它还使除了最差的寄存器分配器以外的所有寄存器分配器都能像最好的寄存器分配器一样工作,并且允许您更容易地进行调试,因为您几乎不必怀疑变量从哪里得到它的值,因为它只分配了一个位置。登记/> 避免全局变量。

当通过引用或指针处理数据到局部变量时,执行您的工作,然后将其复制回来。 (除非你有充分理由不这样做)

利用大多数处理器在进行数学运算或逻辑运算时给出的几乎免费的0比较。你几乎总是得到一个== 0和&lt; 0的标志,你可以从中轻松获得3个条件:

x= f();
if(!x){
   a();
} else if (x<0){
   b();
} else {
   c();
}

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

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

#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,...)记住它们返回的内容 - 目的地!您可以通过“忘记”指向目标的指针副本来获得更好的代码,并从这些函数的返回中获取它。

永远不要忽视机会返回与你调用的最后一个函数完全相同的东西。编制者并不那么认真:

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

当然,如果且只有一个返回点,你可以颠倒逻辑。

(我后来回忆起的技巧)

尽可能将函数声明为静态是一个好主意。如果编译器可以向自己证明它已经占用了特定函数的每个调用者,那么它可以在优化名称中破坏该函数的调用约定。编译器通常可以避免将参数移动到寄存器或堆栈位置,这些位置调用函数通常期望它们的参数存在(它必须在所调用的函数和所有调用者的位置都有偏差)。编译器还经常利用知道所调用的函数将需要什么内存和寄存器,并避免生成代码以保留寄存器或被调用函数不会干扰的存储器位置中的变量值。当对函数的调用很少时,这种方法特别有效。这得到了内联代码的大部分好处,但没有实际内联。

答案 10 :(得分:7)

一个愚蠢的小技巧,但会为你节省一些微观的速度和代码。

始终以相同的顺序传递函数参数。

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

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

在阅读反汇编代码的过程中,我看到一些荒谬的寄存器改组,因为人们没有遵循这条规则。

答案 11 :(得分:7)

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

在某些平台(例如vxworks)中,用于管理堆的算法非常慢。更糟糕的是,从调用返回到malloc所需的时间在很大程度上取决于堆的当前状态。因此,任何调用malloc的函数都会受到无法轻易解释的性能影响。如果堆仍然是干净的,那么性能损失可能是最小的,但是在该设备运行一段时间后,堆可能变得碎片化。这些电话需要更长的时间,你无法轻易计算出性能会随着时间的推移而降低的程度。你无法真正产生更糟糕的案例估计。在这种情况下,优化器也无法为您提供任何帮助。更糟糕的是,如果堆碎片过于分散,则调用将完全失败。解决方案是使用内存池(例如,glib slices)而不是堆。如果你做对了,分配调用会更加快速和确定。

答案 12 :(得分:5)

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

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

虽然单独的编译非常适合编译时间,但是当你谈到优化时,这是非常糟糕的。基本上编译器无法优化编译单元,即链接器保留域。

但如果你设计好你的程序,你也可以通过一个独特的公共源程序来编译它。那就是编译unit1.c和unit2.c然后链接两个对象,编译all.c只是#include unit1.c和unit2.c。因此,您将受益于所有编译器优化。

这非常像只用C ++编写标题程序(在C语言中更容易编写)。

如果您编写程序从头开始启用它,这种技术很容易,但您还必须意识到它会改变C语义的一部分,您可以遇到一些问题,如静态变量或宏冲突。对于大多数程序来说,很容易克服发生的小问题。还要注意,编译作为一个独特的源代码会慢一些,并且可能需要大量内存(通常不会对现代系统造成问题)。

使用这种简单的技术,我碰巧制作了一些我写得快了十倍的程序!

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

循环中单独的原子任务

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

小心这个(使用这个技巧或不使用这个技巧的表现)就像使用注册它可能会提供比改进的性能更低的性能。

答案 13 :(得分:4)

大多数现代编译器都应该加快tail recursion的速度,因为可以优化函数调用。

示例:

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的要求是专门设计的,因此可以通过尾端递归进行优化。

答案 14 :(得分:4)

我实际上已经在SQLite中看到了这一点,他们声称它会带来性能提升~5%:将所有代码放在一个文件中或使用预处理器来完成相同的操作。这样,优化器就可以访问整个程序,并可以进行更多的过程间优化。

答案 15 :(得分:4)

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

我看到的一个常见反模式如下:

void Function()
{
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomething();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingElse();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingCool();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingReallyNeat();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingYetAgain();
}

编译器实际上必须始终调用所有这些函数。假设你,程序员,知道聚合的对象在这些调用的过程中没有改变,因为对所有神圣的爱的爱...

void Function()
{
   MySingleton* s = MySingleton::GetInstance();
   AggregatedObject* ao = s->GetAggregatedObject();
   ao->DoSomething();
   ao->DoSomethingElse();
   ao->DoSomethingCool();
   ao->DoSomethingReallyNeat();
   ao->DoSomethingYetAgain();
}

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

答案 16 :(得分:3)

  1. 对所有变量声明使用尽可能最本地的范围。

  2. 尽可能使用const

  3. 不要使用注册,除非您计划使用和不使用寄存器

  4. 前两个,尤其是#1,帮助优化器分析代码。它将特别有助于它在寄存器中保留哪些变量。

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

    从代码中获得良好性能还有其他重要事项;设计数据结构以最大化缓存一致性。但问题是关于优化器。

答案 17 :(得分:3)

我从@MSalters评论this answer上学到的一种巧妙的技巧允许编译器在根据某些条件返回不同的对象时进行复制省略:

// before
BigObject a, b;
if(condition)
  return a;
else
  return b;

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

答案 18 :(得分:3)

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

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

解决方案的一部分是将Alexandrescu风格的小对象池放在一起,但扩展它以便我可以分配小对象的数组以及单个项目。这对性能也有很大帮助,因为任何时候都有更多项目适合缓存。

解决方案的另一部分是用SSO(小字符串优化)字符串替换手动管理的char *成员的猖獗使用。最小分配是32个字节,我构建了一个字符串类,在char *后面有一个嵌入的28个字符的缓冲区,所以95%的字符串不需要做额外的分配(然后我手动替换了几乎所有的外观这个新类中的char *与这个新类有关,这很有趣。这也有助于实现内存碎片化,从而增加了其他指向对象的引用局部性,同样也有性能提升。

答案 19 :(得分:3)

答案 20 :(得分:2)

当DEC出现其alpha处理器时,建议将函数的参数数量保持在7以下,因为编译器总是会尝试在寄存器中自动添加6个参数。

答案 21 :(得分:2)

如果你有重复调用的小函数,我过去通过将它们作为“静态内联”放在标题中获得了很大的收益。 ix86上的函数调用非常昂贵。

使用显式堆栈以非递归方式重新实现递归函数也可以获得很多,但是你真的处于开发时间与增益之间。

答案 22 :(得分:2)

这是我的第二条优化建议。与我的第一条建议一样,这是一般目的,而不是语言或处理器特定的。

仔细阅读编译器手册并了解它告诉您的内容。尽量使用编译器。

我同意其中一两个已经确定选择正确算法对于从程序中挤出性能至关重要的其他受访者。除此之外,您投资使用编译器的时间的回报率(在代码执行改进中衡量)远远高于调整代码的回报率。

是的,编译器编写者不是来自编码巨头和编译器的竞争包含错误,根据手册和编译器理论,应该使事情变得更快有时会使事情变慢。这就是为什么你必须一步一步地测量前后调整性能的原因。

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

此致

标记

当我第一次拿起一段代码时,我通常可以获得1.4到2.0倍的性能(即新版本的代码运行在旧版本的1 / 1.4或1/2的时间内)在一两天之内摆弄编译器标志。当然,这可能是对我所研究的大部分代码的科学家缺乏编译器悟性的评论,而不是我卓越的症状。将编译器标志设置为max(并且它很少只是-O3)可能需要数月的努力才能获得另一个因子1.05或1.1

答案 23 :(得分:1)

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

这对我需要这些类的大型向量的情况有好处。

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

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

答案 24 :(得分:1)

为了提高性能,首先要关注编写可维护代码 - 组件化,松散耦合等,所以当你必须将一个部分隔离以重写,优化或简单地分析时,你可以毫不费力地完成它。

优化工具可以帮助您轻松实现计划的效果。

答案 25 :(得分:1)

我做过的一件事是尝试将昂贵的操作保存到用户可能期望程序延迟一点的位置。整体性能与响应性有关,但并不完全相同,对于许多事情而言,响应性是性能中更重要的部分。

上一次我真的需要对整体性能进行改进时,我一直关注次优算法,并寻找可能存在缓存问题的地方。我首先分析并测量了性能,并在每次更改后再次测量。然后公司崩溃了,但无论如何这都是有趣且富有启发性的工作。

答案 26 :(得分:1)

我使用的是intel编译器。在Windows和Linux上。

当或多或少完成时我会分析代码。然后挂在热点上并尝试更改代码以允许编译器做得更好。

如果代码是计算代码并包含大量循环 - 英特尔编译器中的矢量化报告非常有用 - 请在帮助中查找“vec-report”。

所以主要的想法 - 抛光性能关键代码。至于其余部分 - 优先考虑是正确和可维护的 - 短期功能,清晰的代码,可以在1年后理解。

答案 27 :(得分:1)

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

  

假设程序已经存在   写得正确,编译完整   优化,测试和投入   生产

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

如果我可以举一个例子,this answer显示了宏优化如何使一个看起来非常合理的程序的速度提高了40倍。在第一次编写的每个程序中都无法实现大的加速,但在许多(除了非常小的程序)之外,根据我的经验,它可以。

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

答案 28 :(得分:0)

我在80年代从cobol中隐约记得的一件事是,有一些链接器选项可以让你实现函数链接在一起的顺序。这允许您(可能)增加代码局部性。

沿着同样的想法。如果想知道是否可以通过使用模式

来实现可能的优化
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可能适合缓存块, 这在理论上可以导致更快的循环执行。

我猜可以在某种程度上优化相似的角色。

评论? 我在做梦吗?

答案 29 :(得分:0)

我一直怀疑,但从未证明声明数组使得它们保持2的幂,因为元素的数量使得优化器能够通过将乘法乘以数字来进行strength reduction在查找单个元素时的位数。

答案 30 :(得分:0)

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

答案 31 :(得分:-4)

让优化器完成它的工作。

严重。不要试图超越优化器。它是由聪明人设计的,比你更有经验。