ChatGPT解决这个技术问题 Extra ChatGPT

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

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

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

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

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

A
Adam Rosenfield

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

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

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

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

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

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

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


这不仅仅是分支错误预测。分支还在编译器级别以及在某种程度上在 CPU 级别(当然对于无序 CPU)也禁止指令重新排序。不错的详细答案。
如果高级语言最终被翻译成低级语言,并且您正在编写非常以性能为中心的代码,那么编写避免 if 语句的代码是否仍然一无所获?这个概念不带入高级语言吗?
您根本不会用高级语言编写以性能为中心的代码,以至于 if 语句很重要。高级语言中的性能关键代码并没有做任何太愚蠢的事情。
Why is processing a sorted array faster than processing an unsorted array? 就是一个很好的演示。正如您所说,无分支避免了错误预测的可能性,例如现代 gcc 或 clang 自动矢量化该示例时:Why is processing an unsorted array the same speed as processing a sorted array with modern x86-64 clang?。但在其他情况下,标量无分支可能比容易预测的分支更糟糕:gcc optimization flag -O3 makes code slower than -O2
J
Joel Coehoorn

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

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


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

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


P
Parappa

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

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

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


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

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

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


M
Marcin

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

一些测试说明

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

与此相关的费用:

低级比较——通常是 1 个 cpu 操作,超级便宜

潜在的跳跃——这可能很昂贵

解释为什么跳跃很昂贵:

您可以跳转到内存中任何位置的任意代码,如果结果证明它没有被 cpu 缓存 - 我们有问题,因为我们需要访问主内存,这比较慢

现代 CPU 进行分支预测。他们试图猜测是否会成功并在管道中提前执行代码,因此加快速度。如果预测失败,则管道提前完成的所有计算都必须无效。这也是一项昂贵的操作

所以总结一下:

如果可以的话,如果你真的,真的,真的很关心性能。

当且仅当您正在编写实时光线追踪器或生物模拟或类似的东西时,您才应该关心它。在大多数现实世界中,没有理由关心它。


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

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

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

Pentium 4 (Prescott) 拥有着名的 31 级长流水线。

更多关于Wikipedia


a
activout.se

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


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

另请注意,循环内部不一定非常昂贵。

现代 CPU 在第一次访问 if 语句时假定要采用“if-body”(或者反过来说:它还假定要多次采用循环体)(*)。在第二次和进一步访问时,它(CPU)可能会查看分支历史表,并查看上次条件如何(是真的吗?是假的吗?)。如果上次为假,则推测执行将继续执行 if 的“else”,或超出循环。

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

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


D
David Thornley

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

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


M
Michael Burr

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

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

说了这么多——在担心这些事情之前先弄清楚你的逻辑是正确的。不正确的代码是尽可能未经优化的。


我听说 ARM 的条件指令禁止 ILP,所以他们可能只是在推动问题。
t
tfinniga

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

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

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

祝你在课堂上好运。

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


v
vonbrand

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


C
Community

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

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

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


只有 Pentium 4 在 x86 机器代码中有硬件分支提示。但是布置分支以便通过函数的最可能路径是直线仍然有帮助:I-cache locality,并且没有采用分支最大化前端指令获取吞吐量(在大块中工作)。
佚名

就 ALU 使用而言,最昂贵的是?它使用 CPU 寄存器来存储要比较的值,并在每次运行 if 语句时占用时间来获取和比较值。

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

只是试图解释你丢失的单词。


D
Demur Rumed

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


E
E L

你的代码应该是可预测的和可能的。

如果你的整个程序是这样的:

诠释苹果 = 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

“简单的整数运算,例如加法、减法、比较、位运算和移位运算(以及增量运算符)在大多数微处理器上只需要一个时钟周期。”

增量运算符:i++优于++I;

布尔操作数:

In && 语句把最有可能是真的放在最后 In ||把最有可能是真的放在第一位。

因此,要回答您的问题,如果条件为真或可能为真,则 if 语句并不昂贵,否则会陷入分支错误预测。


编译器使用启发式方法来决定 if 的哪一侧最有可能运行或不运行。 (或者如果可用,来自运行时分析的数据;这称为“配置文件引导优化”,如 gcc -fprofile-generate / -fprofile-use)。它不像假设通常采用 if() 语句那样简单。即,当您在启用优化的情况下进行编译时,最好将 if (early_out) return 0; 替换为 if( !early_out ){}else{ return 0; }
对于标量整数,i++ 不优于 ++i;如果您不在同一个表达式中使用结果,它们是完全相等的,并且许多人喜欢 ++i 因为具有重载运算符的 C++ 类以这种方式编译得更好。此外,编译器已经将 for() 循环转换为 if(){ do{} while(); };请参阅Why are loops always compiled into "do...while" style (tail jump)? 当然,我说的是现代优化 C 编译器,例如 GCC、clang 和 MSVC。如果你有一个非常愚蠢的编译器,你可能需要像 asm 一样布置你的 C。
但是,其中一些是正确的,例如短路布尔值应将最有可能发生短路的条件放在首位。 (假设它们的评估都很便宜。)关于常量情况的“没有什么可预测的”答案的第一部分只有在您使用优化编译时才是正确的,因此常量传播使 if 总是被采用,所以编译器根本不会发出让 CPU 运行的分支指令。如果你编译时没有优化,或者编译器看不到 val 总是 1,CPU 仍然需要预测它。 (当然很容易预测)。
s
supercat

在许多较旧的处理器上,可以识别“如果”会很昂贵的情况和不会的情况,但是现代高性能处理器包括预测哪些分支将被采用和不会被采用的电路,并且分支仅在以下情况下成本高昂这样的电路猜错了。不幸的是,这通常使得确定编写一段代码的最佳方式变得非常困难,因为处理器完全有可能在处理人为的测试数据时正确预测分支结果,但在处理现实世界时猜测其中的许多结果是错误的数据,反之亦然。

除非有人试图优化其分支时序已被很好理解的特定目标的性能,否则最好的方法通常是假设分支时序不太可能成为整体性能的重要因素,除非或直到可以证明并非如此。分支时序可能会受到输入数据的细微差异的影响,并且通常没有实用的方法来确保测试数据包含可能影响性能的所有变化。


关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅