问题

为什么下面程序递归计算斐波那契数列java比c++要快?

回答
你提出的问题非常有意思,也很具有挑战性。实际上,通常情况下,在相同的硬件和编译优化级别下,递归计算斐波那契数列的 Java 程序并不会比 C++ 程序更快,反而很可能要慢一些。

之所以你可能会看到或认为 Java 比 C++ 快,可能存在以下几种情况:

1. 测试环境或测试方法的问题:
编译器优化差异: C++ 编译器(如 GCC, Clang)通常提供非常强大的优化选项(如 `O2`, `O3`, `Ofast`)。如果你的 C++ 代码没有启用这些优化,或者启用了较低级别的优化,而 Java 虚拟机(JVM)的 JIT(JustInTime)编译器在特定情况下进行了高效的优化,就可能出现 Java 更快的假象。
JIT 编译的“预热”效应: Java 程序在运行一段时间后,JIT 编译器会分析热点代码并进行即时编译,生成高度优化的机器码。如果你的测试时间很短,可能没有给 JVM 足够的“预热”时间来达到最佳性能。而 C++ 代码是一次性编译成机器码的,没有这个预热过程。
测试粒度太小: 递归计算斐波那契数列本身就是一种效率极低的算法,存在大量的重复计算。如果测试的斐波那契数非常小(例如 F(10)),计算量本身就非常小,任何微小的系统开销(如函数调用、栈帧管理)都可能成为测量噪声。
不同的 JVM 版本或配置: 不同版本的 JVM 可能有不同的 JIT 优化策略。
操作系统或硬件调度: 线程调度、缓存一致性等也可能影响短期测试结果。

2. 对递归斐波那契数列的理解有误:
原生递归的低效性: 无论是 Java 还是 C++,纯粹的递归实现斐波那契数列都是非常低效的(指数级时间复杂度 O(2^n)),因为它会重复计算大量的子问题。例如,计算 F(5) 需要计算 F(4) 和 F(3),而计算 F(4) 又需要计算 F(3) 和 F(2),F(3) 被计算了两次。
实际中更快的实现方式: 在实际开发中,很少会直接使用纯递归来计算斐波那契数列。通常会使用 记忆化递归(Memoization) 或 迭代(Iteration) 的方式来优化,将时间复杂度降低到 O(n)。如果你测试的是优化后的版本,那么比较的基础就不同了。

3. 感官或主观判断:
在非常小的测试范围内,结果可能不具有统计学意义。

让我们假设你确实在某些特定测试环境下观察到了 Java 比 C++ 更快,并尝试从理论和实现层面分析可能的原因,即使这并非普遍情况:

潜在但不太可能导致 Java 比 C++ 更快的理论原因分析 (通常反之):

JVM 字节码与 JIT 优化:
JIT 的激进优化能力: modern 的 JVM 拥有非常强大的 JIT 编译器,例如 HotSpot VM 的 C2 编译器。在某些情况下,JIT 可能会比编译器的某些默认优化(如果 C++ 编译器未启用高级优化)做得更好。例如,JIT 可能会更积极地进行方法内联(inlining)、逃逸分析(escape analysis)以及死代码消除(dead code elimination)。
JVM 的内存管理与垃圾回收(GC): 尽管听起来与速度无关,但 JVM 的 GC 在某些情况下可能会比手动内存管理(在 C++ 中)减少一些显式的内存分配和释放的开销。然而,GC 本身也可能引入暂停。对于递归这种容易导致栈溢出的场景,如果 JVM 的栈管理更“灵活”,或者 C++ 的栈帧管理开销更大,理论上可能存在微弱差异。但对于斐波那契这种计算密集型且不涉及大量对象创建的场景,GC 的影响通常不大。

函数调用开销:
Java 的方法调用: Java 的方法调用通常涉及一些验证和查找过程(尽管 JIT 会优化很多)。
C++ 的函数调用: C++ 的函数调用在编译时已经确定,并且可以被编译器更积极地进行内联优化,这通常会减少函数调用的实际开销。因此,理论上 C++ 的函数调用开销应该更小。
堆栈帧(Stack Frame)管理: 每次函数调用都需要在堆栈上分配一个栈帧来存储局部变量、参数和返回地址。堆栈帧的管理在 C++ 中是手动控制(或由编译器自动处理但有明确的ABI约定),而在 Java 中由 JVM 管理。现代 JVM 的栈管理和 C++ 的栈管理在效率上是相似的,并且 C++ 的编译器往往能更有效地管理堆栈,例如通过寄存器传参(calling conventions)。

数据类型和操作:
整数溢出: 斐波那契数列增长很快。如果计算的斐波那契数较大,可能会导致整数溢出。Java 的 `int` 和 `long` 在溢出时表现为环绕(wraparound),而 C++ 的标准行为(对于有符号整数)是未定义的。然而,通常我们会使用无符号整数或更大数据类型的(如 `BigInteger`)来处理大的斐波那契数,这会增加计算复杂性,而不是影响基础的递归速度。在原始的递归实现中,这个差异通常不显著。

为什么 C++ 通常更快?

让我们反过来思考,为什么 C++ 通常在原始计算密集型任务上比 Java 快:

1. 直接编译为机器码: C++ 代码在编译时就直接转换成特定平台的机器指令。这意味着 C++ 程序可以利用硬件的所有特性,并且没有虚拟机解释或 JIT 编译的中间层开销。
2. 更低的抽象级别和直接硬件访问: C++ 允许更底层的内存管理和对硬件的直接控制。虽然对于递归斐波那契这种简单任务来说,这种优势不明显,但在复杂的底层操作中,C++ 的优势就体现出来了。
3. 更小的运行时开销: C++ 程序通常拥有一个非常小的运行时库(runtime library),相比之下,Java 程序依赖于庞大的 JVM。JVM 的启动、类的加载、安全检查、内存管理等都需要额外的开销。
4. 更强的编译时优化: C++ 编译器可以进行非常激进的编译时优化,例如模板元编程、常量折叠、循环展开等,这些优化可以在代码运行之前完成,生成更高效的机器码。
5. 手动内存管理(可选): 开发者可以精确控制内存的分配和释放,避免不必要的内存分配和复制。

如何优化递归斐波那契数列(无论语言):

为了公正地比较,你应该比较 优化后的 版本。以下是优化递归斐波那契数列的两种主要方法:

1. 记忆化递归 (Memoization):
使用一个数组或 Map 来存储已经计算过的斐波那契数。
在递归函数中,先检查结果是否已在缓存中,如果在则直接返回;否则,计算结果,存入缓存后再返回。
时间复杂度:O(n)
空间复杂度:O(n) (用于存储缓存)

Java 示例(记忆化):
```java
import java.util.HashMap;
import java.util.Map;

public class Fibonacci {
private static Map memo = new HashMap<>();

public static long fibonacciMemo(int n) {
if (n <= 1) {
return n;
}
if (memo.containsKey(n)) {
return memo.get(n);
}
long result = fibonacciMemo(n 1) + fibonacciMemo(n 2);
memo.put(n, result);
return result;
}

public static void main(String[] args) {
int n = 40; // 示例输入
long startTime = System.nanoTime();
long result = fibonacciMemo(n);
long endTime = System.nanoTime();
System.out.println("Fibonacci(" + n + ") = " + result);
System.out.println("Java Execution Time: " + (endTime startTime) / 1_000_000.0 + " ms");
}
}
```

C++ 示例(记忆化):
```cpp
include
include
include

// 使用 std::vector 作为缓存,初始化为 1 表示未计算
std::vector memo;

long long fibonacciMemo(int n) {
if (n <= 1) {
return n;
}
if (memo[n] != 1) {
return memo[n];
}
memo[n] = fibonacciMemo(n 1) + fibonacciMemo(n 2);
return memo[n];
}

int main() {
int n = 40; // 示例输入
memo.assign(n + 1, 1); // 初始化缓存

auto startTime = std::chrono::high_resolution_clock::now();
long long result = fibonacciMemo(n);
auto endTime = std::chrono::high_resolution_clock::now();

std::cout << "Fibonacci(" << n << ") = " << result << std::endl;

std::chrono::duration duration = endTime startTime;
std::cout << "C++ Execution Time: " << duration.count() << " ms" << std::endl;

return 0;
}
```

2. 迭代 (Iteration):
使用循环来计算斐波那契数列,只需要存储前两个数字即可。
时间复杂度:O(n)
空间复杂度:O(1)

Java 示例(迭代):
```java
public class FibonacciIterative {
public static long fibonacciIterative(int n) {
if (n <= 1) {
return n;
}
long fibMinus2 = 0;
long fibMinus1 = 1;
long currentFib = 0;
for (int i = 2; i <= n; i++) {
currentFib = fibMinus1 + fibMinus2;
fibMinus2 = fibMinus1;
fibMinus1 = currentFib;
}
return currentFib;
}

public static void main(String[] args) {
int n = 40; // 示例输入
long startTime = System.nanoTime();
long result = fibonacciIterative(n);
long endTime = System.nanoTime();
System.out.println("Fibonacci(" + n + ") = " + result);
System.out.println("Java Execution Time: " + (endTime startTime) / 1_000_000.0 + " ms");
}
}
```

C++ 示例(迭代):
```cpp
include
include

long long fibonacciIterative(int n) {
if (n <= 1) {
return n;
}
long long fibMinus2 = 0;
long long fibMinus1 = 1;
long long currentFib = 0;
for (int i = 2; i <= n; ++i) {
currentFib = fibMinus1 + fibMinus2;
fibMinus2 = fibMinus1;
fibMinus1 = currentFib;
}
return currentFib;
}

int main() {
int n = 40; // 示例输入
auto startTime = std::chrono::high_resolution_clock::now();
long long result = fibonacciIterative(n);
auto endTime = std::chrono::high_resolution_clock::now();

std::cout << "Fibonacci(" << n << ") = " << result << std::endl;

std::chrono::duration duration = endTime startTime;
std::cout << "C++ Execution Time: " << duration.count() << " ms" << std::endl;

return 0;
}
```

结论:

如果您在某个特定的测试场景下观察到 Java 的递归斐波那契计算比 C++ 快,那么这 极有可能不是因为 Java 的原生递归本身比 C++ 更快,而是由于测试环境、编译器优化设置、JVM 的预热效应,或者测试数据量过小等因素造成的。

在通常的、经过充分优化的比较中,C++ 的原生递归(或更常见的优化版本)在性能上通常会优于 Java 的原生递归实现,尤其是在计算量较大时。 这是因为 C++ 更接近硬件,编译后的代码开销更小,并且编译器优化能力更直接。

如果您确实想得出“Java 比 C++ 快”的结论,您需要非常仔细地设计您的测试,确保:
C++ 编译器启用了最高级别的优化。
Java 程序有足够的“预热”时间。
测试的输入值足够大,能够体现出算法的实际计算量。
测量方法准确可靠,并进行多次平均以消除噪声。

但即使如此,也很难在纯递归斐波那契的场景下看到 Java 显著快于 C++。更有可能的情况是两者在优化后都很快,C++ 可能会略微领先。

网友意见

user avatar

前面已有的回答一个都没答到点上啊。

(就跟其他答主提到的一样,题主的电脑似乎有点慢…)

其实这个例子反映出来的跟Java与C++语言上的差异没有任何关系,只是反映出了在这个特定例子上某个编译器(及JIT编译器)的优化的差异。

本文后面用到的编译器生成的代码的样子我都记录下来放在这个传送门了,欢迎参考:

gist.github.com/rednaxe

这个例子的重点是函数调用次数特别高,而每次调用在函数内部做的事情特别少,所以比拼的是降低函数调用开销——要么减少未内联的函数调用次数,要么让函数调用自身的开销减少。

题主只给出了测试代码,没有说明测试性能用的环境(后来补充说C++编译器是VS2015里的MSVC)。从截图看至少可知是Windows上做的测试 ,但看不出来C++版本所使用的编译器是哪个;Java的话想来应该是用的Oracle JDK的某个版本,所以Java代码是在HotSpot VM上运行的。

这里根据题主给的例子来给出一组对比用的测试代码:

(拜托题主(们)以后请直接贴代码的文本,不要只贴截图)

C++版本:fib.cc

       #include <ctime> #include <iostream>  using namespace std;  int o = 0;  int fib(int a) {   o++;   return a <= 2 ? 1 : fib(a - 1) + fib(a - 2); }  clock_t to_duration_in_ms(clock_t start, clock_t end) {   return 1000 * (end - start) / CLOCKS_PER_SEC; }  int main() {   int n;    cin >> n;    clock_t start = clock();   int answer = fib(n);   clock_t end = clock();    cout << answer << ", run time: " << to_duration_in_ms(start, end) << "ms, repeat times: " << o << endl;    return 0; }      

Java版本:Fib.java

       import java.util.*;  public class Fib {   static int o = 0;    public static void main(String[] args) {     Scanner in = new Scanner(System.in);     int n = in.nextInt();     long start = System.currentTimeMillis();     int answer = fib(n);     long end = System.currentTimeMillis();     System.out.printf("%d, run time: %dms, repeat times: %d
", answer, (end - start), o);   }    public static int fib(int a) {     o++;     return a <= 2 ? 1 : fib(a - 1) + fib(a - 2);   } }      

这里C++版与Java版的 fib(int) 函数的语义是基本一样的,并不会体现出语言层面的语义差异。

事实上Java的 int 跟C/C++的 int32_t 是基本一样的,唯一的差别就是Java的 int 在算术运算溢出的时候有明确规定要用signed wrapping语义,而C/C++的 int / int32_t 在算术溢出时则是undefined behavior。

这里两个版本的测试剩下的一个差异就是,Java版由JVM规范规定说要在方法调用即将导致栈溢出的时候要抛出StackOverflowException,所以大部分JVM都会在方法入口处做一些检查,所以即便方法的内容相同,JVM的JIT编译器给这个Java版 fib(int) 生成的机器码也可能会比C++版代码被编译后生成的机器码要在方法入口处多一丁点代码。

然后有些JVM通过safepoint polling方式来实现VM对Java线程的暂停(例如stop-the-world)的功能,例如HotSpot VM至少会在所有JIT编译后代码的返回处添加safepoint polling来保证在VM需要暂停Java线程时,Java线程可以及时响应。这里又会多一丁点代码。

除此之外就没有任何差异了。Java的静态方法的调用语义跟C/C++的非虚函数的调用语义并没有显著差异,如果由同样优化的编译器来编译的话,就会得到同样级别性能的代码。

在Mac OS X 10.9上分别用Oracle JDK8u101和Clang 3.5 -O2来测试,两边用的编译命令分别为:

       $ clang++ -O2 fib.cc $ javac Fib.java     

然后运行各自运行5次的结果:

C++:

       $ ./a.out 40 102334155, run time: 426ms, repeat times: 204668309 $ ./a.out 40 102334155, run time: 420ms, repeat times: 204668309 $ ./a.out 40 102334155, run time: 418ms, repeat times: 204668309 $ ./a.out 40  102334155, run time: 410ms, repeat times: 204668309 $ ./a.out 40 102334155, run time: 432ms, repeat times: 204668309      

(使用同版本Clang用 clang++ -O3 fib.cc 来编译的话,测试时间结果在完全相同的范围。这是因为这个版本的Clang在-O2和-O3下编译这个程序得到的是完全一样的代码。)

Java:

       $ java Fib 40 102334155, run time: 357ms, repeat times: 204668309 $ java Fib 40 102334155, run time: 323ms, repeat times: 204668309 $ java Fib 40 102334155, run time: 319ms, repeat times: 204668309 $ java Fib 40 102334155, run time: 325ms, repeat times: 204668309 $ java Fib 40 102334155, run time: 341ms, repeat times: 204668309      

有同学好奇多跑一会儿的话会怎样。那我们就在同一环境运行同一测试对比一下 fib(n = 46) 的情况:

       $ java Fib 46 1836311903, run time: 6423ms, repeat times: -622343491 $ java Fib 46 1836311903, run time: 5815ms, repeat times: -622343491 $ java Fib 46 1836311903, run time: 6063ms, repeat times: -622343491  $ clang++ -O3 zz.cc $ ./a.out 46 1836311903, run time: 7442ms, repeat times: -622343491 $ ./a.out 46 1836311903, run time: 7392ms, repeat times: -622343491 $ ./a.out 46 1836311903, run time: 7379ms, repeat times: -622343491      

(是的,repeat times已经溢出了,但这里我们先不管它)

尴尬?顺带在我的环境上就算用最新的Clang 5.0 svn-trunk也是这样的。

可以看到Java版确实更快一些。这里没有“作弊”的因素,双方也都开启了编译器优化,并没有说一方因为是debug版本编译而影响了优化选项。

在上述的测试环境中,Java版本的 fib(int) 方法一开始由HotSpot VM的解释器解释执行,经过若干次调用之后会被JIT编译为机器码来执行。由于这里计算Fibonacci数列所用的是最直观的递归算法,调用次数会非常高,因而很快就会达到JIT编译的条件进而触发JIT编译。

在 fib(n = 40) 的用例中,这个测试程序只会有很小部分时间在解释器里执行,而绝大部分时间都在JIT编译优化过的代码中执行。所以它正好规避了一般Java程序在启动时有很大部分时间耗在解释器里使得启动慢的状况,而跟C++版的代码基本上在同一起跑线上——拼编译器优化。

在这个测试环境中,Java版本的 fib(int) 最终会被HotSpot VM的Server Compiler(“C2”)所编译。这是一个还算优化的编译器,它的优化程度大致上能跟GCC、Clang的-O2相似(或略弱于GCC / Clang)。

C2并不会做尾调用(tail call)或者尾递归(tail recursion)优化,但是它默认可以对递归调用的方法做1层递归内联。在这里具体来说就是会把一层 fib(a - 1) 与 fib(a - 2) 给内联进来。所以经过它的优化后,实际被编译出来的 fib(int) 代码是类似这样的:

         public static int fib(int a) {     ++o;     if (a <= 2) return 1;      int result;      // inlined fib(a - 1)     ++o;     if ((a - 1) <= 2) {       result = 1;     } else {       result = fib(a - 2) + fib(a - 3);     }      // inlined fib(a - 2)     ++o;     if ((a - 2) <= 2) {       result += 1;     } else {       result += fib(a - 3) + fib(a - 4);     }      return result;   }      

这样比起原本的代码就大幅度降低了未内联方法的调用次数,从而正中靶心降低了这个测试用例里最大头的开销。

而C++版的测试环境中,Clang 3.5在-O2级别上做的优化是:把两个对 fib(int) 的递归调用的其中一个当作尾调用并做优化,另一个仍然实现为递归调用。重要:Clang+LLVM是不会对递归函数做递归内联的,一层都不会内联,但它可以做尾调用 / 尾递归优化。这个具体用例优化后的代码是类似这样的:

       int fib(int a) {   ++o;   int result = 1;   while (a > 2) {     result += fib(a - 1);     a -= 2;     ++o;   }   return result; }      

这是个相当简洁漂亮的形式,它可以看作是把 fib(a) = fib(a - 1) + fib(a - 2) 中的 fib(a - 2) 部分当作尾递归(于是就消除了这个调用,变为内联的循环形式),而保留 fib(a - 1) 为未内联的函数调用。

可惜这个形式在这个具体例子上性能却不如前面C2编译出来的版本好。让我们来对比一下两个优化后版本的代码的实际未内联调用次数的情况。下面给出的两列数据,左边是该次调用自身调用未内联函数的次数,右边是该次调用总共引起的未内联函数调用的次数。

Java版经过C2编译后:

       fib(1) = 0 calls / 0 calls fib(2) = 0 calls / 0 calls fib(3) = 0 calls / 0 calls fib(4) = 2 calls / 2 calls fib(5) = 4 calls / 4 calls fib(6) = 4 calls / 6 calls fib(7) = 4 calls / 12 calls fib(8) = 4 calls / 20 calls fib(9) = 4 calls / 32 calls fib(10) = 4 calls / 54 calls fib(11) = 4 calls / 88 calls fib(12) = 4 calls / 142 calls fib(13) = 4 calls / 232 calls fib(14) = 4 calls / 376 calls fib(15) = 4 calls / 608 calls fib(16) = 4 calls / 986 calls fib(17) = 4 calls / 1596 calls fib(18) = 4 calls / 2582 calls fib(19) = 4 calls / 4180 calls fib(20) = 4 calls / 6764 calls fib(21) = 4 calls / 10944 calls fib(22) = 4 calls / 17710 calls fib(23) = 4 calls / 28656 calls fib(24) = 4 calls / 46366 calls fib(25) = 4 calls / 75024 calls fib(26) = 4 calls / 121392 calls fib(27) = 4 calls / 196416 calls fib(28) = 4 calls / 317810 calls fib(29) = 4 calls / 514228 calls fib(30) = 4 calls / 832038 calls fib(31) = 4 calls / 1346268 calls fib(32) = 4 calls / 2178308 calls fib(33) = 4 calls / 3524576 calls fib(34) = 4 calls / 5702886 calls fib(35) = 4 calls / 9227464 calls fib(36) = 4 calls / 14930350 calls fib(37) = 4 calls / 24157816 calls fib(38) = 4 calls / 39088168 calls fib(39) = 4 calls / 63245984 calls fib(40) = 4 calls / 102334154 calls     

C++版经过 Clang 3.5 -O2 编译后:

       fib(1) = 0 calls / 0 calls fib(2) = 0 calls / 0 calls fib(3) = 1 calls / 1 calls fib(4) = 1 calls / 2 calls fib(5) = 2 calls / 4 calls fib(6) = 2 calls / 7 calls fib(7) = 3 calls / 12 calls fib(8) = 3 calls / 20 calls fib(9) = 4 calls / 33 calls fib(10) = 4 calls / 54 calls fib(11) = 5 calls / 88 calls fib(12) = 5 calls / 143 calls fib(13) = 6 calls / 232 calls fib(14) = 6 calls / 376 calls fib(15) = 7 calls / 609 calls fib(16) = 7 calls / 986 calls fib(17) = 8 calls / 1596 calls fib(18) = 8 calls / 2583 calls fib(19) = 9 calls / 4180 calls fib(20) = 9 calls / 6764 calls fib(21) = 10 calls / 10945 calls fib(22) = 10 calls / 17710 calls fib(23) = 11 calls / 28656 calls fib(24) = 11 calls / 46367 calls fib(25) = 12 calls / 75024 calls fib(26) = 12 calls / 121392 calls fib(27) = 13 calls / 196417 calls fib(28) = 13 calls / 317810 calls fib(29) = 14 calls / 514228 calls fib(30) = 14 calls / 832039 calls fib(31) = 15 calls / 1346268 calls fib(32) = 15 calls / 2178308 calls fib(33) = 16 calls / 3524577 calls fib(34) = 16 calls / 5702886 calls fib(35) = 17 calls / 9227464 calls fib(36) = 17 calls / 14930351 calls fib(37) = 18 calls / 24157816 calls fib(38) = 18 calls / 39088168 calls fib(39) = 19 calls / 63245985 calls fib(40) = 19 calls / 102334154 calls     

可以看到在未内联函数的总调用次数上,两个版本在过程中是几乎一样的,在 fib(n = 40) 的用例中是正好一样。但C2版的代码只有函数调用而没有循环,而Clang版代码则在函数调用之外还有循环的开销,这么一加上去后者的开销就比前者大,造成了题主看到的“C++比Java慢”的现象。

如果我们模拟C2的优化方式,人肉把C++的测试代码改为:

       __attribute__((__noinline__)) int fib(int a) {   ++o;   if (a <= 2) return 1;    int result;    // emulate inlining fib(a - 1)   ++o;   if ((a - 1) <= 2) {     result = 1;   } else {     result = fib(a - 2) + fib(a - 3);   }    // emulate inlining fib(a - 2)   ++o;   if ((a - 2) <= 2) {     result += 1;   } else {     result += fib(a - 3) + fib(a - 4);   }    return result; }      

同样用 Clang 3.5 -O2 来编译,在我的机器上运行的结果就变成:

       $ ./a.out 40 102334155, run time: 333ms, repeat times: 204668309 $ ./a.out 40 102334155, run time: 341ms, repeat times: 204668309 $ ./a.out 40 102334155, run time: 347ms, repeat times: 204668309 $ ./a.out 40 102334155, run time: 327ms, repeat times: 204668309 $ ./a.out 40 102334155, run time: 332ms, repeat times: 204668309      

跟Java版测试的结果就几乎一样了。

顺带一题,MSVC编译器在/Ox(打开所有优化)的情况下对本文开头的C++版 fib(int) 函数也没做多少优化,既没有做递归内联也没有做尾递归优化,所以经过它编译后的代码仍然后 fib(a) = fib(a - 1) + fib(a - 2) 这样的代码结构,未内联函数调用的次数就跟代码中全局变量“o”的值一样多。

然后,GCC在-O2级别上生成的代码跟上面演示的 Clang 3.5 -O2 的形式基本上是一样的,性能自然也差不多。

GCC 7 -O2在Linux/x86-64上生成的代码是这样的:

       int foo(int a) {   ++o;   if (a <= 2) return 1;      int limit = (a - 3) & 1;   --a;   int result = 0;    while (a != limit) {     result += fib(a);     a -= 2;     ++o;   }    return result + 1; }      

是不是跟前面举例的Clang 3.5 -O2非常相似?

而GCC在-O3级别生成的代码则复杂许多,会做多层展开,可以进一步提高性能。

它做的优化反映在这个例子上,主要是:既像C2那样做的内联展开,又非常聪明地做了尾递归优化,还能够智能地发现内联后多次调用诸如 fib(a - 3) 应该得到相同的结果于是进行了合并,最后它还大幅减少了更新全局变量o所需要的内存写操作,而是把o的中间值都放在寄存器里,最后再一口气把最终o应有的值写回内存。

我在另一个测试环境,一台Skylake服务器上用本文开头原始版本的C++版 fib(int) 用GCC 5.4测试,分别以-O2 / -O3来编译,结果如下:

       $ g++ -O2 fib.cc $ ./a.out  40 102334155, run time: 390ms, repeat times: 204668309 $ ./a.out  40 102334155, run time: 357ms, repeat times: 204668309 $ ./a.out  40 102334155, run time: 388ms, repeat times: 204668309 $ g++ -O3 fib.cc $ ./a.out  40 102334155, run time: 189ms, repeat times: 204668309 $ ./a.out  40 102334155, run time: 194ms, repeat times: 204668309 $ ./a.out  40 102334155, run time: 191ms, repeat times: 204668309      

在同一环境中用Oracle JDK8u25来运行本文最初的Java版代码:

       $ ~/sdk/jdk1.8.0_25/bin/java Fib 40 102334155, run time: 375ms, repeat times: 204668309 $ ~/sdk/jdk1.8.0_25/bin/java Fib 40 102334155, run time: 378ms, repeat times: 204668309 $ ~/sdk/jdk1.8.0_25/bin/java Fib 40 102334155, run time: 376ms, repeat times: 204668309      

群众喜闻乐见的C++比Java快又回来了(误

(重要提醒:这里仍然只是反映了GCC 5.4 -O3比HotSpot C2更加优化而已,跟C++与Java的语言差异没有任何关系)

再更新一点。如果我想让这个Java版的“成绩”再稍微提高那么一丁点,只要加两行代码就好:

       import java.util.*;  public class Fib {   static int o = 0;    public static void main(String[] args) {     Scanner in = new Scanner(System.in);     int n = in.nextInt();     fib(n);   // add this line     o = 0;    // and add this line     long start = System.currentTimeMillis();     int answer = fib(n);     long end = System.currentTimeMillis();     System.out.printf("%d, run time: %dms, repeat times: %d
", answer, (end - start), o);   }    public static int fib(int a) {     o++;     return a <= 2 ? 1 : fib(a - 1) + fib(a - 2);   } }     

在跟上面相同的我的Mac上用JDK8u101来跑:

       $ java Fib 40 102334155, run time: 313ms, repeat times: 204668309 $ java Fib 40 102334155, run time: 321ms, repeat times: 204668309 $ java Fib 40 102334155, run time: 320ms, repeat times: 204668309 $ java Fib 40 102334155, run time: 331ms, repeat times: 204668309 $ java Fib 40 102334155, run time: 327ms, repeat times: 204668309 $ java Fib 40 102334155, run time: 319ms, repeat times: 204668309      

对HotSpot VM的执行方式有一定了解的同学肯定一下就能明白这么做的意图是什么,所以就不多写,留作习题吧 ^_^

就这样嗯。

类似的话题

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有