Ответ 1
Как отмечали другие, тест имеет недостатки во многих отношениях.
Вы точно не сказали нам, как вы это сделали. Тем не менее, я попытался реализовать "наивный" тест (без обид):
class PrePostIncrement
{
public static void main(String args[])
{
for (int j=0; j<3; j++)
{
for (int i=0; i<5; i++)
{
long before = System.nanoTime();
runPreIncrement();
long after = System.nanoTime();
System.out.println("pre : "+(after-before)/1e6);
}
for (int i=0; i<5; i++)
{
long before = System.nanoTime();
runPostIncrement();
long after = System.nanoTime();
System.out.println("post : "+(after-before)/1e6);
}
}
}
private static void runPreIncrement()
{
final int n = Integer.MAX_VALUE;
int i = 0;
while (++i < n) {}
}
private static void runPostIncrement()
{
final int n = Integer.MAX_VALUE;
int i = 0;
while (i++ < n) {}
}
}
При запуске с настройками по умолчанию, похоже, небольшая разница. Но недостаток реального теста становится очевидным, когда вы запускаете его с помощью флага -server
. Результаты в моем случае затем следуют примерно как
...
pre : 6.96E-4
pre : 6.96E-4
pre : 0.001044
pre : 3.48E-4
pre : 3.48E-4
post : 1279.734543
post : 1295.989086
post : 1284.654267
post : 1282.349093
post : 1275.204583
Очевидно, что пре-инкрементная версия полностью оптимизирована. Причина довольно проста: результат не используется. Не имеет значения, выполняется ли цикл или нет, поэтому JIT просто удаляет его.
Это подтверждается просмотром разборки хот-спота: версия предварительного инкремента приводит к этому коду:
[Entry Point]
[Verified Entry Point]
[Constants]
# {method} {0x0000000055060500} 'runPreIncrement' '()V' in 'PrePostIncrement'
# [sp+0x20] (sp of caller)
0x000000000286fd80: sub $0x18,%rsp
0x000000000286fd87: mov %rbp,0x10(%rsp) ;*synchronization entry
; - PrePostIncrement::[email protected] (line 28)
0x000000000286fd8c: add $0x10,%rsp
0x000000000286fd90: pop %rbp
0x000000000286fd91: test %eax,-0x243fd97(%rip) # 0x0000000000430000
; {poll_return}
0x000000000286fd97: retq
0x000000000286fd98: hlt
0x000000000286fd99: hlt
0x000000000286fd9a: hlt
0x000000000286fd9b: hlt
0x000000000286fd9c: hlt
0x000000000286fd9d: hlt
0x000000000286fd9e: hlt
0x000000000286fd9f: hlt
Версия после инкремента приводит к этому коду:
[Entry Point]
[Verified Entry Point]
[Constants]
# {method} {0x00000000550605b8} 'runPostIncrement' '()V' in 'PrePostIncrement'
# [sp+0x20] (sp of caller)
0x000000000286d0c0: sub $0x18,%rsp
0x000000000286d0c7: mov %rbp,0x10(%rsp) ;*synchronization entry
; - PrePostIncrement::[email protected] (line 35)
0x000000000286d0cc: mov $0x1,%r11d
0x000000000286d0d2: jmp 0x000000000286d0e3
0x000000000286d0d4: nopl 0x0(%rax,%rax,1)
0x000000000286d0dc: data32 data32 xchg %ax,%ax
0x000000000286d0e0: inc %r11d ; OopMap{off=35}
;*goto
; - PrePostIncrement::[email protected] (line 36)
0x000000000286d0e3: test %eax,-0x243d0e9(%rip) # 0x0000000000430000
;*goto
; - PrePostIncrement::[email protected] (line 36)
; {poll}
0x000000000286d0e9: cmp $0x7fffffff,%r11d
0x000000000286d0f0: jl 0x000000000286d0e0 ;*if_icmpge
; - PrePostIncrement::[email protected] (line 36)
0x000000000286d0f2: add $0x10,%rsp
0x000000000286d0f6: pop %rbp
0x000000000286d0f7: test %eax,-0x243d0fd(%rip) # 0x0000000000430000
; {poll_return}
0x000000000286d0fd: retq
0x000000000286d0fe: hlt
0x000000000286d0ff: hlt
Это не совсем понятно для меня, почему это, по-видимому, не удаляет версию после инкремента. (На самом деле, я рассматриваю вопрос как отдельный вопрос). Но, по крайней мере, это объясняет, почему вы можете видеть различия с "порядком величины"...
EDIT: Интересно, что при изменении верхнего предела цикла от Integer.MAX_VALUE
до Integer.MAX_VALUE-1
, обе версии оптимизируются и требуют "нулевого" времени. Как-то этот предел (который все еще появляется как 0x7fffffff
в сборке) предотвращает оптимизацию. Предположительно, это имеет какое-то отношение к сопоставлению, сопоставленному с инструкцией (singed!) cmp
, но я не могу дать более глубокую причину помимо этого. JIT работает загадочно...