通过重新排序优化分支

时间:2009-09-24 20:19:45

标签: c++ c optimization performance

我有这种C功能 - 被称为无数次:

void foo ()
{
    if (/*condition*/)
    {

    }
    else if(/*another_condition*/)
    {

    }
    else if (/*another_condition_2*/)
    {

    } 
          /*And so on, I have 4 of them, but we can generalize it*/
    else
    {

    }
 }

我有一个很好的测试用例来调用这个函数,导致某些if-branches被调用比其他的更多。

我的目标是找出安排if语句以最小化分支的最佳方法。

我能想到的唯一方法是为每个if条件分支写入文件,从而创建直方图。这似乎是一种乏味的方式。有更好的方法,更好的工具吗?

我使用gcc 3.4在AS3 Linux上构建它;使用oprofile(opcontrol)进行性能分析。

9 个答案:

答案 0 :(得分:14)

它不可移植,但许多版本的GCC支持一个名为__builtin_expect()的函数,可用于告诉编译器我们期望值是什么:

if(__builtin_expect(condition, 0)) {
  // We expect condition to be false (0), so we're less likely to get here
} else {
  // We expect to get here more often, so GCC produces better code
}

Linux内核将它们用作宏来使它们更直观,更清晰,更便携(即重新定义非GCC系统上的宏):

#ifdef __GNUC__
#  define likely(x)   __builtin_expect((x), 1)
#  define unlikely(x) __builtin_expect((x), 0)
#else
#  define likely(x)   (x)
#  define unlikely(x) (x)
#endif

有了这个,我们可以改写上面的内容:

if(unlikely(condition)) {
  // we're less likely to get here
} else {
  // we expect to get here more often
}

当然,这可能是不必要的,除非你的目标是原始速度和/或你已经分析并发现这是一个问题。

答案 1 :(得分:4)

试用一个探查器(gprof?) - 它会告诉你花了多少时间。我不记得gprof是否计算分支,但如果没有,只需在每个分支中调用一个单独的空方法。

答案 2 :(得分:3)

Callgrind下运行您的程序将为您提供分支信息。此外,我希望你描述并实际确定这段代码是有问题的,因为这似乎是微观优化充其量。编译器将从if / else if / else生成一个分支表,如果它能够不需要分支(这取决于条件是什么,显然)0,甚至失败了处理器上的分支预测器(假设这不适用于嵌入式工作,如果可以随意忽略我),则非常擅长确定分支的目标。

答案 3 :(得分:2)

你改变它们的顺序实际上并不重要,IMO。分支预测器将存储最常见的分支并自动接收它。

也就是说,您可以尝试一些事情......您可以维护一组作业队列,然后根据if语句将它们分配给正确的作业队列在最后一个接一个地执行它们之前。

这可以通过使用条件移动等进一步优化(这确实需要汇编程序,AFAIK)。这可以通过在条件a下有条件地将1移入寄存器(即初始化为0)来完成。将指针值放在队列的末尾,然后通过将条件1或0添加到计数器来决定是否增加队列计数器。

突然间你已经消除了所有分支,并且有多少分支误预测变得无关紧要。当然,与任何这些东西一样,你最好不要进行剖析,因为虽然它似乎会提供一个胜利......它可能不会。

答案 4 :(得分:2)

我们使用这样的机制:

// pseudocode
class ProfileNode
{
public:
   inline ProfileNode( const char * name ) : m_name(name)
   {  }
   inline ~ProfileNode()
   {
      s_ProfileDict.Find(name).Value() += 1; // as if Value returns a nonconst ref
   }

   static DictionaryOfNodesByName_t  s_ProfileDict;
   const char * m_name; 
}

然后在你的代码中

void foo ()
{
    if (/*condition*/)
    {
       ProfileNode("Condition A");
       // ...
    }
    else if(/*another_condition*/)
    {
       ProfileNode("Condition B");
       // ...
    } // etc..
    else
    {
       ProfileNode("Condition C");
       // ...
    }
 }

void dumpinfo()
{
  ProfileNode::s_ProfileDict.PrintEverything();
}

您可以看到如何在这些节点中放置秒表计时器并查看哪些分支消耗的时间最多。

答案 5 :(得分:1)

有些反击可能有所帮助。看到计数器后,存在很大差异,您可以按递减顺序对条件进行排序。

static int cond_1, cond_2, cond_3, ...

void foo (){
    if (condition){
      cond_1 ++;
      ...
    }
    else if(/*another_condition*/){
      cond_2 ++;
      ...
    }
    else if (/*another_condtion*/){
      cond_3 ++;
      ...
    } 
    else{
      cond_N ++;
      ...
    }
 }

编辑:“析构函数”可以在测试运行结束时打印计数器:

void cond_print(void) __attribute__((destructor));

void cond_print(void){
  printf( "cond_1: %6i\n", cond_1 );
  printf( "cond_2: %6i\n", cond_2 );
  printf( "cond_3: %6i\n", cond_3 );
  printf( "cond_4: %6i\n", cond_4 );
}

我认为只修改包含foo()函数的文件就足够了。

答案 6 :(得分:0)

将每个分支中的代码包装到一个函数中,并使用分析器查看每个函数的调用次数。

答案 7 :(得分:0)

逐行分析可以让您了解哪些分支被更频繁地调用。

使用LLVM之类的内容可以自动进行优化。

答案 8 :(得分:0)

作为一种分析技术,this是我所依赖的。

您想知道的是:在评估这些条件上花费的时间是否是执行时间的一小部分?

样本会告诉你,如果没有,那就无所谓了。

如果它确实很重要,例如,如果条件包括在很大一部分时间内在堆栈上的函数调用,那么你想要避免会花费很多时间进行假即可。你告诉它的方法是,如果你经常看到它从第一个或第二个if语句中调用一个比较函数,那么在这样的样本中捕获它并跳出它以查看它是返回false还是true。如果它通常返回false,它可能应该在列表中更远。