是“如果”昂贵?
在我的生活中,我不能记得那天我们的老师说了些什么,我希望你可能知道。
该模块是“数据结构和algorithm”,他告诉我们一些事情:
if语句是最昂贵的[something]。 [某事]登记[某事]。
是的,我有一个可怕的记忆,我真的很抱歉,但我一直在Google上search几个小时,没有出现。 有任何想法吗?
在最低层(在硬件上),是的, 如果 s很昂贵的话。 为了理解为什么,你必须了解pipe道是如何工作的。
要执行的当前指令存储在通常称为指令指针 (IP)或程序计数器 (PC)的某些内容中; 这些术语是同义词,但不同的术语与不同的体系结构一起使用。 对于大多数指令,下一条指令的PC只是当前的PC加上当前指令的长度。 对于大多数RISC体系结构来说,指令的长度都是固定的,所以PC可以增加一个常量。 对于x86等CISC体系结构,指令可以是可变长度的,所以解码指令的逻辑必须确定当前指令多长时间才能find下一条指令的位置。
但是,对于分支指令,下一条要执行的指令不是当前指令之后的下一个位置。 分支是gotos – 它们告诉处理器下一条指令的位置。 分支可以是有条件的也可以是无条件的,目标位置可以是固定的也可以是计算的。
有条件的和无条件的很容易理解 – 条件分支只有在某种条件成立的情况下才会被采用(例如一个数字是否等于另一个数字)。 如果没有采取分支,则控制像正常一样继续到分支之后的下一条指令。 对于无条件分支机构,总是采取分支机构。 条件分支出现在if
语句和for
循环的控制testing中。 无条件分支出现在无限循环,函数调用,函数返回, break
和continue
语句,臭名昭着的goto
语句等等(这些列表远非详尽无遗)。
分支目标是另一个重要问题。 大多数分支有一个固定的分支目标 – 他们去编译时固定的代码中的特定位置。 这包括if
语句,各种循环,常规函数调用等等。 计算分支在运行时计算分支的目标。 这包括switch
语句(有时),从函数返回,虚函数调用和函数指针调用。
那么这对性能意味着什么呢? 当处理器看到分支指令出现在其pipe线中时,需要弄清楚如何继续填充其pipe线。 为了弄清程序stream中分支之后有什么指令,需要知道两点:(1)分支是否被占用;(2)是分支的目标。 搞清楚这个叫做分支预测 ,这是一个具有挑战性的问题。 如果处理器猜测正确,程序将继续全速运行。 如果处理器不正确地猜测,它只是花了一些时间计算错误的东西。 它现在必须刷新其pipe道,并用正确执行path的指令重新加载它。 底线:一个很大的performance。
因此,如果报表很贵的原因是分支机构错误的预测 。 这只是在最低的水平。 如果您正在编写高级代码,则根本无需担心这些细节。 如果你正在用C语言或汇编写非常关键的代码,你应该只关心这个。 如果是这样的话,即使需要多个指令,编写无分支代码通常可以优于分支代码。 有一些很酷的小技巧可以用来计算像abs()
, min()
和max()
没有分支的东西。
“昂贵”是一个非常相对的术语,尤其是与“ if
”声明的关系,因为您还必须考虑到条件的成本。 这可以从几个简短的cpu指令到testing调用远程数据库的函数的结果。
我不会为此担心的。 除非你在做embedded式编程,否则你可能根本不应该关心“ if
”的代价。 对于大多数程序员来说,它不会成为应用程序性能的驱动因素。
分支,特别是在RISC架构微处理器上,是一些最昂贵的指令。 这是因为在许多体系结构中,编译器会预测哪个执行path最有可能,然后将这些指令放在可执行文件中,所以当分支发生时它们已经在CPUcaching中了。 如果分支以另一种方式运行,则必须返回主内存并获取新的指令 – 这相当昂贵。 在许多RISC体系结构中,除了分支(通常是2个周期),所有指令都是一个周期。 我们并不是在这里谈论一个主要的成本,所以不要担心。 另外,编译器会优化99%的时间。关于EPIC体系结构(Itanium就是一个例子),真正令人敬畏的事情之一是caching(并开始处理)来自分支两端的指令,然后丢弃一旦知道分支的结果就不需要的集合。 这节省了典型体系结构的额外内存访问,如果它沿着不可预测的path分支的话。
请查阅文章“ 更好的通过消除分支对性能的影响”的文章。 另一个有趣的是这个post关于实时碰撞检测博客无分支select 。
除了针对这个问题已经发布的优秀答案之外,我想提醒一下,尽pipe“if”语句被认为是昂贵的低级操作,试图在更高级别的环境中使用无分支编程技术,例如脚本语言或业务逻辑层(不pipe语言),可能是不合适的。
绝大多数情况下,程序应该首先写清楚,然后优化性能。 有很多问题领域的性能是至关重要的,但简单的事实是,大多数开发人员不是写模块深入渲染引擎的核心或高性能stream体动力学模拟运行几个星期结束。 当你的解决scheme的首要任务是“正常工作”时,你最头脑的事情应该是你是否可以节省代码中条件语句的开销。
现代处理器具有很长的执行stream水线,这意味着几条指令同时在不同的阶段执行。 当下一个指令开始运行时,他们可能并不总是知道一个指令的结果。 当他们遇到一个条件跳转(如果),他们有时必须等待,直到pipe道是空的,才可以知道指令指针应该走哪条路。
我认为这是一个很长的货运列车。 它可以在一条直线上快速运送大量的货物,但是它很糟糕。
奔腾4(普雷斯科特)有31个阶段的着名的长pipe道。
更多关于维基百科
也许分支杀死CPU指令预取?
在可能的最低级别( if
包含)(在计算特定if
所有特定于应用程序的先决条件之后):
- 一些testing指令
- 如果testing成功,则跳转到代码中的某处,否则继续前进。
与此相关的成本:
- 低级比较 – 通常1个CPU操作,超级便宜
- 潜在的跳跃 – 这可能是昂贵的
Reson为什么跳得很贵:
- 你可以跳转到内存中任何地方的代码,如果事实certificate它没有被cpucaching – 我们有一个问题,因为我们需要访问主存储器,这是较慢
- 现代CPU做分支预言。 他们试图猜测是否会成功,并且正在执行代码,所以加快速度。 如果预测失败,那么所有由stream水线完成的计算必须失效。 这也是一个昂贵的操作
所以总结一下:
- 如果可以预见,如果你真的,真的,关心性能。
- 你应该关心它, 当且仅当你正在写实时光线追迹或生物模拟或类似的东西。 在现实世界的大部分地方没有理由关心它。
我能想象到的唯一可能是指的是if
语句通常会导致分支。 根据处理器体系结构的具体情况,分支机构可能会导致stream水线停顿或其他不太理想的情况。
然而,这是特定的情况 – 大多数现代处理器具有分支预测能力,试图最小化分支的负面影响。 另一个例子是ARM体系结构(也可能是其他的)可以处理条件逻辑–ARM具有指令级的条件执行,所以简单的条件逻辑不会导致分支 – 如果条件不满足,指令只是作为NOP执行。
所有这一切 – 在担心这个问题之前先弄清楚你的逻辑。 不正确的代码像你所能得到的那样未被优化。
if
本身不慢的话。 缓慢总是相对的,我为我的生活打赌,你从来没有感受到if语句的“开销”。 如果你打算做一个高性能的代码,那么你仍然希望避免使用分支。 if
基于某种启发式和不足之处, if
处理器是从后加载代码的if
变得很慢。 由于处理器不知道将采取什么path(在stream水线处理器中,多个指令被交错和执行),它还将停止pipe线在机器码中的if
分支指令之后直接执行代码。 执行的代码可能必须反向执行(如果另一个分支被采用,则称为branch misprediction
),或者在这些地方填充noop
,以防止这种情况发生。
如果if
是邪恶的,那么switch
也是邪恶的, &&
, ||
太。 别担心。
CPU是深深的stream水线。 任何分支指令(如/ for / while / switch / etc)意味着CPU不知道接下来要加载和运行的指令。
在等待CPU知道该怎么做的时候,CPU或者停下来,或者CPU猜测。 在较旧的CPU的情况下,或者如果猜测是错误的,你将不得不忍受stream水线失速,并加载正确的指令。 根据CPU的不同,这可能会高达10-20个指令的失速。
现代CPU试图通过做好分支预测来避免这种情况,并且通过同时执行多个path,并且只保留实际的path。 这帮助了很多,但只能走得这么远。
祝你好运。
另外,如果您在现实生活中不得不担心这一点,那么您可能正在进行操作系统devise,实时graphics,科学计算或类似于CPU的操作。 简介之前担心。
还要注意,在一个循环内不一定非常昂贵。
现代CPU首先访问一个if语句,假设“if-body”将被采用(或者换句话说,它也假定一个循环体被多次采用)(*)。 在第二次和进一步访问时,它(CPU)可以查看分支历史logging表 ,并查看条件是最后一次(这是真的吗?它是假的?)。 如果最后一次是错误的,那么投机性执行就会继续执行if或else循环的“else”。
(*)规则实际上是“ 前进分支未采取,后退分支采取 ”。 在一个if语句中,如果条件计算结果为false(记住:CPU无论如何都不采取分支/跳转), 只有一个[forward]跳转(到if-body之后 ),但是在循环,可能有一个前向分支到循环后面的位置(不被采用),而后面的分支在重复(要被采用)。
这也是为什么调用虚函数或函数指针调用并不像许多人认为的那样糟糕的原因之一( http://phresnel.org/blog/ )
ALU使用最贵? 它使用CPU寄存器来存储要比较的值,并在每次运行if语句时花费时间来获取和比较值。
因此,优化是在循环运行之前进行一次比较并将结果存储为一个variables。
只是试图解释你遗漏的话。
我和我的一个朋友曾经有过这个争执。 他正在使用一个非常天真的圈子algorithm,但声称他比我的更快(那种只计算圈的1/8),因为我用的是if。 最后,if语句被sqrt取代,不知何故,速度更快。 也许是因为FPU有sqrt内置?
正如许多人指出的,有条件的分支机构在现代计算机上可能会很慢。
也就是说,有很多条件分支不能生活在if语句中,你不能总是知道编译器会提出什么,而担心基本语句将花费多less时间实际上总是错误的东西去做。 (如果你能确定编译器能够可靠地生成什么,你可能没有一个好的优化编译器。)
一些CPU(如X86)为编程级别提供分支预测,以避免这样的分支预测延迟。
一些编译器(像GCC)将这些扩展为高级编程语言(如C / C ++)的扩展。
在Linux内核中引用可能的()/不太可能的()macros – 它们是如何工作的? 他们有什么好处? 。
写出你的程序最清晰,最简单,最清洁的方式,效率并不明显。 这使您能够最好地利用最昂贵的资源。 无论是写或稍后debugging(需要理解)的程序。 如果性能不够,请测量瓶颈所在,并了解如何缓解这些瓶颈。 只有在非常罕见的情况下,您才需要担心个人(来源)的指示。 性能是关于在第一行中select正确的algorithm和数据结构,仔细编程,获得足够快的机器。 使用一个好的编译器,当看到现代编译器重构的代码时,你会感到惊讶。 重组代码的性能是一种最后的手段措施,代码变得更复杂(因此buggier),难以修改,从而全面的更昂贵。