请问代码
int a = ((1 + 2) + 3); // Easy to read
运行慢于
int a = 1 + 2 + 3; // (Barely) Not quite so easy to read
还是现代的编译器足够聪明,可以删除/优化“无用的”括号。
看起来似乎很少有优化方面的问题,但是选择C ++而不是C#/ Java / ...都是关于优化(IMHO)的。
请问代码
int a = ((1 + 2) + 3); // Easy to read
运行慢于
int a = 1 + 2 + 3; // (Barely) Not quite so easy to read
还是现代的编译器足够聪明,可以删除/优化“无用的”括号。
看起来似乎很少有优化方面的问题,但是选择C ++而不是C#/ Java / ...都是关于优化(IMHO)的。
Answers:
编译器实际上从未插入或删除括号;它只是创建一个与您的表达式相对应的解析树(其中没有括号),并且这样做必须遵守您编写的括号。如果您完全用括号括起来,那么对人类读者来说,解析树将是什么也将立即清晰可见。如果您像这样极端地放入多余的括号,int a = (((0)));
则会对阅读器的神经元造成不必要的压力,同时还会浪费解析器中的一些周期,而不会更改结果的解析树(因此不会更改生成的代码) )一点点。
如果您不写任何括号,那么解析器仍然必须完成创建解析树的工作,并且运算符优先级和关联性的规则会准确地告诉它必须构造的解析树。您可能会认为这些规则是告诉编译器应该在代码中插入哪个(隐式)括号,尽管在这种情况下解析器实际上从未处理过括号:它只是构造为产生与括号相同的解析树出现在某些地方。如果将括号恰好放在这些位置(如的int a = (1+2)+3;
关联性+
在左侧),则解析器将通过稍有不同的路径获得相同的结果。如果您将括号放在int a = 1+(2+3);
那么您将强制使用不同的解析树,这可能会导致生成不同的代码(尽管可能不会,因为编译器可能会在构建解析树后应用转换,只要执行生成的代码的效果永远不会不同它)。假设重新启动的代码有所不同,那么一般来讲,哪一个效率更高?当然,最重要的一点是,在大多数情况下,解析树不会给出数学上相等的表达式,因此比较它们的执行速度就不重要了:应该只写一个给出正确结果的表达式。
因此,结果是:根据正确性和可读性的需要使用括号;如果是冗余的,它们对执行速度完全没有影响(对编译时间的影响可以忽略不计)。
而且,这与优化无关,优化是在解析树构建之后进行的,因此它不知道解析树是如何构造的。从最老,最笨的编译器到最聪明,最现代的编译器,都无需更改。只有使用解释性语言(“编译时间”和“执行时间”一致)时,多余的括号才有可能受到惩罚,但是即使如此,我仍然认为大多数此类语言都是经过组织的,因此至少解析阶段仅进行一次对于每个语句(将其存储为预先准备好的形式以供执行)。
a = b + c * d;
,a = b + (c * d);
将是[无害的]多余的括号。如果它们可以帮助您使代码更具可读性,那就好。a = (b + c) * d;
将会是非冗余的括号-它们实际上会更改结果分析树并给出不同的结果。这样做完全合法(实际上是必要的),但是它们与隐含的默认分组不同。
括号仅是为了您的利益,而不是编译器。编译器将创建正确的机器代码来表示您的语句。
仅供参考,编译器足够聪明,可以对其进行完全优化。在您的示例中,这将int a = 6;
在编译时变成。
int a = 8;// = 2*3 + 5
int five = 7; //HR made us change this to six...
您实际提出的问题的答案是“否”,但是您要提出的问题的答案是“是”。添加括号不会降低代码速度。
您问了一个关于优化的问题,但是括号与优化无关。编译器应用了各种优化技术,旨在提高所生成代码的大小或速度(有时两者)。例如,它可以采用表达式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)时,将发生无符号行为。这是一个复杂的主题,超出了此答案的范围。
a
可以真正地进行无符号签名-a + b
,那么如果a
是负数也可以是b
正数,则导致计算也很容易溢出。
(b+c)
最后一行中的表达式会将其参数提升为int
,因此,除非编译器将其定义int
为16位(要么因为它是古老的,要么针对一个小型微控制器),否则最后一行将是完全合法的。
int
被提升为的内容,int
除非该类型无法表示其所有值,否则将被提升为unsigned int
。如果所有定义的行为都与包含促销一样,则编译器可以省略促销。在16位类型表现为环绕抽象代数环的机器上,(a + b)+ c和a +(b + c)是等效的。但是,如果int
是陷入溢出的16位类型,那么在某些情况下会出现以下表达式之一……
括号仅用于您操作运算符优先级的顺序。编译后,括号不再存在,因为运行时不需要它们。编译过程会删除您和我需要的所有方括号,空格和其他语法糖,并将所有运算符更改为使计算机执行起来更简单的操作。
因此,您和我可能会在哪里看到...
...编译器可能会发出类似以下的内容:
通过从头开始并依次执行每个指令来执行程序。
运营商的优先级现在为“先到先得”。
一切都是强类型的,因为编译器可以全部工作是出,而这是撕开原来的语法。
好的,这与您和我所处理的东西完全不同,但是然后我们就没有运行它!
取决于它是否是浮点数:
在浮点运算中,加法不是关联的,因此优化器无法对操作进行重新排序(除非您添加fastmath编译器开关)。
在整数运算中,它们可以重新排序。
在您的示例中,两者将在完全相同的时间运行,因为它们将编译为完全相同的代码(加法运算从左到右)。
但是即使Java和C#也可以对其进行优化,但它们只会在运行时进行。
不,但是是的,但是也许,但是也许相反,但是没有。
正如人们已经指出的那样(假设加法是左相关的语言,例如C,C ++,C#或Java),该表达式((1 + 2) + 3)
完全等同于1 + 2 + 3
。它们是用不同的方式在源代码中编写内容的方式,这对最终的机器代码或字节代码产生零影响。
无论哪种方式,结果都将是一条指令,例如,添加两个寄存器,然后添加第三个,或者从堆栈中获取两个值,将其添加,推回,然后将其与另一个相加,或者在其中添加三个寄存器。一次操作,或以其他方式求和三个数字的方法,具体取决于下一级最有意义的内容(机器码或字节码)。在字节码的情况下,这反过来可能会经历类似的重组,例如,与之等效的IL(这将是一系列加载到堆栈,并弹出对以进行添加,然后推回结果)不会在机器代码级别上直接获得该逻辑的副本,但是对于所讨论的机器来说更明智。
但是您的问题还有更多。
对于任何理智的C,C ++,Java或C#编译器,我希望给出的两个语句的结果与以下内容完全相同:
int a = 6;
为什么结果代码会浪费时间对文字进行数学运算?对程序状态的任何更改都不会停止1 + 2 + 3
being 的结果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) / 2
。d * 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 ++编写的平均程序会一个较小的可执行文件,与这两种语言中的任何一种相比,它的工作速度更快,使用的内存更少。
这不是最优化。
优化有时被用来表示“使事情变得更快”。这是可以理解的,因为通常当我们真正在谈论“优化”时,我们实际上是在谈论使事情进展得更快,所以一个已经成为另一个的简写,我承认我自己这样滥用这个词。
“使事情前进得更快”的正确说法不是优化。正确的词是改善。如果您对程序进行更改,唯一有意义的区别就是它现在更快,没有任何优化,那就更好了。
优化是指我们在特定方面和/或特定情况方面进行改进时。常见的例子有:
这种情况是合理的,例如:
但是,在其他情况下,这种情况也不会被证明是正确的。优化。
语言的选择确实会产生影响,因为速度,内存使用和可读性都会受其影响,但是与其他系统的兼容性,库的可用性,运行时的可用性以及给定操作系统上这些运行时的成熟度也会受到影响。 (由于我的过失,我最终以某种方式最终选择了Linux和Android作为我最喜欢的操作系统,并以C#作为我最喜欢的语言,虽然Mono很棒,但是我还是遇到了很多麻烦)。
如果您认为C ++确实很糟糕,那么说“在C#/ Java / ...上选择C ++完全是关于优化”才有意义,因为优化是“尽管...更好”而不是“更好”。如果您认为C ++尽管本身就更好,那么您需要做的最后一件事就是担心这种微小的微选择。确实,最好完全放弃它。快乐的黑客也是一种需要优化的素质!
但是,如果您倾向于说“我爱C ++,而我所钟爱的事情之一就是挤出额外的循环”,那么那就不一样了。仍然有一种情况,微型选择只有在可以养成反身习惯的情况下才值得(也就是说,您倾向于自然编写代码的方式会变得越来越快而不是越慢)。否则,他们甚至都不是过早的乐观,而是过早的悲观,只会使事情变得更糟。
括号告诉编译器应该按哪个顺序对表达式求值。有时它们是无用的(除非它们提高或降低了可读性),因为它们指定了无论如何都会使用的顺序。有时他们改变顺序。在
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)。
我同意上面所说的许多内容,但是……这里的主要问题是括号可以强制操作顺序……编译器绝对会这样做。是的,它产生机器代码……但是,这不是重点,也不是要问的内容。
括号确实消失了:正如已经说过的那样,它们不是机器码的一部分,机器码是数字,而不是其他东西。汇编代码不是机器代码,它是半人类可读的,并且按名称包含指令-而不是操作码。机器运行所谓的操作码-汇编语言的数字表示。
Java之类的语言属于中间语言,因为它们仅在产生它们的机器上部分编译。它们被编译为在运行它们的机器上的机器特定代码,但这对这个问题没有影响-括号在第一次编译后仍然消失了。
a = f() + (g() + h());
中,编译器是免费的来电f
,g
以及h
按照这个顺序(或以任何顺序它为所欲为)。
编译器,无论使用哪种语言,都将所有中缀数学转换为后缀。换句话说,当编译器看到如下内容时:
((a+b)+c)
它将其转换为:
a b + c +
这样做是因为虽然人们更容易阅读infix表示法,但postfix表示法更接近于计算机完成工作所必须采取的实际步骤(并且因为已经有完善的算法可以解决此问题。)定义,后缀消除了操作顺序或括号的所有问题,这自然使实际编写机器代码时变得容易得多。
我推荐有关反向波兰记法的维基百科文章,以获取有关该主题的更多信息。