问:在什么情况下,Java 比 C++ 慢很多?
答:Ben Maurer:
为了回答这个问题,需要先将该问题分成几个可能引起慢的原因:
垃圾回收器。这是一把“双刃剑”。如果你的程序遵循“大部分对象都在年青代中消亡”模型,垃圾回收器是非常有利的(很少的碎片,更好的缓存局部性)。但是,如果程序不遵循该模型,JVM将花费很多资源来回收堆内存。
大对象。在Java中,所有的对象都有一个vtable指针,而C++中使用POD结构没有额外开销。此外,所有的Java对象是可以被锁定的。其 实现依赖于JVM,这可能需要在对象中增加额外的字段。大对象 == 缓存更少的对象 == 更慢。(另一方面,Java 7 用64位记录压缩后的指针,这也是造成该问题的一部分原因。
缺乏内联对象。在Java中,所有的类都是指针。在C++中,对象可以和其它对象一起分配,或者在栈上分配。这样可以提高缓存的局部性,从而减少动态内存分配的开销。
平台函数调用。在Java中,JNI的调用或者将对象编译成本地代码都会带来不小的开销。如果你需要频繁调用客户端的C++代码,会增加很大的开销。
低效的强制抽象。例如,在Java中字符串是不可变的。如果你想写一个XML分析器,你只使用String对象(没有char[]),它将会很慢,因为需要分配额外的空间。
虚函数调用增加。JVM中,几乎所有的函数调用都是虚函数调用。有许多代码尝试避免虚函数调用,但是很多场景下,JVM无法解决这个问题。这阻碍了代码的内联,使代码变慢。
缺乏高级的编译特征及转为汇编的能力。 如果你写了一段能从汇编得益的代码Java可能表现不佳。
在我看来,最大的问题是垃圾回收。在程序中,强制在大的内存中进行多次完全GC,是最容易导致Java和C++之间产生鸿沟的原因之一。除此之外,如果将程序的工作集放在L2缓存之外,像大对象、缺乏内联对象等问题,也会导致两者之间的巨大差别。
低效的强制抽象和平台函数也会导致速度下降,但是这通常只会因为低级的代码才会产生。如果你使用写得很好的Java代码库,这通常不是什么大问题。
答:Todd Lipcon
我基本同意Ben Maurer(hey Ben!)的回答。有几个小点不同:
在最新的JVM中,当这种分配永远不会从(a)局部函数或(b)局部线程逃逸出去的时候,逃逸分析能有效地决定一种固定分配。也就是说当分配不需要 加锁,通常是在自身的栈空间上进行的。这两种情况下都是一种简单的“指针碰撞(bump the pointer)”分配,这等同于C中的栈分配。
译者注:
- 逃逸分析 Escape Analysis,是一种编译优化技术,指分析指针动态范围的方法。通俗地说,当一个对象的指针被多个方法或线程引用时,我们称这个指针发生了逃逸。
- 指针碰撞(bump the point)。假设Java堆中内存是绝对规整的,所有用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配 内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”。
即使没有逃逸分析,年青代的分配也是通过指针碰撞方式,在线程本地分配缓冲区(TLAB)中完成的,不需要进行同步。所以Java中小对象的分配有 的时候比C语言实现的 malloc() 方式更快。更好的 malloc 方法像Google的 tcmalloc,采用了类似的方式。但是由于C语言无法在内存中对分配后的对象重新分配,所以某些方面会受到限制。
虽然存在内联和虚函数问题,但是实际上,Java在某些情况下甚至可以做的比C更好。特别是,C不能通过动态链接功能来实现内联,因为内联是在编译 时期进行的,而不是运行时期。而Java可越过不同的类或库的边界来动态内联一个函数,即使该类的真正实现在编译期间还不可用。许多工作中,这种方式比 C++的虚函数调用更有效,C++虚函数调用总是需要调用虚表。而JIT编译器,如果之前动态属性已经丢失(如新的类已经被加载),能够聪明地取消内联优 化。
新版本的GCC提供一些这方面优化,称为“全程序优化”或“链接时优化”(http://gcc.gnu.org/wiki/LinkTime…),允许在工程范围内越过对象文件进行内联。但是,基本上还是不允许通过动态链接的方式来实现内联(如通过内联的方式实现zlib的调用等)。许多大型项目都是通过复制标准库的功能到它们的代码中来实现。
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/tech/aiops/57795.html