为什么编译器如此愚蠢?

我总是想知道为什么编译器无法弄清楚人眼所见的简单的事情。 他们做了很多简单的优化,但从来没有一点点复杂。 例如,这段代码在我的电脑上需要大约6秒的时间来打印零值(使用java 1.6):

int x = 0; for (int i = 0; i < 100 * 1000 * 1000 * 1000; ++i) { x += x + x + x + x + x; } System.out.println(x); 

这是完全明显的,x从来没有改变,所以不pipe你多久添加0自己它保持为零。 所以编译器理论上可以用System.out.println(0)来代替它。

甚至更好,这需要23秒:

 public int slow() { String s = "x"; for (int i = 0; i < 100000; ++i) { s += "x"; } return 10; } 

首先,编译器可能会注意到我实际上创build了一个100000“x”的string,所以它可以自动使用s StringBuilder,或者更好地直接用结果stringreplace它,因为它总是相同的。 其次,它不承认我根本没有使用string,所以整个循环都可以被丢弃!

为什么在这么多人手进入快速编译器之后,他们还是那么笨?

编辑 :当然这些都是愚蠢的例子,不应该在任何地方使用。 但是,每当我必须重写一个漂亮的,可读的代码到不可读的东西,以便编译器很快乐,并产生快速的代码,我不知道为什么编译器或其他自动化工具不能为我做这个工作。

哦,我不知道。 有时编译器很聪明。 考虑下面的C程序:

 #include <stdio.h> /* printf() */ int factorial(int n) { return n == 0 ? 1 : n * factorial(n - 1); } int main() { int n = 10; printf("factorial(%d) = %d\n", n, factorial(n)); return 0; } 

在我的GCC版本上(4.3.2在Debiantesting中),当编译时没有优化,或-O1时,它会像您所期望的那样为factorial()生成代码,并使用recursion调用来计算值。 但在-O2上,它做了一些有趣的事情:它编译成一个紧密的循环:

  factorial: .LFB13: testl %edi, %edi movl $1, %eax je .L3 .p2align 4,,10 .p2align 3 .L4: imull %edi, %eax subl $1, %edi jne .L4 .L3: rep ret 

令人印象深刻 recursion调用(甚至不是尾recursion)已经被完全消除,所以factorial现在使用O(1)堆栈空​​间而不是O(N)。 尽pipe我对x86汇编只有非常肤浅的了解(在这种情况下实际上是AMD64,但是我认为上面没有使用任何AMD64扩展),但我怀疑你可以手动编写更好的版本。 但是真正引起我注意的是它在-O3上生成的代码。 阶乘的实施保持不变。 但是main()改变了:

  main: .LFB14: subq $8, %rsp .LCFI0: movl $3628800, %edx movl $10, %esi movl $.LC0, %edi xorl %eax, %eax call printf xorl %eax, %eax addq $8, %rsp ret 

请参阅movl $3628800, %edx行? gcc在编译时是预计算阶乘(10)。 它甚至不称之为阶乘()。 难以置信。 我的帽子是GCC开发团队的。

当然,所有通常的免责声明都适用,这只是一个玩具的例子,不成熟的优化是万恶之源等等,但是这说明编译器通常比你想象的更聪明。 如果你认为自己可以做得更好,那么你几乎肯定是错的。

(改编自我博客上的一篇文章 。)

在我看来,我不认为编译器的工作就是修复那些实际上是错误的编码。 你已经非常明确地告诉编译器你想要执行第一个循环。 这是一样的:

 x = 0 sleep 6 // Let's assume this is defined somewhere. print x 

我不希望编译器删除我的sleep声明只是因为它什么都没做。 你可能会争辩说,睡眠声明是一个明确的延迟请求,而你的例子不是。 但是,那么你将允许编译器做出关于你的代码应该做什么的非常高层次的决定,我相信这是一件坏事。

代码和处理它的编译器是工具,如果你想有效地使用它们,你需要成为一个工具匠。 有多less12个电锯会拒绝砍掉一棵30英寸的树? 如果探测到混凝土墙,会有多less钻机自动切换到锤击模式?

没有,我怀疑,这是因为devise这个产品的成本将是一个开始可怕的。 但是,更重要的是,如果你不知道自己在做什么,你就不应该使用钻头或者电锯。 例如:如果你不知道什么是回扣(一个新手脱手的简单方法),那么直到你离开链锯。

我全都希望编译器能够提出改进build议,但我宁愿自己保持控制权。 编译器不应该由单方面决定一个循环是不必要的。

例如,我已经在embedded式系统中完成了时钟循环,其中CPU的时钟速度准确无误,但没有可靠的定时器。 在这种情况下,您可以准确计算给定循环将花多长时间,并使用该循环来控制事件发生的频率。 如果编译器(或者汇编程序)决定我的循环是无用的,并且优化它不存在,那么这将不起作用。

话虽如此,让我留下一个VAX FORTRAN编译器的老故事,这个编译器正在经历性能的基准testing,发现它比最接近的竞争对手快了许多个数量级。

事实certificate,编译器注意到,基准循环的结果并没有在其他地方使用,并且将循环优化为遗忘。

从C / C ++的观点来看:

你的第一个例子将被大多数编译器优化。 如果Sun公司的java编译器真的执行了这个循环,那么这是编译器的错误,但是要承认,1990年以后的任何C,C ++或Fortran编译器都完全消除了这种循环。

你的第二个例子不能在大多数语言中进行优化,因为内存分配是串联在一起的副作用。 如果编译器会优化代码,内存分配模式将会改变,这可能会导致程序员试图避免的影响。 内存碎片和相关问题是embedded式程序员每天仍然面临的问题。

总的来说,我对编译器最近可以做的优化感到满意。

编译器被devise为可预测的 。 这可能会使他们不时看起来很愚蠢,但没关系。 编译器作者的目标是

  • 你应该能够看看你的代码,并对其性能做出合理的预测。

  • 代码中的小改动不会导致性能上的巨大差异。

  • 如果一个小小的改变让程序员看起来应该会提高性能,至less不应该降低性能(除非硬件中出现了令人吃惊的事情)。

所有这些标准都不利于仅适用于angular落案例的“魔术”优化。


你的两个例子都有一个循环更新variables,但在其他地方没有使用 。 这种情况实际上是相当难以拿起,除非你使用某种框架,可以结合死代码消除与其他优化如副本传播或常量传播。 对于一个简单的数据stream优化器来说,这个variables看起来不会死的。 要理解为什么这个问题很难,请参阅POPL 2002中的Lerner,Grove和Chambers的论文 ,它使用了这个例子,并解释了为什么这很难。

HotSpot JIT编译器将只优化已经运行一段时间的代码。 当你的代码很热的时候,循环已经启动了,JIT编译器必须等到下一次进入方法来寻找优化循环的方法。 如果您多次调用该方法,您可能会看到更好的性能。

HotSpot常见问题解答中包含了这个问题:“我写了一个简单的循环来定时进行一个简单的操作,而且速度很慢,我做错了什么?”

真的吗? 为什么有人会写这样的真实世界的代码? 恕我直言,代码,而不是编译器是这里的“愚蠢的”实体。 我非常高兴编译器作者不用浪费时间来优化这样的东西。

编辑/澄清:我知道这个问题中的代码只是一个例子,但是这只是certificate了我的观点:你要么必须在努力,要么就是毫无办法地编写这样的无效代码。 握住我们的手不是编译器的工作,所以我们不写可怕的代码。 作为编写代码的人,我们有责任充分了解我们的工具,以便高效,清晰地编写代码。

那么,我只能说C ++,因为我完全是一个Java初学者。 在C ++中,编译器可以自由地忽略标准所要求的任何语言要求,只要可观察到的行为是– 如果编译器实际上模拟了标准放置的所有规则。 可观察行为被定义为对易失性数据和对库函数的调用的任何读取和写入 。 考虑一下:

 extern int x; // defined elsewhere for (int i = 0; i < 100 * 1000 * 1000 * 1000; ++i) { x += x + x + x + x + x; } return x; 

C ++编译器可以优化出这段代码,只需将一个由该循环产生的适当的值添加到x中,因为代码的行为如果循环从未发生,并且不涉及易失性数据或库函数这可能会导致所需的副作用。 现在考虑一下volatilevariables:

 extern volatile int x; // defined elsewhere for (int i = 0; i < 100 * 1000 * 1000 * 1000; ++i) { x += x + x + x + x + x; } return x; 

编译器不允许再做相同的优化,因为它不能certificate写入x引起的副作用不会影响程序的可观察行为。 毕竟,x可以被设置为一个硬件设备监视的存储单元,每次写入都会触发。


说到Java,我已经testing了你的循环,而且恰巧GNU Java Compiler( gcj )花费了很多时间来完成你的循环(它根本没有完成,我杀了它)。 我启用优化标志(-O2),它发生了立即打印出0

 [js@HOST2 java]$ gcj --main=Optimize -O2 Optimize.java [js@HOST2 java]$ ./a.out 0 [js@HOST2 java]$ 

也许这个观察可能会对这个线程有所帮助? 为什么gcj会这么快? 那么,一个原因肯定是gcj编译成机器代码,所以不可能根据代码的运行时行为来优化代码。 它将所有的强度结合在一起,并尽可能在编译时尽可能优化。 但是,虚拟机可以编译Just in Time代码,因为java的这个输出显示了这个代码:

 class Optimize { private static int doIt() { int x = 0; for (int i = 0; i < 100 * 1000 * 1000 * 1000; ++i) { x += x + x + x + x + x; } return x; } public static void main(String[] args) { for(int i=0;i<5;i++) { doIt(); } } } 

java -XX:+PrintCompilation Optimize输出java -XX:+PrintCompilation Optimize

 1 java.lang.String::hashCode (60 bytes) 1% Optimize::doIt @ 4 (30 bytes) 2 Optimize::doIt (30 bytes) 

正如我们看到的,JIT编译doIt函数2次。 根据第一次执行的观察,再次编译。 但它恰好与字节码有两次相同的大小,这表明循环仍在原地。

正如另一位程序员所表明的,对于某些情况下,对于后续编译的代码甚至会增加某些死循环的执行时间。 他报告了一个可以在这里阅读的错误,截至2008年10月24日。

在你的第一个例子中,这是一个只有当值为零时才起作用的优化。 在编译器中额外的if语句需要查找这个很less见的子句可能不值得(因为它将不得不在每个variables上检查这个)。 而且,这又如何呢?

 int x = 1; int y = 1; int z = x - y; for (int i = 0; i < 100 * 1000 * 1000 * 1000; ++i) { z += z + z + z + z + z; } System.out.println(z); 

这仍然是相同的事情,但现在有一个额外的情况,我们必须编译在编译器。 只有无数的方法可以最终归零,不值得为之编码,我想你可以说,如果你将要拥有其中的一个,那么你最好还是把它们全部归为一类。

一些优化确实照顾了你发布的第二个例子,但是我想我已经在函数式语言中看到了更多,而不是Java。 使新语言变得困难的一件大事是修补猴子 。 现在+=可以有一个副作用 ,这意味着如果我们优化它,它可能是错误的(例如添加function+=打印出当前值将意味着一个不同的程序)。

但是这又一次归结为同样的事情:有太多的情况下,你必须寻找,以确保没有副作用正在执行,可能会改变最终的程序的状态。

多花些时间,确保你写的是你真正想要的电脑。 🙂

一般编译器都非常聪明。

你必须考虑的是,他们必须考虑到每个可能的例外情况,或者优化代码或重新分解代码可能会导致不必要的副作用。

诸如线程化程序,指针别名,dynamic链接代码和副作用(系统调用/内存分配)等等都使得正式的重构非常困难。

尽pipe你的例子很简单,但仍然可能有困难的情况下考虑。

至于你的StringBuilder参数,这不是一个编译器作业来select使用哪些数据结构。

如果你想要更强大的优化转移到一个更强大的types的语言,如fortran或haskell,编译器被提供更多的信息来处理。

大多数教授编译器/优化(甚至是acedemically)的课程都让人感觉如何让正式的prooven optimisatons而不是黑客攻击特定的案例是一个非常困难的问题。

我认为你低估了多less工作,以确保一段代码不会影响另一段代码。 只要对您的示例x​​,i和s稍作修改就可以指向相同的内存。 一旦其中一个variables是一个指针,就很难告诉哪些代码可能有副作用,取决于指向什么。

另外,我认为编程人员宁愿花时间进行优化,这对人类来说也不是那么容易。

因为我们还没到 你可以简单地问:“为什么我仍然需要编写程序…为什么我不能提供需求文档,让计算机为我编写应用程序?”

编译器编写者将时间花在小事情上,因为这些是应用程序员往往错过的东西的types。

此外,他们不能假设太多(也许你的循环是某种贫民窟时间延迟什么的)?

这是编译器编写者和程序员之间永恒的军备竞赛。

非人为的例子工作得很好 – 大多数编译器的确可以优化掉明显无用的代码。

构build的检查总是会让编译器停滞不前。 certificate,如果需要的话,任何程序员都比任何程序都更聪明。

在将来,你需要比你在这里发布的更多人为的例子。

正如其他人已经充分解决了你的问题的第一部分,我会尝试解决第二部分,即“自动使用StringBuilder”。

有几个很好的理由不去做你的build议,但实践中最大的原因可能是优化器在实际源代码被消化和遗忘之后运行很久。 优化器通常在生成的字节码(或程序集,三个地址码,机器码等)上或在parsing代码所产生的抽象语法树上运行。 优化器通常对运行时库(或任何库)一无所知,而是在指令级(即低级控制stream和寄存器分配)运行。

其次,随着图书馆进化(尤其是在Java中)比语言快得多,跟上它们,知道什么是什么,以及什么样的其他库组件可能更适合这项任务,将是一项艰巨的任务。 也可能是不可能的,因为这个提议的优化器将不得不精确理解你的意图和每个可用库组件的意图,并以某种方式find它们之间的映射。

最后,正如其他人所说(我认为),编译器/优化器编写者可以合理地认为编写input代码的程序员不是脑死亡。 当其他更普遍的优化比比皆是时,花费大量的精力来处理像这样的特殊情况是浪费时间的。 此外,正如其他人也提到的那样,看起来大脑死亡的代码可以有一个实际的目的(自旋locking,在系统级产量之前忙着等待),编译器必须尊重程序员要求的东西(如果它在语法和语义上是有效的)。

你编译来释放代码吗? 我认为一个好的编译器会在你的第二个例子中检测到这个string从来没有被使用过。

实际上,Java应该在第二个例子中使用string生成器。

试图优化这些例子的基本问题是这样做需要定理certificate 。 这意味着编译器需要构build一个代码将实际执行的mathcertificate。 这完全不是一件小事。 事实上,能够certificate所有的代码确实有效果相当于暂停问题。

当然,你可以拿出微不足道的例子,但是一些微不足道的例子是无限的。 你总是可以想到其他的东西,所以没有办法把它们都抓住。

当然, 有些代码可以被certificate是没有任何作用的,就像你的例子。 你想要做的是编译器优化每一个可以certificate在P时间未使用的问题。

但无论如何,这是一个很大的工作,并没有得到你所有的。 人们花费大量的时间试图找出方法来防止程序出现错误,像Java和Scala这样的types系统试图防止错误,但现在没有人使用types系统来声明执行时间, 我所知道的。

你可能想看看哈斯克尔,我认为它有最先进的理论certificate,尽pipe我不确定。 我自己并不知道。

大多数情况下,你所抱怨的是“为什么Java编译器如此愚蠢”,因为大多数其他语言编译器更聪明。

Java编译器愚蠢的原因是历史的。 首先,原始的Java实现是基于解释器的,性能是不重要的。 其次,许多原始的java基准testing都是有问题的。 我记得有一个基准看起来很像你的第二个例子。 不幸的是,如果编译器优化了循环,那么当基准数字除以经过的时间来计算其性能得分时,基准会得到除以零的exception。 所以编写一个优化的java编译器时,必须非常小心,不要优化某些东西,因为人们会声称你的编译器坏了。

在编译成JVM字节码时,优化这样的东西几乎被认为是不好的做法。 Sun的javac确实有一些基本的优化,就像scalacscalac等一样。总之,任何真正特定于语言的东西都可以在编译器中得到优化。 然而,这样的显然是如此人为的语言不可知论的事情将通过简单的政策。

这样做的原因是它允许HotSpot对字节码及其模式有更加一致的视图。 如果编译器开始讨论边缘情况,那么会降低VM优化一般情况的能力,这在编译时可能不明显。 Steve Yeggie喜欢这样做:通过一个聪明的虚拟机在运行时执行优化通常更容易 。 他甚至声称HotSpot去掉了javac的优化。 虽然我不知道这是否属实,但我不会感到惊讶。

总而言之:针对虚拟机的编译器有一套完全不同的标准,特别是在优化领域和合适的时候。 不要指责编译器编写者将工作交给function更强大的JVM。 正如在这个线程上多次指出的那样,针对本地体系结构(比如gcc系列)的现代编译器是非常聪明的,通过一些非常聪明的优化来产生猥琐的快速代码。

我从来没有看到首先在死码清除的重点。 为什么程序员写这个? 如果你要去做一些关于死代码的东西,那就声明它是一个编译器错误! 这几乎肯定意味着程序员犯了一个错误,而在less数情况下,编译器指令使用variables将是正确的答案。 If I put dead code in a routine I want it executed–I'm probably planning to inspect the results in the debugger.

The case where the compiler could do some good is pulling out loop invariants. Sometimes clarity says to code the calculation in the loop and having the compiler pull such things out would be good.

Compilers that can do strict-aliasing optimizations, will optimize first example out. 看到这里 。

Second example can't be optimized because the slowest part here is memory allocation/reallocation and operator+= is redefined into a function that does the memory stuff. Different implementations of strings use different allocation strategies.

I myself also would rather like to have malloc(100000) than thousand malloc(100) too when doing s += "s"; but right now that thing is out of scope of compilers and has to be optimized by people. This is what D language tries to solve by introducing pure functions .

As mentioned here in other answers, perl does second example in less than a second because it allocates more memory than requested just in case more memory will be needed later.

In release mode VS 2010 C++ this doesnt take any time to run. However debug mode is another story.

 #include <stdio.h> int main() { int x = 0; for (int i = 0; i < 100 * 1000 * 1000 * 1000; ++i) { x += x + x + x + x + x; } printf("%d", x); } 

Absolute optimization is an undecidable problem, that means, there is no Turing machine (and, therefore, no computer program) that can yield the optimal version of ANY given program.

Some simple optimizations can be (and, in fact, are) done, but, in the examples you gave…

  1. To detect that your first program always prints zero, the compiler would have to detect that x remains constant despite all the loop iterations. How can you explain (I know, it's not the best word, but I can't come up with another) that to a compiler?

  2. How can the compiler know that the StringBuilder is the right tool for the job without ANY reference to it?

In a real-world application, if efficiency is critical in a part of your application, it must be written in a low-level language like C. (Haha, seriously, I wrote this?)

This is an example of procedural code v. functional code.

You have detailed a procedure for the compiler to follow, so the optimisations are going to be based around the procedure detailed and will minimise any side effects or not optimise where it will not be doing what you expect. This makes it easier to debug.

If you put in a functional description of what you want eg. SQL then you are giving the compiler a wide range of options to optimise.

Perhaps some type of code analysis would be able to find this type of issue or profiling at run-time, but then you will want to change the source to something more sensible.

Because compiler writers try add optimizations for things that matter (I hope) and that are measured in *Stone benchmarks (I fear).

There are zillions of other possible code fragments like yours, which do nothing and could be optimized with increasing effort on the compiler writer, but which are hardly ever encountered.

What I feel embarrassing is that even today most compilers generate code to check for the switchValue being greater than 255 for a dense or almost full switch on an unsigned character. That adds 2 instructions to most bytecode interpreter's inner loop.

I hate to bring this up on such an old question (how did I get here, anyway?), but I think part of this might be something of a holdout from the days of the Commodore 64.

In the early 1980s, everything ran on a fixed clock. There was no Turbo Boosting and code was always created for a specific system with a specific processor and specific memory, etc. In Commodore BASIC, the standard method for implementing delay s looked a lot like:

 10 FOR X = 1 TO 1000 20 NEXT : REM 1-SECOND DELAY 

(Actually, in practice, it more closely resembled 10FORX=1TO1000:NEXT , but you know what I mean.)

If they were to optimize this, it would break everything—nothing would ever be timed. I don't know of any examples, but I'm sure there are lots of little things like this scattered through the history of compiled languages that prevented things from being optimized.

Admittedly, these non-optimizations aren't necessary today. There's probably, however, some unspoken rule among compiler developers not to optimize things like this. I wouldn't know.

Just be glad that your code is optimized somewhat, unlike code on the C64. Displaying a bitmap on the C64 could take up to 60 seconds with the most efficient BASIC loops; thus, most games, etc. were written in machine language. Writing games in machine language isn't fun.

只是我的想法。

Premise: I studied compilers at university.

The javac compiler is extremely stupid and performs absolutely no optimization because it relies on the java runtime to do them. The runtime will catch that thing and optimize it, but it will catch it only after the function is executed a few thousand times.

If you use a better compiler (like gcc) enabling optimizations, it will optimize your code, because it's quite an obvious optimization to do.

Compilers are as smart as we make them. I don't know too many programmers who would bother writing a compiler that would check for constructs such as the ones you used. Most concentrate on more typical ways to improve performance.

It is possible that someday we will have software, including compilers, that can actually learn and grow. When that day comes most, maybe all, programmers will be out of job.

The meaning of your two examples is pointless, useless and only made to fool the compiler.

The compiler is not capable (and should not be) to see the meaning of a method, a loop or a program. That is where you get into the picture. You create a method for a certain functionality/meaning, no matter how stupid it is. It's the same case for simple problems or extreme complex programs.

In your case the compiler might optimize it, because it "thinks" it should be optimized in another way but why stay there?

Extreme other situation. We have a smart compiler compiling Windows. Tons of code to compile. But if it's smart, it boils it down to 3 lines of code…

 "starting windows" "enjoy freecell/solitaire" "shutting down windows" 

The rest of the code is obsolete, because it's never used, touched, accessed. Do we really want that?

It forces you (the programmer) to think about what you're writing. Forcing compilers to do your work for you doesn't help anyone: it makes the compilers much more complex (and slower!), and it makes you stupider and less attentive to your code.

Interesting Posts