函数调用和条件语句哪个更快?

11

在回答这个问题之前,请考虑分支预测。

我有一些场景,可以通过使用函数指针来替换条件语句并调用一个函数。可以将其类比于基于组件的编程代替继承的情况。

     class Shape
     {
        float Area()
        {
            if(type == SQUARE)
             {
                return length*length;
             }
            else if(type == RECTANGLE)
            {
             return length*breadth;
            }
        } 
     }

同一个类可以这样写。

       class Shape
     {
        void SetAreaFunction(void *funcptr)//this function is used to set the current AreaFunc
        {
             CurrentAreaFunc = funcptr ;//this holds the pointer to current area func
        }
        float SqauareArea();//this will return square area
        float RectangleArea();//this will return rectangle area 
        float Area()
        {
            currentAreaFunc();
        } 
     }
如果你考虑上述情况,两种方法都可以达到相同的结果。但是,我在思考性能开销。在第二种情况下,通过进行函数调用来避免分支预测问题。

现在,请告诉我哪种做法更好,并且在这种情况下进行“更好的优化代码”(顺便说一句,“过早优化是万恶之源”的说法我不喜欢,因为优化有其好处,所以我确实考虑优化我的代码!)

附言:如果有人想详细了解“分支预测有多糟糕”,即使在汇编代码中,我也不介意。

更新:在对类似于上述代码的情况进行性能测试后,如果条件成功,则此类情况将获得成功。有人可以给出原因吗?

功能调用代码可以预取,因为没有分支代码,对吧?但是在这里,情况正好相反。分支代码胜利了!:O 在Intel Mac Osx、GCC O3/Os优化中进行了测试。


11
尝试两种方法并进行测量。 - fredoverflow
1
@FredOverflow:那个例子只是一个场景。我想知道这方面是否有更好的实践方法。 - Ayyappa
@KerrekSB:在不同的平台和架构上,分支预测是不同的。我不能仅依赖于自己的测试用例。 - Ayyappa
9
@Ayyappa:你非常接近“微优化”领域。首先编写正确、易读和易于维护的代码。如果通过分析确定了特定函数是瓶颈,可以尝试不同的变体,并选择在该程序的该特定位置、该编译器和该CPU上表现最佳的那个版本。 - Mat
7
你可能不喜欢“过早优化是万恶之源”这个说法,但你需要了解其背后的原因。优化可以有好处,但在可读性方面它也可能带来重大弊端。很多时候,最高效的代码比足够高效的代码更加复杂。 - Jon Skeet
显示剩余5条评论
3个回答

12

你用间接引用替换了一个if语句。

无论是if语句还是间接引用都需要内存访问。

然而,if语句将导致短跳转-这可能不会使流水线失效,而间接引用可能会使流水线失效。

另一方面,间接引用是跳转,而if语句是条件跳转。分支预测器可能会失败。

很难说哪个更快,需要进行测试。我预测if语句会赢。

请分享您的结果!


短跳指令即使在分支失败时也不需要刷新当前预取流水线吗? - Ayyappa
正如你所说,如果条件跳转赢得了比赛。请问你能告诉我间接跳转和条件跳转之间的区别吗? - Ayyappa
2
无条件跳转总是可以“预测”,代码可以被预取。对于有条件的跳转,只能进行概率预测。新架构的概率会更好。最近变得非常复杂。例如,请参见http://www.bit-tech.net/hardware/cpus/2008/11/03/intel-core-i7-nehalem-architecture-dive/5。 - Lior Kogan

3
您需要对此类代码进行分析以便为特定的环境(编译器、编译器版本、操作系统、硬件)做出特定的说明,并且您需要在特定的应用程序中进行测量,以便了解这是否对该应用程序有影响。除非您正在编写库代码,否则不要费心,除非分析表明这是应用程序中的热点。
只需编写最易于维护的、最易于阅读的代码即可。优化干净、无错误且易于阅读的代码总是比修复经过优化的代码中的错误更容易。
话虽如此,我记得Lippman在他的《C++对象模型》一书中引用了研究,发现在实际应用中,虚函数(基本上是函数指针)至少与类型转换开销一样快。我不知道具体细节,但是在书中可以找到。

3
更可能的是,优化器可以在if语句上施展他的魔法,而不是在动态更改的函数指针上。只是一个猜测,理论上编译器可以做任何他能证明不改变语义的事情。
但是,在您仅实现分支(在您的情况下使用if)而不调用函数的情况下,CPU更有可能应用它的魔力,即重新排序指令,预取事物等等。如果中间有函数调用,大多数CPU很可能会“清空”其管道,无法进行CPU优化。
话虽如此,如果将所有内容都放在标题、调用者和被调用者中,编译器可能会消除函数调用,自行重新排序等等。
请尝试自行测量。调用1M次。使用C++11中的,monotonic_clock::now()。
更新: 我的经验是:不要过度优化代码--很可能会让情况变得更糟。让编译器做工作,并且为此使尽可能多的代码可见。如果这样做,您绝对需要一个分析器,尝试替代方案,使用一些提示。但不要忘记:这必须非常小心地进行微调。唯一的性能衡量标准是速度。还有可读性、可测试性、可重用性等等。引用唐纳德·克努斯(Donald Knuth)的话说:“过早优化是万恶之源”。

我听说,在分支失败的情况下,会导致预取管道中的函数刷新。这不是真的吗?为什么处理器不能预取没有任何分支预测开销的函数?请澄清一下,我可能在某些概念上有误。 - Ayyappa
@Ayyappa - 这是处理器特定的情况,可能会出现这种情况。一些大型计算机可以同时并行预取多个分支。但这需要匹配的内存带宽,而x86可能没有那么高的带宽。 - Bo Persson
是的,Bo是正确的。那是高度CPU特定的,但我相信现在的x86 可以。或者至少它会猜测和预测。有时这些是正确的,有时它们是错误的。因此,人们可以优化ifs,甚至(像其他人指出的那样)进行分析 - 前进并分析您的代码!真的!但几乎可以肯定的是:失败的(本地)分支不像远跳转(即函数调用)那样糟糕。失败的分支可能会使流水线无效,而远跳转几乎肯定 - towi
感谢您提供的信息towi...我对类似的代码进行了分析,并在问题中更新了结果... - Ayyappa
@Ayy:嗯,不行,我需要更多的数据。分析意味着逐行计时。我猜你做了一个总体计时,这是一个好的开始。但是,我不能立即告诉你。编译器?CPU?优化级别?逐行计时?一个可能会给你洞见的好的分析工具可能是英特尔VTune。我想它有一个试用期。 - towi
@towi:我在运行OSX的英特尔Mac上进行了分析(使用gcc编译器-优化-O3)。我有两种情况,一种是条件语句,一种是函数调用,每种情况都运行了10000000次。这给出了一个大致的估计哪个更快。 - Ayyappa

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接