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

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

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

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

What coding practices are available that may enable the compiler/optimizer to generate faster code?

  • 识别您使用的平台和编译器,将不胜感激 .

  • 为什么这项技术似乎有效?

  • 鼓励使用示例代码 .

这是related question

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

[Edit] Offset related link

回答(30)

2 years ago

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

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 years ago

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

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

接下来,分析您的代码 .

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

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

2 years ago

您遍历内存的顺序会对性能产生深远的影响,而编译器并不擅长确定并修复它 . 如果您关心性能,则在编写代码时必须充分考虑缓存局部性问题 . 例如,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];
  }
}

2 years ago

通用优化

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

将小函数声明为内联或宏

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

删除死机和冗余代码

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

简化算法设计

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

循环展开

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

Edit: 提供了一个循环展开示例之前:

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 快得多 . - TM值 .

减少if语句

处理器讨厌分支或跳转,因为它迫使处理器重新加载其指令队列 .

布尔算术(编辑:应用代码格式代码片段,添加示例)

if 语句转换为布尔赋值 . 某些处理器可以有条件地执行指令而无需分支:

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

如果 statusfalseLogical AND 运算符( && )的短路将阻止执行测试 .

例:

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.

以大块(块)读写数据 . 越大越好 . 例如,一次读取一个八位字节比一次读取读取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系列用于常量数据

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

格式化为内存,然后写入

使用多个 sprintf 格式化为 char 数组,然后使用 fwrite . 这也允许将数据布局分解为"constant sections"和可变部分 . 想想邮件合并 .

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

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

最后,代码就像编译器一样

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

2 years ago

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

也就是说,你不应该从另一个文件中的一个文件内部循环一个小函数,因为这样就不会内联它 .

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

这导致下一点,阅读^#$ @手册!如果你在这里撒一个 __restrict__ 和那里有一个 __attribute__( __aligned__ ) ,GCC可以矢量化普通的C代码 . 如果您想要优化器中非常具体的东西,您可能必须具体 .

2 years ago

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

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

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

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

2 years ago

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

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

2 years ago

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

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

强调:

  • 使用构造函数初始化列表

  • 使用前缀运算符

  • 使用显式构造函数

  • 内联函数

  • 避免临时对象

  • 了解虚拟功能的成本

  • 通过参考参数返回对象

  • 考虑每班分配

  • 考虑stl容器分配器

  • 'empty member'优化

2 years ago

尝试尽可能使用静态单一分配进行编程 . SSA与大多数函数式编程语言中的结果完全相同,并且's what most compilers convert your code to to do their optimizations because it'更易于使用 . 通过这样做,编译器可能会混淆的地方被揭露出来 . 它还使除了最差的寄存器分配器之外的所有寄存器分配器都能像最好的寄存器分配器一样工作,并且允许您更容易地进行调试,因为您几乎不必怀疑变量从何处获得它的值,因为它只分配了一个位置 .
避免全局变量 .

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

利用大多数处理器在进行数学运算或逻辑运算时给出的几乎免费的0比较 . 你几乎总是得到一个== 0和<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;
}

当然,如果且只有一个返回点,你可以反转逻辑 .

(后来我回忆起的技巧)

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

2 years ago

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

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

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

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

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

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

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

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

  • 使用开关比执行if(a || b || ... || z)等测试更快 . 首先检查你的编译器是否自动执行此操作,有些执行此操作,并且具有if但更具可读性 .

2 years ago

对于嵌入式系统和用C / C编写的代码,我尽量避免使用 dynamic memory allocation . 我这样做的主要原因不一定是性能,但这个经验法则确实有性能影响 .

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

2 years ago

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

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

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

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

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

2 years ago

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

Bypass linker by writing code as an unique source

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

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

它非常像只用C编写头文件(在C中更容易编写) .

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

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

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

Separate atomic tasks in loops

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

小心这个(使用这个技巧或没有使用这个技巧),就像使用寄存器一样,它可能会比改进的性能更低 .

2 years ago

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

2 years ago

大多数现代编译器都应该加快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);
}

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

Late Edit

虽然我对代码没有直接的了解;很明显,在SQL Server上使用CTE的要求是专门设计的,因此可以通过尾端递归进行优化 .

2 years ago

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

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

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的情况下,调用可能不会太昂贵,但它肯定是一个成本(通常,“检查对象是否已创建,如果没有,创建它,然后返回它) . 这个吸气剂链变得越来越复杂,我们浪费的时间就越多 .

2 years ago

  • 对所有变量声明使用尽可能最大的局部范围 .

  • 尽可能使用 const

  • 除非您计划使用和不使用寄存器,否则不要使用寄存器

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

盲目地使用register关键字可能有助于损害您的优化,直到知道重要的事情太难了您查看程序集输出或配置文件 .

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

2 years ago

我想起曾经遇到的一些问题,其症状仅仅是我们的内存耗尽,但结果却大大提高了性能(以及内存占用量的大幅减少) .

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

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

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

2 years ago

我从@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;

2 years ago

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

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

2 years ago

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

仔细阅读编译器手册并了解它告诉您的内容 . 最大限度地使用编译器 .

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

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

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

问候

标记

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

2 years ago

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

2 years ago

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

优化程序将有助于您的程序性能略有提升 .

2 years ago

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

假设程序编写正确,编译完全优化,测试并投入 生产环境 .

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

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

在此之后,微优化(热点)可以给你带来很好的回报 .

2 years ago

我使用intel编译器 . 在Windows和Linux上 .

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

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

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

2 years ago

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

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

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

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

2 years ago

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

上一次我真的不得不在整体性能方面做一些改进,我一直在关注次优算法,并寻找可能存在缓存问题的地方 . 我首先分析并测量了性能,并在每次更改后再次测量 . 然后公司崩溃了,但无论如何这都是有趣且有益的工作 .

2 years ago

我一直怀疑,但从来没有证明声明数组使得它们保持2的幂,因为元素的数量使得优化器能够通过将乘法乘以一个位来执行strength reduction,当查找个体时元素 .

2 years ago

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