Ответ 1
ПРИМЕЧАНИЕ. Этот ответ является отредактированной версией того, что появилось в списке рассылки для собственных клиентов
Microbenchmarks сложны: если вы не понимаете, что делаете ОЧЕНЬ хорошо, легко производить сравнения яблок и апельсинов, которые не имеют отношения к поведению, которое вы хотите наблюдать/измерять вообще.
Я немного опишу ваш собственный пример (я исключу NaCl и придерживаюсь существующих, "проверенных и правдивых" технологий).
Вот ваш тест как родная программа C:
$ cat test1.c
#include <math.h>
#include <time.h>
#include <stdio.h>
int main() {
clock_t t = clock();
float result = 0;
for(int i = 0; i < 1000000000; ++i) {
result += sqrt(i);
}
t = clock() - t;
float tt = ((float)t)/CLOCKS_PER_SEC;
printf("%g %g\n", result, tt);
}
$ gcc -std=c99 -O2 test1.c -lm -o test1
$ ./test1
5.49756e+11 25.43
Ok. Мы можем сделать миллиард циклов за 25,43 секунды. Но давайте посмотрим, что требует времени: замените "result + = sqrt (i)"; с "результатом + = i;"
$ cat test2.c
#include <math.h>
#include <time.h>
#include <stdio.h>
int main() {
clock_t t = clock();
float result = 0;
for(int i = 0; i < 1000000000; ++i) {
result += i;
}
t = clock() - t;
float tt = ((float)t)/CLOCKS_PER_SEC;
printf("%g %g\n", result, tt);
}
$ gcc -std=c99 -O2 test2.c -lm -o test2
$ ./test2
1.80144e+16 1.21
Ничего себе! 95% времени было фактически потрачено на CPU-предоставленную функцию sqrt, все остальное заняло менее 5%. Но что, если мы немного изменим код: замените "printf (" % g% g\n ", result, tt);" с "printf (" % g\n ", tt);"
$ cat test3.c
#include <math.h>
#include <time.h>
#include <stdio.h>
int main() {
clock_t t = clock();
float result = 0;
for(int i = 0; i < 1000000000; ++i) {
result += sqrt(i);
}
t = clock() - t;
float tt = ((float)t)/CLOCKS_PER_SEC;
printf("%g\n", tt);
}
$ gcc -std=c99 -O2 test3.c -lm -o test3
$ ./test
1.44
Хм... Похоже, теперь "sqrt" почти так же быстро, как "+". Как это может быть? Как printf влияет на предыдущий цикл AT ALL?
Посмотрим:
$ gcc -std=c99 -O2 test1.c -S -o -
...
.L3:
cvtsi2sd %ebp, %xmm1
sqrtsd %xmm1, %xmm0
ucomisd %xmm0, %xmm0
jp .L7
je .L2
.L7:
movapd %xmm1, %xmm0
movss %xmm2, (%rsp)
call sqrt
movss (%rsp), %xmm2
.L2:
unpcklps %xmm2, %xmm2
addl $1, %ebp
cmpl $1000000000, %ebp
cvtps2pd %xmm2, %xmm2
addsd %xmm0, %xmm2
unpcklpd %xmm2, %xmm2
cvtpd2ps %xmm2, %xmm2
jne .L3
...
$ gcc -std=c99 -O2 test3.c -S -o -
...
xorpd %xmm1, %xmm1
...
.L5:
cvtsi2sd %ebp, %xmm0
ucomisd %xmm0, %xmm1
ja .L14
.L10:
addl $1, %ebp
cmpl $1000000000, %ebp
jne .L5
...
.L14:
sqrtsd %xmm0, %xmm2
ucomisd %xmm2, %xmm2
jp .L12
.p2align 4,,2
je .L10
.L12:
movsd %xmm1, (%rsp)
.p2align 4,,5
call sqrt
movsd (%rsp), %xmm1
.p2align 4,,4
jmp .L10
...
Первая версия на самом деле называет sqrt миллиард раз, но второй этого не делает вообще! Вместо этого он проверяет, является ли число отрицательным и вызывает sqrt только в этом случае! Зачем? Что здесь пытаются сделать компилятор (или, скорее, авторы компилятора)?
Ну, это просто: поскольку мы не использовали "результат" в этой конкретной версии, он может спокойно опустить вызов "sqrt"... если значение не является отрицательным, то есть! Если он отрицательный, тогда (в зависимости от флагов FPU) sqrt может делать разные вещи (возвращать бессмысленный результат, сбой программы и т.д.). Вот почему эта версия в десятки раз быстрее - но она не вычисляет квадратные корни вообще!
Вот пример, который показывает, как могут быть ошибочные микрообъекты:
$ cat test4.c
#include <math.h>
#include <time.h>
#include <stdio.h>
int main() {
clock_t t = clock();
int result = 0;
for(int i = 0; i < 1000000000; ++i) {
result += 2;
}
t = clock() - t;
float tt = ((float)t)/CLOCKS_PER_SEC;
printf("%d %g\n", result, tt);
}
$ gcc -std=c99 -O2 test4.c -lm -o test4
$ ./test4
2000000000 0
Время выполнения... ZERO? Как это может быть? Миллион вычислений меньше, чем мгновение глаз? Давайте посмотрим:
$ gcc -std=c99 -O2 test1.c -S -o -
...
call clock
movq %rax, %rbx
call clock
subq %rbx, %rax
movl $2000000000, %edx
movl $.LC1, %esi
cvtsi2ssq %rax, %xmm0
movl $1, %edi
movl $1, %eax
divss .LC0(%rip), %xmm0
unpcklps %xmm0, %xmm0
cvtps2pd %xmm0, %xmm0
...
Uh, oh, цикл полностью устранен! Все расчеты произошли во время компиляции и чтобы добавить оскорбление к травме, как "часы" были выполнены перед телом цикла для загрузки!
Что делать, если мы поместим его в отдельную функцию?
$ cat test5.c
#include <math.h>
#include <time.h>
#include <stdio.h>
int testfunc(int num, int max) {
int result = 0;
for(int i = 0; i < max; ++i) {
result += num;
}
return result;
}
int main() {
clock_t t = clock();
int result = testfunc(2, 1000000000);
t = clock() - t;
float tt = ((float)t)/CLOCKS_PER_SEC;
printf("%d %g\n", result, tt);
}
$ gcc -std=c99 -O2 test5.c -lm -o test5
$ ./test5
2000000000 0
По-прежнему то же самое??? Как это может быть?
$ gcc -std=c99 -O2 test5.c -S -o -
...
.globl testfunc
.type testfunc, @function
testfunc:
.LFB16:
.cfi_startproc
xorl %eax, %eax
testl %esi, %esi
jle .L3
movl %esi, %eax
imull %edi, %eax
.L3:
rep
ret
.cfi_endproc
...
Uh-oh: компилятор достаточно умен, чтобы заменить цикл на умножение!
Теперь, если вы добавите NaCl с одной стороны и JavaScript с другой стороны, вы получите такую сложную систему, что результаты будут буквально непредсказуемыми.
Проблема заключается в том, что для microbenchmark вы пытаетесь изолировать кусок кода, а затем оцениваете его свойства, но тогда компилятор (независимо от JIT или AOT) попытается помешать вашим усилиям, потому что он пытается удалить все бесполезные вычисления из вашей программы!
Microbenchmarks полезны, конечно, но они являются инструментом FORENSIC ANALYSIS, а не тем, что вы хотите использовать для сравнения скорости двух разных систем! Для этого вам нужна определенная "реальная" (в некотором смысле мир: то, что не может быть оптимизировано на куски чрезмерно жарким компилятором): в частности, популярны алгоритмы сортировки.
Тесты, которые используют sqrt, особенно неприятны, поскольку, как мы видели, обычно они тратят более 90% времени на выполнение одной команды CPU: sqrtsd (fsqrt, если это 32-разрядная версия), которая, конечно же, идентична для JavaScript и NaCl. Эти контрольные показатели (если они правильно реализованы) могут служить лакмусовой бумажкой (если скорость некоторой реализации слишком сильно отличается от того, что показывает простая собственная версия, то вы делаете что-то не так), но они бесполезны в сравнении со скоростью NaCl, JavaScript, С# или Visual Basic.