通过重新排序优化分支
Posted
技术标签:
【中文标题】通过重新排序优化分支【英文标题】:optimizing branching by re-ordering 【发布时间】:2009-09-24 20:19:44 【问题描述】:我有这种 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 分支比其他分支被调用得更多。
我的目标是找出安排 if 语句以最小化分支的最佳方式。
我能想到的唯一方法是为每个分支到的 if 条件写入文件,从而创建直方图。这似乎是一种乏味的方式。有没有更好的方法、更好的工具?
我正在使用 gcc 3.4 在 AS3 Linux 上构建它;使用 oprofile (opcontrol) 进行分析。
【问题讨论】:
这是一个令人毛骨悚然的用户名... 【参考方案1】:它不可移植,但许多版本的 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的很好解释。不过,我想回应您的“这可能是不必要的”。对于任何经常发生的分支(如果分支不经常发生,您可能不关心错误分支对性能的影响),处理器的分支预测器通常在没有这些提示的情况下已经做得很好了。 就其价值而言,在 Linux 内核之类的东西中,需要考虑分支对性能的影响。然而,对于大多数项目来说,你是对的:分支不会成为瓶颈。【参考方案2】:尝试使用分析器(gprof?) - 它会告诉您花费了多少时间。我不记得 gprof 是否计算分支,但如果没有,只需在每个分支中调用一个单独的空方法。
【讨论】:
每个分支中的一个单独的空方法。如果我正在构建优化的二进制文件,如何告诉编译器不要内联函数? 您要做的就是查看最常执行的分支。构建未优化并获取数据 @Andrei: 或者指定noinline函数属性。【参考方案3】:在Callgrind 下运行您的程序将为您提供分支信息。此外,我希望您分析并实际确定这段代码是有问题的,因为这似乎充其量只是一个微优化。编译器将从 if/else if/else 生成一个分支表,如果它能够生成一个分支表,则不需要分支(这显然取决于条件是什么)0,甚至在处理器上的分支预测器失败(假设这不是嵌入式工作,如果可以随意忽略我)非常擅长确定分支的目标。
【讨论】:
【参考方案4】:实际上,您将它们更改为什么顺序并不重要,IMO。分支预测器将存储最常见的分支并自动获取它。
也就是说,您可以尝试一些事情...您可以维护一组 job 队列,然后根据 if 语句将它们分配给正确的 job em> 在最后一个接一个地执行之前排队。
这可以通过使用条件移动等进一步优化(但这确实需要汇编程序,AFAIK)。这可以通过在条件 a 下有条件地将 1 移动到寄存器中来完成,该寄存器初始化为 0。将指针值放在队列的末尾,然后通过将条件 1 或 0 添加到计数器来决定是否增加队列计数器。
突然间,您已经消除了所有分支,并且有多少分支错误预测变得无关紧要。当然,与任何这些事情一样,您最好不要进行分析,因为尽管它看起来会带来胜利……但可能不会。
【讨论】:
+1 以获得深思熟虑的答案,但坦率地说,这不取决于在每个条件中花费的周期,以及在最终选择的分支中花费的周期吗?在分支预测产生显着差异之前,这些必须非常瘦。【参考方案5】:我们使用这样的机制:
// 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();
您还可以看到在这些节点中放置秒表计时器是多么容易,并查看哪些分支消耗的时间最多。
【讨论】:
当然会有一些开销,如果你调用你引入的函数一百万次你可以改变你看到的时间...... 是的,实际上我们使用常量整数符号作为节点标识符而不是字符串,以使字典查找实际上是一个 O(1) 数组索引。【参考方案6】:一些计数器可能会有所帮助。看到计数器后,发现有较大的差异,可以将条件按降序排列。
静态 int cond_1, cond_2, cond_3, ... 无效富() 如果(条件) 条件_1++; ... else if(/*another_condition*/) 条件_2++; ... else if (/*another_condtion*/) 条件_3 ++; ... 别的 条件_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() 函数的文件就足够了。
【讨论】:
按顺序对它们进行排序并不一定能保证编译器会按给定的顺序输出它们,尽管我认为它不会受到伤害。请参阅我的回答,了解可以说是更好的订购方式。【参考方案7】:将每个分支中的代码封装成一个函数,并使用分析器查看每个函数被调用了多少次。
【讨论】:
【参考方案8】:逐行分析让您了解哪些分支被更频繁地调用。
使用LLVM 之类的东西可以自动进行这种优化。
【讨论】:
【参考方案9】:作为一种分析技术,this 是我所依赖的。
您想知道的是:评估这些条件所花费的时间是否占执行时间的很大一部分?
样本会告诉你,如果没有,那也没关系。
如果它确实很重要,例如,如果条件包括大部分时间都在堆栈上的函数调用,那么您要避免花费大量时间进行假。你告诉这个的方式是,如果你经常看到它从第一个或第二个 if 语句中调用一个比较函数,那么在这样的示例中捕获它并退出它以查看它是否返回 false 或 true。如果它通常返回 false,那么它可能应该在列表中更靠后。
【讨论】:
以上是关于通过重新排序优化分支的主要内容,如果未能解决你的问题,请参考以下文章