C ++编译器会删除/优化无用的括号吗?


19

请问代码

int a = ((1 + 2) + 3); // Easy to read

运行慢于

int a = 1 + 2 + 3; // (Barely) Not quite so easy to read

还是现代的编译器足够聪明,可以删除/优化“无用的”括号。

看起来似乎很少有优化方面的问题,但是选择C ++而不是C#/ Java / ...都是关于优化(IMHO)的。


9
我认为C#和Java也将对此进行优化。我相信当他们解析并创建AST时,他们只会删除明显的无用的东西。
Farid Nouri Neshat 2014年

5
我读过的所有内容都指向JIT编译,很容易就可以提前进行编译,因此,仅凭这个并不是一个非常引人注目的论点。您带来了游戏编程-支持提前编译的真正原因是可以预见的-使用JIT编译,您永远不知道编译器何时启动并尝试开始编译代码。但是我要指出的是,对本机代码的提前编译并不与垃圾回收互斥,例如参见Standard ML和D。而且我已经看到令人信服的论点,即垃圾回收更有效……
Doval

7
...比RAII和智能指针更重要,所以更多的是遵循那些人迹罕至的路径(C ++)和相对未读的以这些语言进行游戏编程的路径。我还要指出,担心括号是疯狂的-我知道您来自哪里,但这是一个荒谬的微观优化。程序中数据结构和算法的选择肯定会决定性能,而不是琐碎的事情。
2014年

6
嗯...到底是什么样的优化?如果您正在谈论静态分析,在我所知道的大多数语言中,它将被静态已知结果替代(基于LLVM的实现甚至强制执行此操作,即AFAIK)。如果您在谈论执行顺序,那没关系,因为它是相同的操作且没有副作用。无论如何,加法需要两个操作数。而且,如果您使用它来比较C ++,Java和C#在性能方面,听起来好像您不清楚什么是优化以及它们如何工作,因此您应该专注于学习。
Theodoros Chatzigiannakis 2014年

5
我不知道为什么:a)您认为带括号的表达式更易读(对我而言,它看起来很丑陋,具有误导性(为什么他们强调此特定顺序?在这里不应该如此吗?)且笨拙)b)为什么您会认为没有用括号括起来可能会更好(显然,解析机器对机器而言,比必须考虑操作员固定性要容易得多。正如Marc van Leuwen所说,这对运行时间完全没有影响)。
2014年

Answers:


87

编译器实际上从未插入或删除括号;它只是创建一个与您的表达式相对应的解析树(其中没有括号),并且这样做必须遵守您编写的括号。如果您完全用括号括起来,那么对人类读者来说,解析树将是什么也将立即清晰可见。如果您像这样极端地放入多余的括号,int a = (((0)));则会对阅读器的神经元造成不必要的压力,同时还会浪费解析器中的一些周期,而不会更改结果的解析树(因此不会更改生成的代码) )一点点。

如果您写任何括号,那么解析器仍然必须完成创建解析树的工作,并且运算符优先级和关联性的规则会准确地告诉它必须构造的解析树。您可能会认为这些规则是告诉编译器应该在代码中插入哪个(隐式)括号,尽管在这种情况下解析器实际上从未处理过括号:它只是构造为产生与括号相同的解析树出现在某些地方。如果将括号恰好放在这些位置(如的int a = (1+2)+3;关联性+在左侧),则解析器将通过稍有不同的路径获得相同的结果。如果您将括号放在int a = 1+(2+3);那么您将强制使用不同的解析树,这可能会导致生成不同的代码(尽管可能不会,因为编译器可能会在构建解析树后应用转换,只要执行生成的代码的效果永远不会不同它)。假设重新启动的代码有所不同,那么一般来讲,哪一个效率更高?当然,最重要的一点是,在大多数情况下,解析树不会给出数学上相等的表达式,因此比较它们的执行速度就不重要了:应该只写一个给出正确结果的表达式。

因此,结果是:根据正确性和可读性的需要使用括号;如果是冗余的,它们对执行速度完全没有影响(对编译时间的影响可以忽略不计)。

而且,这与优化无关,优化是在解析树构建之后进行的,因此它不知道解析树是如何构造的。从最老,最笨的编译器到最聪明,最现代的编译器,都无需更改。只有使用解释性语言(“编译时间”和“执行时间”一致)时,多余的括号才有可能受到惩罚,但是即使如此,我仍然认为大多数此类语言都是经过组织的,因此至少解析阶段仅进行一次对于每个语句(将其存储为预先准备好的形式以供执行)。


s / oldes / oldest /。好的答案,+ 1。
戴维·康拉德

23
总的“挑剔”警告: “问题不是很好。”我不同意,“很好”的意思是“清楚地表明该查询者想填补哪些知识空白”。本质上,问题是“优化X,选择A或B,为什么?在下面发生什么?”,至少对我来说,这很清楚地表明了知识差距是什么。您正确指出并很好解决的问题中的缺陷是,它基于错误的心理模型。
乔纳斯·科尔克(JonasKölker)

对于a = b + c * d;a = b + (c * d);将是[无害的]多余的括号。如果它们可以帮助您使代码更具可读性,那就好。a = (b + c) * d;将会是非冗余的括号-它们实际上会更改结果分析树并给出不同的结果。这样做完全合法(实际上是必要的),但是它们与隐含的默认分组不同。
Phil Perry

1
@OrangeDog是的,这真是令人遗憾。
gbjbaanb 2014年

1
@JonasKölker:我的开头句子实际上是标题中提到的问题:一个人不能真正回答有关编译器是插入还是删除括号的问题,因为这是基于对编译器操作方式的误解。但我同意,很明显需要解决哪些知识鸿沟。
Marc van Leeuwen 2014年

46

括号仅是为了您的利益,而不是编译器。编译器将创建正确的机器代码来表示您的语句。

仅供参考,编译器足够聪明,可以对其进行完全优化。在您的示例中,这将int a = 6;在编译时变成。


9
绝对-随您喜欢插入尽可能多的括号,然后让编译器进行艰苦的工作来找出所需的内容:)
gbjbaanb 2014年

23
@Serge真正的编程更多是关于代码的可读性,而不是性能。明年,当您需要调试崩溃时,您将讨厌自己,只有“优化”代码可以通过。
棘轮怪胎

1
@ratchetfreak,您说得对,但我也知道如何注释我的代码。整数= 6; // =(1 + 2)+ 3
Serge

20
@Serge我知道经过一年的调整不会停滞不前,及时的注释和代码将不同步,然后您最终会遇到int a = 8;// = 2*3 + 5
棘手的怪胎2014年

21
或www.thedailywtf.com:int five = 7; //HR made us change this to six...
鸣叫鸭

23

您实际提出的问题的答案是“否”,但是您要提出的问题的答案是“是”。添加括号不会降低代码速度。

您问了一个关于优化的问题,但是括号与优化无关。编译器应用了各种优化技术,旨在提高所生成代码的大小或速度(有时两者)。例如,它可以采用表达式A ^ 2(A的平方),然后用A x A(乘以A的乘积)代替它,这样会更快。答案是否定的,编译器在优化阶段没有任何不同,这取决于是否存在括号。

我想您想问的是,如果在表达式中添加不必要的括号,在您认为可能会提高可读性的地方,编译器是否仍会生成相同的代码。换句话说,如果添加括号,则编译器足够聪明,可以再次将其删除,而不必以某种方式生成较差的代码。答案是肯定的。

让我仔细地说。如果在表达式中添加绝对不必要的括号(对表达式的含义或求值顺序没有影响),编译器将静默丢弃它们并生成相同的代码。

但是,存在某些表达式,其中显然不必要的括号实际上会改变表达式的求值顺序,在这种情况下,编译器将生成使您实际编写的内容生效的代码,这可能与您的预期有所不同。这是一个例子。不要这样!

short int a = 30001, b = 30002, c = 30003;
int d = -a + b + c;    // ok
int d = (-a + b) + c;  // ok, same code
int d = (-a + b + c);  // ok, same code
int d = ((((-a + b)) + c));  // ok, same code
int d = -a + (b + c);  // undefined behaviour, different code

因此,如果需要,请添加括号,但请确保确实没有必要!

我从来没有做。存在没有真正利益的错误风险。


脚注:当有符号整数表达式的值超出其可表示的范围(在这种情况下为-32767至+32767)时,将发生无符号行为。这是一个复杂的主题,超出了此答案的范围。


最后一行中未定义的行为是因为有符号的短路符在符号后仅具有15位,所以最大大小为32767,对吗?在这个简单的例子中,编译器应该警告溢出,对吗?+1是一个反例,无论哪种方式。如果它们是函数的参数,则不会收到警告。此外,如果a可以真正地进行无符号签名-a + b,那么如果a是负数也可以是b正数,则导致计算也很容易溢出。
Patrick M

@PatrickM:请参见编辑。未定义的行为表示编译器可以执行其喜欢的操作,包括是否发出警告。无符号算术运算不会产生UB,但会以下一个2的高次幂为模降低。
david.pfx 2014年

(b+c)最后一行中的表达式会将其参数提升为int,因此,除非编译器将其定义int为16位(要么因为它是古老的,要么针对一个小型微控制器),否则最后一行将是完全合法的。
超级猫

@supercat:我不这么认为。通用类型和结果类型应为short int。如果不是有人问过这个问题,也许您想提出一个问题?
david.pfx

@ david.pfx:C算术提升的规则非常清楚:所有小于int被提升为的内容,int除非该类型无法表示其所有值,否则将被提升为unsigned int如果所有定义的行为都与包含促销一样,则编译器可以省略促销。在16位类型表现为环绕抽象代数环的机器上,(a + b)+ c和a +(b + c)是等效的。但是,如果int是陷入溢出的16位类型,那么在某些情况下会出现以下表达式之一……
supercat

7

括号仅用于您操作运算符优先级的顺序。编译后,括号不再存在,因为运行时不需要它们。编译过程会删除您和我需要的所有方括号,空格和其他语法糖,并将所有运算符更改为使计算机执行起来更简单的操作。

因此,您和我可能会在哪里看到...

  • “ int a =((1 + 2)+ 3);”

...编译器可能会发出类似以下的内容:

  • 字符[1] ::“ a”
  • Int32 :: DeclareStackVariable()
  • 整数32 :: 0x00000001
  • 整数32 :: 0x00000002
  • Int32 :: Add()
  • 整数32 :: 0x00000003
  • Int32 :: Add()
  • Int32 :: AssignToVariable()
  • void :: DiscardResult()

通过从头开始并依次执行每个指令来执行程序。
运营商的优先级现在为“先到先得”。
一切都是强类型的,因为编译器可以全部工作出,而这是撕开原来的语法。

好的,这与您和我所处理的东西完全不同,但是然后我们就没有运行它!


4
没有一个C ++编译器可以像这样远程生成任何东西。通常,它们会生成实际的CPU代码,而汇编程序也不会这样做。
MSalters 2014年

3
此处的目的是演示编写的代码和编译的输出之间的结构差异。即使在这里,大多数人也无法阅读实际的机器代码或汇编
DHall 2014年

@MSalters如果您将LLVM ISA视为“类似的东西”(它不是基于堆栈的SSA),则Nitpicking铛会发出“类似的东西”。鉴于可以为LLVM编写JVM后端,而JVM ISA(AFAIK)是基于堆栈的clang-> llvm-> JVM看起来非常相似。
Maciej Piechotka

我认为LLVM ISA无法使用运行时字符串文字(仅前两个指令)来定义名称堆栈变量。这严重混合了运行时和编译时。区别很重要,因为这个问题正是关于这种混乱的。
MSalters 2014年

6

取决于它是否是浮点数:

  • 在浮点运算中,加法不是关联的,因此优化器无法对操作进行重新排序(除非您添加fastmath编译器开关)。

  • 在整数运算中,它们可以重新排序。

在您的示例中,两者将在完全相同的时间运行,因为它们将编译为完全相同的代码(加法运算从左到右)。

但是即使Java和C#也可以对其进行优化,但它们只会在运行时进行。


+1表示浮点运算不是关联的。
德瓦尔(Doval)

在问题的示例中,括号不会更改默认的(左)关联性,因此这一点没有意义。
马克·范·吕文

1
关于最后一句话,我不这么认为。在java a和c#中,编译器都会生成优化的字节码/ IL。运行时不受影响。
Stefano Altieri 2014年

IL在这种类型的表达式上不起作用,指令从堆栈中获取一定数量的值,然后将一定数量的值(通常为0或1)返回到堆栈中。谈论这种在C#中运行时优化的事情是胡说八道。
乔恩·汉纳

6

典型的C ++编译器转换为机器代码,而不是C ++本身。是的,它删除了无用的paren,因为到完成时,根本没有paren。机器代码不能那样工作。



1

不,但是是的,但是也许,但是也许相反,但是没有。

正如人们已经指出的那样(假设加法是左相关的语言,例如C,C ++,C#或Java),该表达式((1 + 2) + 3)完全等同于1 + 2 + 3。它们是用不同的方式在源代码中编写内容的方式,这对最终的机器代码或字节代码产生零影响。

无论哪种方式,结果都将是一条指令,例如,添加两个寄存器,然后添加第三个,或者从堆栈中获取两个值,将其添加,推回,然后将其与另一个相加,或者在其中添加三个寄存器。一次操作,或以其他方式求和三个数字的方法,具体取决于下一级最有意义的内容(机器码或字节码)。在字节码的情况下,这反过来可能会经历类似的重组,例如,与之等效的IL(这将是一系列加载到堆栈,并弹出对以进行添加,然后推回结果)不会在机器代码级别上直接获得该逻辑的副本,但是对于所讨论的机器来说更明智。

但是您的问题还有更多。

对于任何理智的C,C ++,Java或C#编译器,我希望给出的两个语句的结果与以下内容完全相同:

int a = 6;

为什么结果代码会浪费时间对文字进行数学运算?对程序状态的任何更改都不会停止1 + 2 + 3being 的结果6,因此这就是执行代码中的内容。确实,甚至可能还没有(取决于您对那6的处理方式,也许我们可以把整个东西扔掉;甚至是C#的哲学是“不要大量优化,因为抖动无论如何都会优化这一点”)等同于int a = 6或不必要地将其扔掉)。

但是,这可能会导致我们扩展您的问题。考虑以下:

int a = (b - 2) / 2;
/* or */
int a = (b / 2)--;

int c;
if(d < 100)
  c = 0;
else
  c = d * 31;
/* or */
int c = d < 100 ? 0 : d * 32 - d
/* or */
int c = d < 100 && d * 32 - d;
/* or */
int c = (d < 100) * (d * 32 - d);

(请注意,这最后两个示例不是有效的C#,而此处的其他所有示例都是有效的,并且它们在C,C ++和Java中均有效。)

同样,在输出方面,我们具有完全等效的代码。由于它们不是常量表达式,因此不会在编译时进行计算。一种形式可能比另一种形式更快。哪个更快?那将取决于处理器,或者可能取决于状态的一些相当任意的差异(特别是因为如果一个更快,那么它不可能快得多)。

它们并不是完全与您的问题无关,因为它们主要涉及概念上完成操作的顺序的差异。

在每一个中,都有理由怀疑一个可能比另一个更快。单个减量可能会有专门的指令,因此(b / 2)--确实比快(b - 2) / 2d * 32或许可以产生将其变成快d << 5所以使得d * 32 - d速度比d * 31。后两者之间的差异特别有趣。一种允许在某些情况下跳过某些处理,但是另一种避免了分支预测错误的可能性。

因此,这给我们留下了两个问题:1.一个实际上比另一个更快吗?2.编译器会把较慢的速度转换成较快的速度吗?

答案是1。这取决于。2.也许吧。

还是要扩展,这取决于它取决于所讨论的处理器。当然,已经存在处理器,其中一个的幼稚的机器代码等效于另一个的幼稚的机器代码等效。在电子计算的整个过程中,也没有一个总是总是更快的(特别是在非流水线CPU更为普遍时,分支错误预测元素与许多元素都不相关)。

也许是因为编译器(以及抖动和脚本引擎)会进行很多不同的优化,尽管在某些情况下可能会强制执行某些优化,但我们通常可以找到一些逻辑上等效的代码,即使是最幼稚的编译器也具有完全相同的结果和一些逻辑上等效的代码,即使最复杂的代码也可以为其中一个生成比其他代码更快的代码(即使为了证明我们的观点而必须编写完全病理的代码)。

这似乎只是一个很小的优化问题,

不会。即使存在比我在这里给出的更复杂的差异,这似乎也与优化毫无关系。如果有的话,这是一个悲观的问题,因为您怀疑较难阅读的内容((1 + 2) + 3可能比较容易阅读的内容要慢1 + 2 + 3

但是选择C ++而不是C#/ Java / ...都是关于优化(IMHO)的。

如果那确实是选择C ++而不是C#或Java的全部目的,那我想说人们应该刻录他们的Stroustrup和ISO / IEC 14882副本,并释放其C ++编译器的空间,以便为更多MP3或其他内容留出空间。

这些语言彼此之间具有不同的优势。

其中之一是C ++通常在内存使用方面仍然更快,更轻便。是的,在某些示例中,C#和/或Java速度更快和/或具有更好的应用程序生命周期内存使用,并且随着所涉及技术的改进,它们变得越来越普遍,但是我们仍然可以预期,用C ++编写的平均程序会一个较小的可执行文件,与这两种语言中的任何一种相比,它的工作速度更快,使用的内存更少。

这不是最优化。

优化有时被用来表示“使事情变得更快”。这是可以理解的,因为通常当我们真正在谈论“优化”时,我们实际上是在谈论使事情进展得更快,所以一个已经成为另一个的简写,我承认我自己这样滥用这个词。

“使事情前进得更快”的正确说法不是优化。正确的词是改善。如果您对程序进行更改,唯一有意义的区别就是它现在更快,没有任何优化,那就更好了。

优化是指我们在特定方面和/或特定情况方面进行改进时。常见的例子有:

  1. 现在,对于一种用例而言,它更快,但是对于另一种用例而言,它却很慢。
  2. 现在速度更快,但占用更多内存。
  3. 现在,它的内存更轻,但速度更慢。
  4. 现在速度更快,但更难维护。
  5. 现在更容易维护,但速度较慢。

这种情况是合理的,例如:

  1. 更快的用例一开始更普遍或更严重地受到阻碍。
  2. 该程序的速度令人无法接受,而且我们有大量的可用RAM。
  3. 该程序停顿了下来,因为它使用了太多的RAM,比执行超快速处理花费了更多的时间交换。
  4. 该程序的速度令人无法接受,并且难以理解的代码已被很好地记录且相对稳定。
  5. 该程序仍然可以接受,而且速度更快,并且更易于理解的代码库维护成本较低,并且可以更轻松地进行其他改进。

但是,在其他情况下,这种情况也不会被证明是正确的。优化。

语言的选择确实会产生影响,因为速度,内存使用和可读性都会受其影响,但是与其他系统的兼容性,库的可用性,运行时的可用性以及给定操作系统上这些运行时的成熟度也会受到影响。 (由于我的过失,我最终以某种方式最终选择了Linux和Android作为我最喜欢的操作系统,并以C#作为我最喜欢的语言,虽然Mono很棒,但是我还是遇到了很多麻烦)。

如果您认为C ++确实很糟糕,那么说“在C#/ Java / ...上选择C ++完全是关于优化”才有意义,因为优化是“尽管...更好”而不是“更好”。如果您认为C ++尽管本身就更好,那么您需要做的最后一件事就是担心这种微小的微选择。确实,最好完全放弃它。快乐的黑客也是一种需要优化的素质!

但是,如果您倾向于说“我爱C ++,而我所钟爱的事情之一就是挤出额外的循环”,那么那就不一样了。仍然有一种情况,微型选择只有在可以养成反身习惯的情况下才值得(也就是说,您倾向于自然编写代码的方式会变得越来越快而不是越慢)。否则,他们甚至都不是过早的乐观,而是过早的悲观,只会使事情变得更糟。


0

括号告诉编译器应该按哪个顺序对表达式求值。有时它们是无用的(除非它们提高或降低了可读性),因为它们指定了无论如何都会使用的顺序。有时他们改变顺序。在

int a = 1 + 2 + 3;

实际上,每种存在的语言都有一个规则,即求和是通过将1 + 2加到结果加3来求和的。

int a = 1 + (2 + 3);

那么括号将强制采用不同的顺序:首先加2 + 3,然后加1加结果。您的括号示例所产生的顺序与本来应该产生的顺序相同。现在,在此示例中,操作顺序略有不同,但是整数加法的工作方式相同。在

int a = 10 - (5 - 4);

括号很关键;忽略它们会将结果从9更改为1。

编译器确定按哪个顺序执行了哪些操作后,括号就完全被遗忘了。编译器在这一点上所记住的只是按照哪个顺序执行哪些操作。因此,实际上编译器在这里没有什么可以优化的,括号消失了


practically every language in existence; APL除外:尝试(here)[tryapl.org]输入(1-2)+3(2),1-(2+3)(-4)和1-2+3(也是-4)。
表示

0

我同意上面所说的许多内容,但是……这里的主要问题是括号可以强制操作顺序……编译器绝对会这样做。是的,它产生机器代码……但是,这不是重点,也不是要问的内容。

括号确实消失了:正如已经说过的那样,它们不是机器码的一部分,机器码是数字,而不是其他东西。汇编代码不是机器代码,它是半人类可读的,并且按名称包含指令-而不是操作码。机器运行所谓的操作码-汇编语言的数字表示。

Java之类的语言属于中间语言,因为它们仅在产生它们的机器上部分编译。它们被编译为在运行它们的机器上的机器特定代码,但这对这个问题没有影响-括号在第一次编译后仍然消失了。


1
我不确定这是否能回答问题。辅助段落比帮助更令人困惑。Java编译器与C ++编译器有何关系?
亚当·祖克曼

OP询问括号是否消失了……我说过了,并进一步解释了可执行代码仅仅是代表操作码的数字。Java是另一个答案。我认为它可以很好地回答问题……但这只是我的观点。感谢回复。
Jinzai 2014年

3
括号不强制“操作顺序”。它们更改优先级。所以,在a = f() + (g() + h());中,编译器是免费的来电fg以及h按照这个顺序(或以任何顺序它为所欲为)。
阿洛克

我对该声明有不同意见……您绝对可以使用括号来强制操作顺序。
jinzai 2014年

0

编译器,无论使用哪种语言,都将所有中缀数学转换为后缀。换句话说,当编译器看到如下内容时:

((a+b)+c)

它将其转换为:

 a b + c +

这样做是因为虽然人们更容易阅读infix表示法,但postfix表示法更接近于计算机完成工作所必须采取的实际步骤(并且因为已经有完善的算法可以解决此问题。)定义,后缀消除了操作顺序或括号的所有问题,这自然使实际编写机器代码时变得容易得多。

我推荐有关反向波兰记法的维基百科文章,以获取有关该主题的更多信息。


5
关于编译器如何转换操作,这是一个错误的假设。例如,您在这里假设一个堆栈计算机。如果您有向量处理器怎么办?如果您有一台拥有大量寄存器的机器怎么办?
艾哈迈德·马苏德
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.