我一辈子都记不住老师那天到底说了什么,我希望你可能知道。
该模块是“数据结构和算法”,他告诉我们一些类似的事情:
if
声明是最昂贵的[东西] 。[某事]注册[某事]。
是的,我确实有一个可怕的记忆,我真的很抱歉,但我已经搜索了几个小时,但没有任何结果。有任何想法吗?
我一辈子都记不住老师那天到底说了什么,我希望你可能知道。
该模块是“数据结构和算法”,他告诉我们一些类似的事情:
if
声明是最昂贵的[东西] 。[某事]注册[某事]。
是的,我确实有一个可怕的记忆,我真的很抱歉,但我已经搜索了几个小时,但没有任何结果。有任何想法吗?
在最低级别(在硬件中),是的,如果s 很昂贵。为了理解原因,您必须了解管道是如何工作的。
当前要执行的指令存储在通常称为指令指针(IP)或程序计数器(PC)的东西中;这些术语是同义词,但不同的术语用于不同的架构。对于大多数指令,下一条指令的 PC 只是当前 PC 加上当前指令的长度。对于大多数 RISC 架构,指令的长度都是恒定的,因此 PC 可以以恒定的量递增。对于 x86 等 CISC 架构,指令可以是可变长度的,因此对指令进行解码的逻辑必须计算出当前指令需要多长时间才能找到下一条指令的位置。
然而,对于分支指令,要执行的下一条指令不是当前指令之后的下一个位置。分支是 goto - 它们告诉处理器下一条指令在哪里。分支可以是有条件的或无条件的,目标位置可以是固定的或计算的。
有条件的与无条件的很容易理解——只有在某个条件成立时才会采用条件分支(例如一个数字是否等于另一个);如果不采取分支,则控制正常进行分支之后的下一条指令。对于无条件分支,总是采用分支。条件分支出现在if
语句for
和while
循环的控制测试中。无条件分支出现在无限循环、函数调用、函数返回break
和continue
语句、臭名昭著的goto
语句等等(这些列表远非详尽)中。
分支目标是另一个重要问题。大多数分支都有一个固定的分支目标——它们转到代码中的特定位置,该位置在编译时是固定的。这包括if
语句、各种循环、常规函数调用等等。 计算分支在运行时计算分支的目标。这包括switch
语句(有时)、从函数返回、虚函数调用和函数指针调用。
那么这一切对性能意味着什么呢?当处理器看到一条分支指令出现在它的流水线中时,它需要弄清楚如何继续填满它的流水线。为了弄清楚程序流中分支之后的指令是什么,它需要知道两件事:(1)是否会采用分支和(2)分支的目标。弄清楚这一点称为分支预测,这是一个具有挑战性的问题。如果处理器猜对了,程序就会全速运行。相反,如果处理器猜错了,它只是花了一些时间计算错误的东西。它现在必须刷新其管道并使用来自正确执行路径的指令重新加载它。底线:性能大受打击。
因此,if 语句昂贵的原因是由于分支错误预测。这只是最低级别的。如果您正在编写高级代码,则根本不需要担心这些细节。只有在使用 C 或汇编语言编写对性能至关重要的代码时,才应该关心这一点。如果是这样的话,即使需要更多的指令,编写无分支代码通常也优于有分支的代码。您可以使用一些很酷的小技巧来计算诸如abs()
、min()
和max()
没有分支之类的东西。
“昂贵”是一个非常相对的术语,尤其是与“ if
”语句的关系,因为您还必须考虑条件的成本。范围可以从几条简短的 cpu 指令到测试调用远程数据库的函数的结果。
我不会担心的。除非您正在做嵌入式编程,否则您可能根本不应该担心“ if
”的成本。对于大多数程序员来说,它永远不会成为您应用程序性能的驱动因素。
分支,尤其是在 RISC 架构微处理器上,是一些最昂贵的指令。这是因为在许多架构上,编译器会预测最有可能采用哪条执行路径并将这些指令放在可执行文件中,因此当分支发生时它们已经在 CPU 缓存中。如果分支走另一条路,它必须返回主内存并获取新指令——这是相当昂贵的。在许多 RISC 架构上,所有指令都是一个周期,除了分支(通常是 2 个周期)。我们在这里谈论的不是主要成本,所以不用担心。此外,编译器在 99% 的时间里会比你优化得更好:) 关于 EPIC 架构(安腾就是一个例子)真正令人敬畏的事情之一是它缓存(并开始处理)来自分支两侧的指令,然后一旦分支的结果是丢弃它不需要的集合已知。如果典型架构沿着不可预测的路径分支,这可以节省额外的内存访问。
查看关于单元性能的文章通过消除分支获得更好的性能。另一个有趣的是这篇关于实时碰撞检测博客上的无分支选择的帖子。
除了已经针对此问题发布的出色答案之外,我想提醒一下,尽管“if”语句被认为是昂贵的低级操作,但尝试在更高级别的环境中使用无分支编程技术,例如脚本语言或业务逻辑层(不管语言),可能是非常不合适的。
绝大多数情况下,程序应该首先为了清晰而编写,然后为了性能而优化。有许多问题领域的性能是最重要的,但一个简单的事实是,大多数开发人员并没有编写用于渲染引擎核心或连续运行数周的高性能流体动力学模拟的模块。当您的解决方案的首要任务是“正常工作”时,您最不想考虑的应该是您是否可以节省代码中条件语句的开销。
if
本身并不慢。缓慢总是相对的,我敢打赌,你从来没有感觉到 if 语句的“开销”。如果您要编写高性能代码,则可能无论如何都希望避免分支。缓慢的原因是处理器基于一些启发式和诸如此类的东西if
从之后预加载代码。if
它还将停止流水线if
在机器代码中的分支指令之后直接执行代码,因为处理器还不知道将采用什么路径(在流水线处理器中,多条指令被交错并执行)。执行的代码可能必须反向执行(如果采用另一个分支。它被称为branch misprediction
),或者noop
在那些地方填充 ' 以便不会发生这种情况。
如果if
是邪恶的,那么switch
也是邪恶的,并且&&
,||
也是。别担心。
在可能的最低级别if
包括(在计算所有特定于应用程序的先决条件之后if
):
与此相关的费用:
解释为什么跳跃很昂贵:
所以总结一下:
现代处理器有很长的执行流水线,这意味着几条指令同时在不同的阶段执行。当下一条指令开始运行时,他们可能并不总是知道一条指令的结果。当他们遇到条件跳转(如果)时,他们有时必须等到管道为空才能知道指令指针应该走哪条路。
我认为它是一列长途货运列车。它可以在直线上快速运载大量货物,但转弯很糟糕。
Pentium 4 (Prescott) 拥有着名的 31 级长流水线。
更多关于维基百科
也许分支会杀死 CPU 指令预取?
另请注意,循环内部不一定非常昂贵。
现代 CPU 在第一次访问 if 语句时假定要采用“if-body”(或者反过来说:它还假定要多次采用循环体)(*)。在第二次和进一步访问时,它(CPU)可能会查看分支历史表,并查看上次条件如何(是真的吗?是假的吗?)。如果上次为假,则推测执行将继续执行 if 的“else”,或超出循环。
(*) 规则实际上是“未采用前向分支,采用后向分支”。在 if 语句中,如果条件评估为假,则只有[forward] 跳转(到if-body 之后的点)(请记住:CPU 无论如何都假定不进行分支/跳转),但在循环中,循环之后的位置可能有一个前向分支(不被采用),重复时可能有一个向后分支(被采用)。
这也是为什么调用虚函数或函数指针调用并不像许多人认为的那样糟糕的原因之一(http://phresnel.org/blog/)
正如许多人所指出的,条件分支在现代计算机上可能非常慢。
话虽这么说,有很多条件分支并不存在于 if 语句中,你不能总是知道编译器会想出什么,担心基本语句需要多长时间实际上总是错误的去做。(如果你能知道编译器会可靠地生成什么,那么你可能没有一个好的优化编译器。)
我能想象的唯一可能是指一个if
语句通常会导致分支的事实。根据处理器架构的具体情况,分支可能会导致流水线停顿或其他不太理想的情况。
然而,这是非常特殊的情况——大多数现代处理器都具有分支预测功能,试图将分支的负面影响降至最低。另一个例子是 ARM 架构(可能还有其他架构)如何处理条件逻辑 - ARM 具有指令级条件执行,因此简单的条件逻辑不会导致分支 - 如果条件不满足,指令将简单地作为 NOP 执行。
说了这么多——在担心这些事情之前先弄清楚你的逻辑是正确的。不正确的代码是尽可能未经优化的。
CPU 是深度流水线的。任何分支指令(if/for/while/switch/etc)都意味着 CPU 并不真正知道接下来要加载和运行什么指令。
CPU 要么在等待知道要做什么时停止,要么 CPU 进行猜测。对于较旧的 CPU,或者如果猜测错误,您将不得不在它运行并加载正确指令时遭受流水线停顿。根据 CPU 的不同,这可能高达 10-20 条指令的停顿。
现代 CPU 试图通过进行良好的分支预测来避免这种情况,同时执行多个路径,并且只保留实际的路径。这有很大帮助,但只能走这么远。
祝你在课堂上好运。
此外,如果您在现实生活中必须担心这一点,您可能正在做操作系统设计、实时图形、科学计算或类似 CPU 密集型的事情。担心之前的个人资料。
以最清晰、最简单、最干净的方式编写程序,这显然不是低效的。这可以充分利用最昂贵的资源,你。无论是编写程序还是稍后调试(需要理解)程序。如果性能不够,请测量瓶颈在哪里,看看如何缓解它们。只有在极少数情况下,您才需要担心个别(源)指令。性能是关于在第一行选择正确的算法和数据结构,仔细编程,获得足够快的机器。使用好的编译器,当您看到现代编译器所做的那种代码重组时,您会感到惊讶。重构代码以提高性能是一种最后的手段,代码变得更复杂(因此更容易出错),更难修改,因此更昂贵。
一些 CPU(如 X86)为编程级别提供分支预测以避免这种分支预测延迟。
一些编译器公开(如 GCC)这些作为高级编程语言(如 C/C++)的扩展。
就 ALU 使用而言,最昂贵的是?它使用 CPU 寄存器来存储要比较的值,并在每次运行 if 语句时占用时间来获取和比较值。
因此,对此的优化是在循环运行之前进行一次比较并将结果存储为变量。
只是试图解释你丢失的单词。
我曾经和我的一个朋友发生过这样的争论。他使用了一个非常幼稚的圆算法,但声称他的比我的更快(只计算圆的 1/8 的那种),因为我的使用了 if。最后, if 语句被替换为 sqrt 并且不知何故更快。也许是因为 FPU 内置了 sqrt?
你的代码应该是可预测的和可能的。
如果你的整个程序是这样的:
诠释苹果 = 1;
if (apple == 1) then 这是可预测且可能的代码。
它也是优化的代码,因为您使编译器和 cpu 变得容易;他们不必预测任何事情,因此不存在代价高昂的错误预测,即分支错误预测。
所以你试着写一个程序,让每一行都是一个自我实现的预言。你有 3 种筹码:Truth、False 和 Unknown。您正在尝试仅使用 Truth 芯片构建程序。
为此:
If else: if should be more likely and if there is a return that should be in else.
For and While should be replace by: do while -> except if there is a continue.
That continue should then become an: if do while -> in that order.
If it absolutely necessary to test at beginning use: if do while
If there is less than 5 cases switch to if else from most likely to least likely
Cases should be of relative likelihood, otherwise should be expressed as if else before switch.
Bitwise operators and better logical operators
“简单的整数运算,例如加法、减法、比较、位运算和移位运算(以及增量运算符)在大多数微处理器上只需要一个时钟周期。”</p>
增量运算符:i++优于++I;
布尔操作数:
因此,要回答您的问题,如果条件为真或可能为真,则 if 语句并不昂贵,否则会陷入分支错误预测。
在许多较旧的处理器上,可以识别“如果”会很昂贵的情况和不会的情况,但是现代高性能处理器包括预测哪些分支将被采用和不会被采用的电路,并且分支仅在以下情况下成本高昂这样的电路猜错了。不幸的是,这通常使得确定编写一段代码的最佳方式变得非常困难,因为处理器完全有可能在处理人为的测试数据时正确预测分支结果,但在处理现实世界时猜测其中的许多结果是错误的数据,反之亦然。
除非有人试图优化其分支时序已被很好理解的特定目标的性能,否则最好的方法通常是假设分支时序不太可能成为整体性能的重要因素,除非或直到可以证明并非如此。分支时序可能会受到输入数据的细微差异的影响,并且通常没有实用的方法来确保测试数据包含可能影响性能的所有变化。