首先,大多数JVM都包含一个编译器,因此“解释的字节码”实际上非常少见(至少在基准代码中如此)在现实生活中并不罕见,因为在您的代码中,通常不只是几个琐碎的循环,而且这些循环经常重复出现)。
其次,涉及的许多基准似乎有很大的偏见(无论是出于意图还是出于无能,我不能说真的)。例如,几年前,我查看了您发布的链接之一中链接的一些源代码。它具有如下代码:
init0 = (int*)calloc(max_x,sizeof(int));
init1 = (int*)calloc(max_x,sizeof(int));
init2 = (int*)calloc(max_x,sizeof(int));
for (x=0; x<max_x; x++) {
init2[x] = 0;
init1[x] = 0;
init0[x] = 0;
}
由于calloc
提供的内存已经归零,因此使用for
循环将其再次归零显然是没有用的。之后(如果有内存可用),无论如何都要用其他数据填充内存(并且不依赖于将其置零),因此无论如何都完全不需要进行所有置零。用一个简单的代码替换上面的代码malloc
(就像任何理智的人一样)可以提高C ++版本的速度,使其足以胜过Java版本(如果有内存的话,可以说是相当大的优势)。
考虑(作为另一个示例)methcall
上一个链接中博客条目中使用的基准。尽管名称(以及外观可能如何),但C ++版本并没有真正衡量方法调用开销。关键的代码部分在Toggle类中:
class Toggle {
public:
Toggle(bool start_state) : state(start_state) { }
virtual ~Toggle() { }
bool value() {
return(state);
}
virtual Toggle& activate() {
state = !state;
return(*this);
}
bool state;
};
关键是事实state = !state;
。考虑一下当我们更改代码以将状态编码为int
而不是时会发生什么bool
:
class Toggle {
enum names{ bfalse = -1, btrue = 1};
const static names values[2];
int state;
public:
Toggle(bool start_state) : state(values[start_state])
{ }
virtual ~Toggle() { }
bool value() { return state==btrue; }
virtual Toggle& activate() {
state = -state;
return(*this);
}
};
微小的变化使整体速度提高了大约5:1。尽管基准旨在测量方法调用时间,但实际上,它所测量的大部分时间都是在int
和之间转换的时间bool
。我当然同意,不幸的是,原始代码显示的效率低下-但鉴于它在实际代码中似乎很少出现,并且很容易在发生时(如果确实发生)进行修复,那么我很难思考它的意义很大。
万一有人决定重新运行所涉及的基准测试,我还应该补充一点,就是对产生的Java版本进行了几乎相同的微不足道的修改(或至少一次生成了-我没有使用最近的JVM(以确认它们仍然可以使用))也对Java版本进行了相当大的改进。Java版本具有一个NthToggle :: activate(),如下所示:
public Toggle activate() {
this.counter += 1;
if (this.counter >= this.count_max) {
this.state = !this.state;
this.counter = 0;
}
return(this);
}
将其更改为调用基本函数而不是this.state
直接进行操作可以大大提高速度(尽管不足以跟上修改后的C ++版本)。
因此,我们最终得到的是关于解释后的字节码与某些(我见过的)最差基准测试的错误假设。两者都没有给出有意义的结果。
我自己的经验是,在经验丰富的程序员同样重视优化的情况下,C ++往往会击败Java,但是(至少在这两者之间),C ++几乎不会像程序员和设计那样产生太大的影响。被引用的基准告诉我们的更多关于作者的能力(不称职)/(不诚实)诚实的信息,而不是他们声称的基准语言。
[编辑:正如上面的某处所暗示,但从未像我应该直接指出的那样,我引用的结果是大约5年前使用当时最新的C ++和Java实现进行测试时得到的结果。我还没有使用当前的实现重新运行测试。乍一看,表明代码尚未修复,因此所有更改将是编译器掩盖代码中问题的能力。]
但是,如果我们忽略Java示例,则实际上解释后的代码可能比编译后的代码运行得更快(尽管很困难,而且有些不寻常)。
发生这种情况的通常方式是,所解释的代码比机器代码紧凑得多,或者它在数据缓存比代码缓存大的CPU上运行。
在这种情况下,小型解释器(例如,Forth实现的内部解释器)可能完全适合代码缓存,而其解释的程序完全适合数据缓存。缓存通常比主内存快至少10倍,并且通常要快得多(100倍也不再罕见)。
因此,如果高速缓存比主内存快N倍,并且实现每个字节代码所需的机器代码指令少于N个,那么字节代码应该会获胜(我正在简化,但我认为总体思路仍然应该显而易见)。