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

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

随着时间的推移,编译器逐渐成熟。他们变得非常聪明,因为他们的流分析使他们能够比您可能做的更好地决定在寄存器中保存什么值。Register 关键字变得不重要了。

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

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

  • 如能确定您所使用的平台和编译器,将不胜感激。
  • 为什么这个方法看起来有效?
  • 鼓励使用示例代码。

这是 相关问题

这个问题不是关于分析和优化的整个过程。假设程序编写正确,编译完成并进行了全面优化,测试并投入生产。代码中可能有一些结构阻止优化器尽其所能地完成最佳工作。您可以做些什么来重构以消除这些限制,并允许优化器生成更快的代码?

译自: http://www.strchr.com/(http://www.strchr.com/)

18918 次浏览

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

例如:

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

Here's a coding practice to help the compiler create fast code—any language, any platform, any compiler, any problem:

没有是否使用任何巧妙的技巧,强迫,甚至鼓励编译器在内存中布置变量(包括缓存和寄存器) ,因为你认为最好。首先编写一个正确和可维护的程序。

接下来,分析代码。

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

可能会感到失望,并且不得不非常努力地工作以获得小的性能改进。针对 Fortran 和 C 等成熟语言的现代编译器非常非常好。如果您阅读了一个“技巧”来提高代码的性能,请记住编译器编写者也阅读了有关它的内容,如果值得一试,可能会实现它。他们可能写了你最初读到的东西。

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

在这个文档中有很多其他的优化技巧: CPP 优化(虽然有点老旧)

重点:

  • 使用构造函数初始化列表
  • 使用前缀运算符
  • 使用显式构造函数
  • 内联函数
  • 避免临时物体
  • 意识到虚拟函数的成本
  • 通过引用参数返回对象
  • 考虑每个班级的分配
  • consider stl container allocators
  • ‘ em 空成员’优化
  • 等等

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

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

如果可能的话,避免使用变量的地址。请求指针并不是“自由”的,因为它意味着变量需要保存在内存中。如果您避免使用指针,即使是数组也可以保存在寄存器中ーー这对于向量化是必不可少的。

这就引出了下一点,阅读 ^ # $@ 手册!GCC 可以向量化纯 C 代码,如果你在这里撒一个 __restrict__,在那里撒一个 __attribute__( __aligned__ )。如果希望从优化器获得非常具体的内容,则可能必须具体。

对于性能,首先要关注编写可维护的组件化代码、松散耦合代码等,因此,当您必须隔离某个部件以重写、优化或简单配置文件时,可以不费吹灰之力地完成。

优化器将稍微帮助您的程序的性能。

让优化器完成它的工作。

说真的。不要试图智胜优化器。它是由聪明人设计的,他们的经验比你丰富得多。

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

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

Cache-miss: 不太确定如何帮助编译器,因为它主要是算法的,但是有一些内部特性可以预取内存。

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

The vast majority of code that people write will be I/O bound (I believe all the code I have written for money in the last 30 years has been so bound), so the activities of the optimiser for most folks will be academic.

However, I would remind people that for the code to be optimised you have to tell the compiler to to optimise it - lots of people (including me when I forget) post C++ benchmarks here that are meaningless without the optimiser being enabled.

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

如果你有一些小的函数需要重复调用,我在过去已经通过把它们放在“ static inline”的头部获得了很大的收益。Ix86上的函数调用开销惊人地高。

使用显式堆栈以非递归方式重新实现递归函数也可以获得很多好处,但是那样的话,您就真的处于开发时间与收益之间的领域了。

遍历内存的顺序可能会对性能产生深远的影响,编译器并不擅长解决这个问题。如果您关心性能,那么在编写代码时必须认真考虑缓存位置问题。例如,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];
}
}

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

彻底阅读编译器手册,了解它告诉你的是什么。最大限度地使用编译器。

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

是的,编译器作者不是来自编码巨人的种族,编译器包含错误,根据手册和编译器理论,什么应该使事情变得更快有时会使事情变得更慢。这就是为什么你必须一步一个脚印,并衡量之前和之后的调整表现。

And yes, ultimately, you might be faced with a combinatorial explosion of compiler flags so you need to have a script or two to run make with various compiler flags, queue the jobs on the large cluster and gather the run time statistics. If it's just you and Visual Studio on a PC you will run out of interest long before you have tried enough combinations of enough compiler flags.

问候

马克

当我第一次拿起一段代码的时候,我通常可以在一两天内通过修改编译器标志获得1.4——2.0倍的性能(即新版本的代码运行时间是旧版本的1/1.4或1/2)。诚然,这可能是科学家们对编译器缺乏了解的评论,他们创建了我所研究的大部分代码,而不是我的优秀的表现。将编译器标志设置为 max (很少只是 -O3)之后,可能需要几个月的艰苦工作才能得到另一个1.05或1.1的因子

  1. 对所有变量声明使用尽可能局部的作用域。

  2. 尽可能使用 const

  3. 不要使用 register,除非您计划同时使用和不使用 register 进行配置文件

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

Blindly using the register keyword is as likely to help as hurt your optimization, It's just too hard to know what will matter until you look at the assembly output or profile.

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

我一直怀疑,但从来没有证明,声明数组,使他们持有2的幂,作为元素的数量,使优化器做一个 强度降低通过替换乘以一个移位的数量位,当查找单独的元素。

Write to local variables and not output arguments! This can be a huge help for getting around aliasing slowdowns. For example, if your code looks like

void DoSomething(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut)
{
for (int i=0; i<numFoo, i++)
{
barOut.munge(foo1, foo2[i]);
}
}

编译器不知道那个傻瓜!= 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;
}

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

一般优化

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

将小函数声明为 inline或宏

Each call to a function (or method) incurs overhead, such as pushing variables onto the stack. Some functions may incur an overhead on return as well. An inefficient function or method has fewer statements in its content than the combined overhead. These are good candidates for inlining, whether it be as #define macros or inline functions. (Yes, I know inline is only a suggestion, but in this case I consider it as a 提醒 to the compiler.)

删除死代码和冗余代码

如果代码没有被使用或者对程序的结果没有贡献,那么删除它。

简化算法设计

我曾经从一个程序中删除了大量的汇编代码和执行时间,我写下了它正在计算的代数方程,然后简化了代数式。实施简化代数式所占用的空间和时间都比原来的功能少。

循环展开

每个循环都有递增和终止检查的开销。要获得性能因子的估计值,请计算开销中的指令数(最小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

Reduction of if statements

Processors hate branches, or jumps, since it forces the processor to reload its queue of instructions.

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

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

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

如果 statusfalse,则 逻辑与操作符(&&)的 短路阻止执行测试。

例如:

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个八位字节的效率。
Example:

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。这也允许将数据布局分解为“常量部分”和变量部分。想想 邮件合并

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

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

Lastly, Code like the compiler would

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

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

用于管理堆的算法在某些平台(例如 vxworks)上非常慢。更糟糕的是,从调用 malloc 返回所需的时间高度依赖于堆的当前状态。因此,任何调用 malloc 的函数都会受到不容易解释的性能影响。如果堆仍然是干净的,但是在设备运行一段时间之后,堆可能会变得支离破碎,那么性能损失可能是最小的。这些调用将花费更长的时间,而且您无法轻易计算出性能将如何随着时间的推移而下降。你不可能真的做出一个更糟糕的情况估计。在这种情况下,优化器也不能为您提供任何帮助。更糟糕的是,如果堆变得过于碎片化,那么调用将开始完全失败。解决方案是使用内存池(例如 油嘴滑舌的切片)而不是堆。如果处理得当,分配调用将会更快,并且具有确定性。

你在这里得到了很好的答案,但是他们假设你的程序从一开始就非常接近最优,然后你说

假设程序已经 编写正确,编译完整 优化,测试和投入 制作。

根据我的经验,一个程序可能编写正确,但这并不意味着它接近最优。要做到这一点需要付出额外的努力。

If I can give an example, 这个答案 shows how a perfectly reasonable-looking program was made over 40 times faster by 宏观优化. Big speedups can't be done in 每个 program as first written, but in many (except for very small programs), it can, in my experience.

在这之后,micro-optimization(热点)可以给你一个很好的回报。

i use intel compiler. on both Windows and Linux.

当我或多或少完成了我的代码配置文件。然后挂在热点和尝试改变代码,让编译器作出更好的工作。

如果一个代码是计算性的,并且包含大量的循环-英特尔编译器中的向量化报告是非常有用的-在帮助中寻找“ vec-report”。

所以主要的思想是——润色性能关键代码。至于其余的-优先权是正确的和可维护的-短的功能,明确的代码,可以理解1年后。

在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 头和 if 可能适合一个缓存块, 理论上可以导致更快的循环执行。

我猜想其他类似的东西可以在某种程度上被优化。

有意见吗? 我是在做梦吗?

One optimization i have used in C++ is creating a constructor that does nothing. One must manually call an init() in order to put the object into a working state.

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

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

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

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

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

通过编写作为唯一源代码的代码绕过链接器

虽然单独的编译对于编译时间来说是非常好的,但是当您谈到优化时,它是非常糟糕的。基本上编译器不能优化超出编译单元,即链接器保留域。

但是如果你设计好了你的程序,你也可以通过一个独特的通用源来编译它。也就是说,不再编译 unit1.c 和 unit2.c,然后链接两个对象,而是编译 all.c,其中只包含 unit1.c 和 unit2.c。因此,您将受益于所有的编译器优化。

It's very like writing headers only programs in C++ (and even easier to do in C).

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

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

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

在循环中分离原子任务

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

小心这一个(配置文件性能使用此技巧或没有) ,因为像使用寄存器,它可能会给较少的性能比改进的。

尝试尽可能多地使用静态单一赋值编程。SSA 与大多数函数式编程语言中的结果完全相同,而且大多数编译器都会将代码转换为 SSA 来进行优化,因为它更容易使用。通过这样做,编译器可能会混淆的地方被揭示出来。除了最差的寄存器分配器之外,它还可以让所有的寄存器分配器和最好的寄存器分配器一样好用,并且可以让你更容易地进行调试,因为你几乎不需要考虑变量的值是从哪里来的,因为它只被分配到了一个地方。
避免全局变量。

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

Make use of the almost free comparison against 0 that most processors give you when doing math or logic operations. You almost always get a flag for ==0 and <0, from which you can easily get 3 conditions:

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

当然,如果并且只有一个返回点,您可以逆转这个逻辑。

(技巧,我回忆后)

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

我所做的一件事情就是尝试将代价高昂的操作保持在用户可能希望程序稍微延迟的位置。总体性能与响应性有关,但并不完全相同,对于许多事情来说,响应性是性能中更重要的部分。

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

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

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

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 的情况下,调用的开销可能不会太大,但是它肯定是一个开销(通常,“检查对象是否已经创建,如果没有,创建它,然后返回它)。这条获取链变得越复杂,我们浪费的时间就越多。

有人提醒我曾经遇到过这样的情况,症状很简单,就是内存不足,但结果是性能大大提高(内存占用也大大减少)。

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

这个解决方案的一部分是将 Alexandrescu 风格的小对象池组合在一起,但是扩展了它,这样我就可以分配小对象数组和单个项目。这也极大地提高了性能,因为在任何时候缓存中都可以容纳更多的项。

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

一个愚蠢的小提示,但是可以为你节省一些微小的速度和代码。

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

如果有调用 f _ 2的 f _ 1(x,y,z) ,声明 f _ 2为 f _ 2(x,y,z)。不要声明为 f _ 2(x,z,y)。

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

While reading disassembled code I've seen some ridiculous register shuffling because people didn't follow this rule.

我从@MSalters 对 这个答案的评论中学到的一个简洁的技巧允许编译器在根据某些条件返回不同对象时进行复制省略:

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


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

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

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

  1. 使大多数函数是静态的。这允许过程间常量传播和别名分析来完成它的工作,否则编译器需要假定可以从转换单元外部调用函数,并且参数的值是完全未知的。如果你看一下众所周知的开源库,它们都标记了静态函数,除了那些真正需要外部的函数。

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

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

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

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

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

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

  8. 使用 开关比执行 if (a | | b | | ... | | z)这样的测试更快。首先检查你的编译器是否自动执行这个操作,有些编译器是这样做的,但是使用 如果更具可读性。