【问题标题】:Is "IF" expensive?“如果”贵吗?
【发布时间】:2010-09-23 20:07:48
【问题描述】:

我这辈子都不记得我们老师那天到底说了什么,我希望你可能知道。

模块是“数据结构和算法”,他告诉我们一些类似的东西:

if 语句是最昂贵的 [某物]。 [某事]注册 [某事]。

是的,我的记忆力确实很糟糕,我真的很抱歉,但我已经搜索了几个小时,但没有任何结果。有什么想法吗?

【问题讨论】:

  • 问你的老师有选择吗?
  • 你为什么不给你的老师发邮件? SO 上的任何人都不太可能知道您的老师说了什么,除非他们当时在场(或者您的老师本人也读过 SO)。
  • 当然还有一个链接到必填railroad answer
  • If 语句,尤其是受 C 语言影响的花括号语言中的“? :”表达式,可以通过 x86 和 arm 处理器等特殊条件执行指令来实现。这些是基于先前测试执行或不执行某些操作的指令。使用这些出色的指令完全避免了条件跳转/分支/“转到”指令的需要。在某些情况下,通过使程序流程完全可预测,从而在某些情况下实现巨大的性能提升,因为它只是直接向前推进,没有(可能不可预测)跳转到代码中的不同点。
  • 一个好的编译器有时可能需要在正确的方向上进行一些推动,以便它使用条件指令而不是愚蠢和使用条件跳转,方法是重新组织代码并可能在表达式中使用聪明的算术还是一个? : 表达。除非你真的了解你的 asm 并且阅读过 Agner Fog 的优化指南,否则不要玩这个。编译器有时会正确处理 if 语句或 ? : 使用表达式。

标签: if-statement language-agnostic branch-prediction


【解决方案1】:

在最低级别(在硬件中),是的,如果很昂贵。要了解原因,您必须了解pipelines 的工作原理。

要执行的当前指令存储在通常称为指令指针(IP)或程序计数器(PC)的东西中;这些术语是同义词,但不同的术语用于不同的架构。对于大多数指令,下一条指令的 PC 只是当前 PC 加上当前指令的长度。对于大多数 RISC 架构,指令的长度都是恒定的,因此 PC 可以以恒定的量递增。对于 x86 等 CISC 架构,指令可以是可变长度的,因此对指令进行解码的逻辑必须计算出当前指令需要多长时间才能找到下一条指令的位置。

对于 branch 指令,但是,下一条要执行的指令不是当前指令之后的下一个位置。分支是 goto - 它们告诉处理器下一条指令在哪里。分支可以是有条件的也可以是无条件的,目标位置可以是固定的也可以是计算的。

有条件与无条件很容易理解 - 仅当某个条件成立时才采用条件分支(例如一个数字是否等于另一个数字);如果不采取分支,则控制正常进行分支之后的下一条指令。对于无条件分支,总是采用分支。条件分支出现在if 语句和forwhile 循环的控制测试中。无条件分支出现在无限循环、函数调用、函数返回、breakcontinue 语句、臭名昭著的goto 语句等等(这些列表远非详尽)。

分支目标是另一个重要问题。大多数分支都有一个固定的分支目标——它们转到代码中的特定位置,该位置在编译时是固定的。这包括if 语句、各种循环、常规函数调用等等。 Computed 分支在运行时计算分支的目标。这包括switch 语句(有时)、从函数返回、虚函数调用和函数指针调用。

那么这一切对性能意味着什么?当处理器看到一条分支指令出现在它的流水线中时,它需要弄清楚如何继续填满它的流水线。为了弄清楚程序流中分支之后的指令是什么,它需要知道两件事:(1)是否会采用分支和(2)分支的目标。解决这个问题称为branch prediction,这是一个具有挑战性的问题。如果处理器猜对了,程序就会全速运行。如果相反,处理器猜测不正确,它只是花了一些时间计算错误的东西。它现在必须刷新其管道并使用来自正确执行路径的指令重新加载它。底线:性能大受打击。

因此,if 语句昂贵的原因是 分支错误预测。这只是最低级别的。如果您正在编写高级代码,则根本不需要担心这些细节。只有在使用 C 或汇编语言编写对性能至关重要的代码时,才应该关心这一点。如果是这样的话,即使需要更多的指令,编写无分支代码通常也优于有分支的代码。您可以使用一些很酷的小技巧来计算 abs()min()max() 等不带分支的东西。

【讨论】:

  • 这不是 只是 分支错误预测。分支还在编译器级别以及在某种程度上在 CPU 级别(当然对于无序 CPU)也禁止指令重新排序。不错的详细答案。
  • 如果高级语言最终被翻译成低级语言,并且您正在编写非常以性能为中心的代码,那么编写避免 if 语句的代码是否仍然一无所获?这个概念是不是不能进入高级语言?
  • 您根本不会用高级语言编写以性能为中心的代码,以至于 if 语句很重要。高级语言中的性能关键代码并没有做太愚蠢的事情。
【解决方案2】:

“昂贵”是一个非常相对的术语,尤其是与“if”语句的关系,因为您还必须考虑条件的成本。范围可以从几条简短的 cpu 指令到测试调用远程数据库的函数的结果。

我不会担心的。除非您在进行嵌入式编程,否则您可能根本不应该担心“if”的成本。对于大多数程序员来说,它只是永远不会成为应用性能的驱动因素。

【讨论】:

  • 绝对相对... cmp/cond jmp 在许多处理器上仍然比 mul 快。
  • 是的,我同意我不应该担心它。我不想在这里优化任何东西。我只是想了解和学习。 ;)
【解决方案3】:

分支,尤其是在 RISC 架构微处理器上,是一些最昂贵的指令。这是因为在许多架构上,编译器会预测最有可能采用哪条执行路径并将这些指令放在可执行文件中,因此当分支发生时它们已经在 CPU 缓存中。如果分支走另一条路,它必须返回主内存并获取新指令——这是相当昂贵的。在许多 RISC 架构上,所有指令都是一个周期,除了分支(通常是 2 个周期)。我们在这里谈论的不是主要成本,所以不用担心。此外,编译器在 99% 的时间里都会比你优化得更好 :) 关于 EPIC 架构(安腾就是一个例子)真正令人敬畏的事情之一是它缓存(并开始处理)来自分支两侧的指令,然后在知道分支的结果后丢弃它不需要的集合。如果典型架构沿着不可预测的路径分支,这可以节省额外的内存访问。

【讨论】:

    【解决方案4】:

    查看有关电池性能的文章Better Performance Through Branch Elimination。另一个有趣的是实时碰撞检测博客上的this post about branchless selections

    除了已经针对此问题发布的出色答案之外,我想提醒一下,尽管“if”语句被认为是昂贵的低级操作,但尝试在更高级别的环境,例如脚本语言或业务逻辑层(无论语言如何),可能非常不合适。

    在绝大多数情况下,程序的编写首先应该清晰明了,其次才是性能优化。有许多问题领域的性能是最重要的,但一个简单的事实是,大多数开发人员并没有编写用于渲染引擎核心或连续运行数周的高性能流体动力学模拟的模块。当您的解决方案“正常工作”是当务之急时,您最不想考虑的应该是是否可以节省代码中条件语句的开销。

    【讨论】:

    • 确实!还可以补充一点,当使用鼓励调用的语言(基本上,除了汇编程序或没有 stdlib 的 C 之外的任何语言)进行编码时,来自正常编程技术的管道干扰将压倒任何有关条件分支的问题。
    【解决方案5】:

    if 本身很慢。缓慢总是相对的,我敢打赌,你从来没有感觉到 if 语句的“开销”。如果您要编写高性能代码,则可能无论如何都希望避免分支。使if 变慢的原因是处理器基于一些启发式方法从if 之后预加载代码。它还将停止流水线在机器代码中的if 分支指令之后直接执行代码,因为处理器还不知道将采用什么路径(在流水线处理器中,多条指令被交错并执行)。执行的代码可能必须反向执行(如果采用另一个分支。它称为branch misprediction),或者在这些地方填充noop,这样就不会发生这种情况。

    如果if 是邪恶的,那么switch 也是邪恶的,&&|| 也是如此。不用担心。

    【讨论】:

      【解决方案6】:

      在可能的最低级别上,if 包括(在计算特定if 的所有特定于应用程序的先决条件之后):

      • 一些测试说明
      • 如果测试成功则跳转到代码中的某个位置,否则继续前进。

      与此相关的费用:

      • 低级比较 -- 通常是 1 个 cpu 操作,超级便宜
      • 潜在的跳跃 -- 这可能很昂贵

      解释为什么跳跃很昂贵:

      • 如果发现它没有被 cpu 缓存,你可以跳转到内存中任意位置的任意代码 -- 我们有问题,因为我们需要访问主内存,这比较慢
      • 现代 CPU 执行分支预测。他们试图猜测是否会成功并在管道中提前执行代码,因此加快速度。如果预测失败,则管道提前完成的所有计算都必须无效。这也是一项昂贵的操作

      总结一下:

      • 如果可能会很昂贵,如果您真的非常关心性能。
      • 当且仅当您正在编写实时光线追踪器或生物模拟或类似的东西时,您应该关心它。在大多数现实世界中,没有理由关心它。

      【讨论】:

      • 把它带到下一个层次:嵌套和/或复合 if 语句呢?如果有人写了很多这样的 if 语句,费用很快就会变得非常明显。并且由于对于大多数开发人员来说 if 语句似乎是一种基本操作,因此避免复杂的条件分支通常归结为文体问题。风格方面的问题仍然很重要,但往往在当下最火热的时候,它们可能是第一个被忽略的问题。
      【解决方案7】:

      现代处理器的执行管道很长,这意味着几条指令同时在不同的阶段执行。当下一条指令开始运行时,他们可能并不总是知道一条指令的结果。当他们遇到条件跳转(if)时,他们有时必须等到管道为空才能知道指令指针应该走哪条路。

      我认为它是一列长途货运列车。它可以在直线上快速运载大量货物,但转弯很糟糕。

      Pentium 4 (Prescott) 有一条著名的长流水线,共有 31 个阶段。

      更多关于Wikipedia

      【讨论】:

        【解决方案8】:

        也许分支会杀死 CPU 指令预取?

        【讨论】:

        • 在我的...“研究”中,我了解了 switch 语句的跳转表和分支,但对 if 语句一无所知。你能详细说明一下吗?
        • IIRC,CPU 通常是沿着单个可能的执行路径预取指令,但是一个“if”语句会导致从预测的执行路径发生分支,它将使预取的指令无效,并且预技术将不得不重启。
        • 任何体面的处理器都应该具有分支预测功能,可以尝试猜测是否会采用分支,并根据预测预取指令(这通常非常好)。 GCC 甚至有 C 扩展,允许程序员为分支预测器提供提示。
        • 此外,CPU 通常会提前开始执行即将到来的指令(而不仅仅是预取它们),并且编译器会尝试重新排序指令,这会在分支之间变得危险,因此您可以真正杀死指令调度分支太多。这会损害性能。
        【解决方案9】:

        还要注意,循环内部不一定非常昂贵。

        现代 CPU 在第一次访问 if 语句时假定要使用“if-body”(或者换句话说:它还假定要多次使用循环体)(*)。在第二次和进一步访问时,它(CPU)可能会查看 Branch History Table,并查看上次情况如何(是真的吗?是假的吗?)。如果上次为假,则推测执行将继续执行 if 的“else”,或超出循环。

        (*) 规则实际上是“不采用前向分支,采用后向分支”。在 if 语句中,如果条件评估为 false,只有一个 [forward] 跳转(到 if-body 之后的点)(请记住:CPU 无论如何假设不采取分支/跳转),但在循环中,可能有一个向前分支到循环之后的位置(不采取),并且在重复时有一个向后分支(要采取)。

        这也是为什么调用虚函数或函数指针调用并不像许多人认为的那样糟糕的原因之一 (http://phresnel.org/blog/)

        【讨论】:

          【解决方案10】:

          正如许多人所指出的,条件分支在现代计算机上可能非常慢。

          话虽如此,有很多条件分支并不存在于 if 语句中,你不能总是知道编译器会想出什么,而且几乎总是担心基本语句需要多长时间做错事。 (如果你能知道编译器会可靠地生成什么,那么你可能没有一个好的优化编译器。)

          【讨论】:

            【解决方案11】:

            我能想到的唯一可能是指if 语句通常会导致分支的事实。根据处理器架构的具体情况,分支可能会导致流水线停顿或其他不太理想的情况。

            但是,这是针对特定情况的 - 大多数现代处理器都具有分支预测功能,试图将分支的负面影响降至最低。另一个例子是 ARM 架构(可能还有其他架构)如何处理条件逻辑 - ARM 具有指令级条件执行,因此简单的条件逻辑不会导致分支 - 如果条件不满足,指令将作为 NOP 执行。

            所有这些 - 在担心这些事情之前先弄清楚你的逻辑。不正确的代码尽可能地未经优化。

            【讨论】:

            • 我听说 ARM 的条件指令禁止 ILP,所以他们可能只是在推动问题。
            【解决方案12】:

            CPU 是深度流水线的。任何分支指令(if/for/while/switch/etc)都意味着 CPU 并不真正知道接下来要加载和运行什么指令。

            CPU 要么在等待知道要做什么时停止,要么 CPU 进行猜测。在较旧的 CPU 的情况下,或者如果猜测错误,您将不得不在它运行并加载正确的指令时遭受流水线停顿。根据 CPU 的不同,这可能高达 10-20 条指令的停顿。

            现代 CPU 试图通过进行良好的分支预测来避免这种情况,同时执行多个路径,并且只保留实际的路径。这有很大帮助,但只能做到这一点。

            祝你在课堂上好运。

            此外,如果您在现实生活中必须担心这一点,您可能正在从事操作系统设计、实时图形、科学计算或类似的 CPU 密集型工作。担心之前的个人资料。

            【讨论】:

              【解决方案13】:

              以最清晰、最简单、最干净的方式编写您的程序,而这显然不是低效的。这可以充分利用最昂贵的资源,你。无论是编写程序还是稍后调试(需要理解)程序。如果性能不够,测量瓶颈在哪里,并查看如何缓解它们。只有在极少数情况下,您才需要担心个别(源)指令。性能是关于在第一行选择正确的算法和数据结构,仔细编程,获得足够快的机器。使用好的编译器,当您看到现代编译器所做的那种代码重组时,您会感到惊讶。重构代码以提高性能是一种不得已而为之的措施,代码变得更复杂(因此更容易出错)、更难修改,因此更昂贵。

              【讨论】:

                【解决方案14】:

                一些 CPU(如 X86)提供编程级别的分支预测以避免这种分支预测延迟。

                一些编译器公开(如 GCC)这些作为高级编程语言(如 C/C++)的扩展。

                请参考likely()/unlikely() macros in the Linux kernel - how do they work? What's their benefit?

                【讨论】:

                  【解决方案15】:

                  在 ALU 使用方面最昂贵?它占用了 CPU 寄存器来存储要比较的值,并且每次运行 if 语句都需要时间来获取和比较值。

                  因此,一种优化是在循环运行之前进行一次比较并将结果存储为变量。

                  只是试图解释你遗漏的单词。

                  【讨论】:

                    【解决方案16】:

                    我曾经和我的一个朋友发生过这样的争论。他使用了一个非常幼稚的圆算法,但声称他的比我的更快(只计算圆的 1/8 的那种),因为我的使用了 if。最后, if 语句被替换为 sqrt 并且不知何故更快。可能是因为 FPU 内置了 sqrt?

                    【讨论】:

                      猜你喜欢
                      • 2015-12-23
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 2018-06-27
                      • 2011-05-03
                      • 1970-01-01
                      • 2013-10-12
                      • 2011-01-05
                      相关资源
                      最近更新 更多