你最喜欢的低级代码优化技巧是什么?

时间:2009-02-27 02:56:08

标签: optimization

我知道你应该只在必要时优化事物。但是,如果认为有必要,你最喜欢的低级别(与算法级别相比)优化技巧是什么。

例如:loop unrolling

24 个答案:

答案 0 :(得分:23)

gcc -O2

编译器可以做得更好。

答案 1 :(得分:16)

为过滤器,循环缓冲器等选择2的幂

非常,非常方便。

- 亚当

答案 2 :(得分:13)

为什么,bit twiddling hacks,当然!

答案 3 :(得分:12)

科学代码中最有用的一个方法是将pow(x,4)替换为x*x*x*x。 Pow几乎总是比乘法更昂贵。接下来是

  for(int i = 0; i < N; i++)
  {
    z += x/y;
  }

  double denom = 1/y;
  for(int i = 0; i < N; i++) 
  {
    z += x*denom;
  }

但我最喜欢的低级优化是要弄清楚哪些计算可以从循环中删除。它总是更快地进行一次计算而不是N次。根据您的编译器,其中一些可能会自动为您完成。

答案 4 :(得分:9)

检查编译器的输出,然后尝试强制它做更快的事情。

答案 5 :(得分:8)

我不一定称之为低级优化,但通过明智地应用缓存,我通过所有低级技巧的应用程序组合,已经节省了数量级更多的周期。其中许多方法都是特定于应用程序的。

  • 拥有数据库查询的LRU缓存(或任何其他基于IPC的请求)。
  • 记住上次失败的数据库查询,并在特定时间范围内重新请求时返回失败。
  • 记住您在大型数据结构中的位置,以确保如果下一个请求是针对同一节点,则搜索是免费的。
  • 缓存计算结果以防止重复工作。除了更复杂的方案之外,这通常可以在iffor语句中找到。

CPU和编译器在不断变化。无论什么样的低级代码技巧,3 CPU芯片以前使用不同的编译器实际上可能在当前架构上更慢,并且很可能这个技巧可能会混淆将来维护此代码的人。

答案 6 :(得分:7)

++i可能比i++更快,因为它可以避免创建临时。

现在的C / C ++ / Java / C#编译器是否仍然适用,我不知道。对于具有重载运算符的用户定义类型,它可能会有所不同,而在简单整数的情况下,它可能无关紧要。

但我喜欢语法......它读起来像“增量i”这是一个合理的顺序。

答案 7 :(得分:7)

使用模板元编程在编译时而不是在运行时计算事物。

答案 8 :(得分:5)

多年前,在一个不那么聪明的编译器中,我从函数内联,行走指针而不是索引数组获得了很大的里程,并且迭代到零而不是最多。

如果有疑问,对汇编的一点知识将让你看看编译器产生了什么并攻击低效的部分(使用源语言,使用对编译器更友好的结构。)

答案 9 :(得分:5)

不要进行循环展开。不要做Duff的设备。使你的循环尽可能小,其他任何东西都会抑制x86性能和gcc优化器性能。

除掉分支可能很有用 - 所以完全摆脱循环是好的,那些无分支的数学技巧确实有效。除此之外,尽量不要离开L2缓存 - 这意味着如果它浪费缓存空间,也应避免大量的预先计算/缓存。

并且,特别是对于x86,尝试在任何时候保持使用的变量数量。很难说这些编译器会用这种东西做什么,但通常用较少的循环迭代变量/数组索引最终会得到更好的asm输出。

当然,这适用于台式机CPU;具有快速内存访问速度的慢速CPU可以预先计算更多,但在这些日子里,这可能是一个总内存很少的嵌入式系统...

答案 10 :(得分:5)

预先计算值。

例如,如果你的应用不一定需要角度非常精确,而不是sin(a)或cos(a),也许你代表一个圆的1/256的角度,并创建浮动数组正弦[]和余弦[]预先计算这些角度的sin和cos。

并且,如果你经常需要一个给定长度的某个角度的矢量,你可能会预先计算所有已经乘以该长度的正弦和余弦。

或者,换句话说,为了速度而交易记忆。

或者,更一般地说,“所有编程都是缓存练习” - Terje Mathisen

有些事情不太明显。例如,遍历二维数组,您可能会执行类似

的操作
    for (x=0;x<maxx;x++)
       for (y=0;y<maxy;y++)
          do_something(a[x,y]);

如果你这样做,你可能会发现处理器缓存更喜欢它:

   for (y=0;y<maxy;y++)
       for (x=0;x<maxx;x++)
           do_something(a[x,y]);

反之亦然。

答案 11 :(得分:4)

优化缓存局部性 - 例如,当将两个不适合缓存的矩阵相乘时。

答案 12 :(得分:3)

我发现从指针转换为索引访问可能会有所不同;编译器有不同的指令形式和注册用法可供选择。反之亦然。但这是非常低级别和编译器相关的,并且只有在需要最后几个百分点时才有效。

E.g。

for (i = 0;  i < n;  ++i)
    *p++ = ...; // some complicated expression

VS

for (i = 0;  i < n;  ++i)
    p[i] = ...; // some complicated expression

答案 13 :(得分:3)

使用C ++的placement new在预先分配的缓冲区上分配new。

答案 14 :(得分:3)

倒计时循环。与0比N比较便宜:

for (i = N; --i >= 0; ) ...

以2的幂移位和屏蔽比除法和余数,和/和%

便宜
#define WORD_LOG 5
#define SIZE (1 << WORD_LOG)
#define MASK (SIZE - 1)

uint32_t bits[K]

void set_bit(unsigned i)
{
    bits[i >> WORD_LOG] |= (1 << (i & MASK))
}

修改

(i >> WORD_LOG) == (i / SIZE) and
(i & MASK) == (i % SIZE)

因为SIZE是32或2 ^ 5。

答案 15 :(得分:3)

Jon Bentley的Writing Efficient Programs是低级和高级技术的重要来源 - 如果你能找到副本。

答案 16 :(得分:3)

使用布尔数学消除分支(if / elses):

if(x == 0)
    x = 5;

// becomes:

x += (x == 0) * 5;
// if '5' was a base 2 number, let's say 4:
x += (x == 0) << 2;

// divide by 2 if flag is set
sum >>= (blendMode == BLEND);

这真的可以加快速度,特别是当这些ifs处于循环或某个被调用的地方时。

答案 17 :(得分:3)

汇编程序中的那个:

xor ax, ax

而不是:

mov ax, 0

节目大小和性能的经典优化。

答案 18 :(得分:2)

在SQL中,如果您只需要知道是否存在任何数据,请不要理会COUNT(*)

SELECT 1 FROM table WHERE some_primary_key = some_value

如果您的WHERE子句可能会返回多行,请同时添加LIMIT 1

(请记住,数据库无法查看代码对其结果的影响,因此他们无法自行优化这些内容!)

答案 19 :(得分:2)

  • 突然回收框架指针
  • Pascal calling-convention
  • 重写堆栈帧尾调用优化(虽然它有时与上面的混淆)
  • vfork()
  • 之前使用fork()代替exec()
  • 我还在寻找一个借口:运行时数据驱动的代码生成

答案 20 :(得分:1)

自由使用__restrict来消除负载命中存储停滞。

答案 21 :(得分:1)

滚动循环。

说真的,上次我需要做这样的事情的时候是一个占用80%运行时间的函数,所以如果我能得到明显的性能提升,那么值得尝试进行微优化。

我做的第一件事就是卷起循环。这给了我一个非常显着的速度提升。我相信这是缓存局部的问题。

我接下来要做的是添加一个间接层,并在循环中添加一些逻辑,这使我只能遍历我需要的东西。这并没有增加速度,但值得做。

如果您要进行微优化,您需要对两件事情有一个合理的认识:您实际使用的架构(这与我长大的系统有很大不同,至少对于微优化而言目的),以及编译器将为您做什么。

许多传统的微观优化会随着时间的推移进行交易。如今,使用更多空间会增加缓存未命中的可能性,并且会影响您的性能。而且,现在很多都是由现代编译器完成的,通常比你可能做得更好。

目前,您应该(a)剖析以查看是否需要微优化,然后(b)尝试交换空间计算,以期尽可能保持缓存。最后,进行一些测试,这样你就知道你是否改进了东西或搞砸了。现代编译器和芯片对于保持良好的心理模型来说太复杂了,而且你知道一些优化是否有效的唯一方法就是测试。

答案 22 :(得分:1)

除了约书亚关于代码生成(大赢)的评论以及其他好的建议,......

我不确定你是否会把它称为“低级”,但是(这是downvote-bait)1)远离使用任何更多级别的抽象而不是绝对必要,2)远离事件如果可能的话,驱动通知式编程。

  1. 如果执行程序的计算机就像运行竞赛的汽车那样,方法调用就像绕道而行。这并不一定是坏事,除非有强烈的诱惑来嵌套这些东西,因为一旦你写了一个方法调用,你往往会忘记那个调用会花费你的成本。

  2. 如果您依赖事件和通知,那是因为您有多个数据结构需要保持一致。这是昂贵的,只有在你无法避免时才应该这样做。

  3. 根据我的经验,最大的性能杀手是过多的数据结构和过多的抽象。

答案 23 :(得分:1)

我对通过替换在结构中添加数字的for循环获得的加速感到惊讶:

const unsigned long SIZE = 100000000;

typedef struct {
    int a;
    int b;
    int result;
} addition;

addition *sum;

void start() {
    unsigned int byte_count = SIZE * sizeof(addition);

    sum = malloc(byte_count);
    unsigned int i = 0;

    if (i < SIZE) {
        do {
            sum[i].a = i;
            sum[i].b = i;
            i++;
        } while (i < SIZE);
    }    
}

void test_func() {
    unsigned int i = 0;

    if (i < SIZE) { // this is about 30% faster than the more obvious for loop, even with O3
        do {
            addition *s1 = &sum[i];
            s1->result = s1->b + s1->a;
            i++;
        } while ( i<SIZE );
    }
}

void finish() {
    free(sum);
}

为什么gcc不优化for循环呢?还是我错过了什么?一些缓存效应?