Таким образом, мы все знаем предубеждение, что Java интерпретируется медленно, а компилируемый и оптимизируемый C работает очень быстро. Ну, как вы, наверное, знаете, картина совсем другая.
TL; DR Java быстрее для созвездий, где JIT может выполнять встраивание, поскольку все методы / функции видны, тогда как компилятор C не может выполнять оптимизацию в единицах компиляции (подумайте о библиотеках и т. Д.).
Компилятор AC принимает код C в качестве входных данных, компилирует и оптимизирует его и генерирует машинный код для конкретного процессора или архитектуры, которая будет выполняться. Это приводит к выполнению исполняемого файла, который можно напрямую запустить на данном компьютере без дальнейших действий. Java, с другой стороны, имеет промежуточный этап: байт-код. Таким образом, компилятор Java принимает код Java в качестве входных данных и генерирует байт-код, который в основном является машинным кодом для абстрактной машины. Теперь для каждой (популярной) архитектуры ЦП существует Java Virual Machine, которая имитирует эту абстрактную машину и выполняет (интерпретирует) сгенерированный байт-код. И это так медленно, как кажется. Но, с другой стороны, байт-код достаточно переносим, поскольку один и тот же вывод будет работать на всех платформах — отсюда и слоган: « Пиши один раз, беги везде »
Теперь с подходом, описанным выше, было бы скорее « напиши один раз, жди везде », поскольку интерпретатор будет довольно медленным. Итак, что делает современная JVM, так это своевременная компиляция. Это означает, что JVM внутренне переводит байт-код в машинный код для процессора в руках. Но поскольку этот процесс довольно сложен, JVM Hotspot (наиболее часто используемая) делает это только для фрагментов кода, которые выполняются достаточно часто (отсюда и название Hotspot ). Помимо ускорения запуска (интерпретатор запускается сразу, компилятор JIT запускается по мере необходимости), у этого есть еще одно преимущество: JIT горячей точки уже знал, какая часть кода вызывается часто, а какая — нет, поэтому он может использовать это при оптимизации вывода. — и здесь наш пример вступает в игру.
Теперь, прежде чем взглянуть на мой крошечный, полностью составленный пример, позвольте мне отметить, что в Java есть много функций, таких как динамическая диспетчеризация (вызов метода в интерфейсе), что также связано с накладными расходами во время выполнения. Таким образом, Java-код, вероятно, легче написать, но все равно будет медленнее, чем C-код. Тем не менее, когда дело доходит до чистого перебора чисел, как в моем примере ниже, есть интересные вещи для открытия.
Итак, без дальнейших разговоров, вот пример кода C:
test.c:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
int compute(int i); int test (int i); int main(int argc, char** argv) { int sum = 0; for (int l = 0; l < 1000; l++) { int i = 0; while (i < 2000000) { if ( test (i)) sum += compute(i); i++; } } return sum ; } |
test1.c:
1
2
3
4
5
6
7
|
int compute(int i) { return i + 1; } int test (int i) { return i % 3; } |
То, что на самом деле вычисляет основная функция, совсем не важно. Дело в том, что он очень часто вызывает две функции (test и compute) и эти функции находятся в другом модуле компиляции (test1.c). Теперь давайте скомпилируем и запустим программу:
01
02
03
04
05
06
07
08
09
10
11
|
> gcc -O2 -c test1.c > gcc -O2 -c test .c > gcc test .o test1.o > time . /a .out real 0m6.693s user 0m6.674s sys 0m0.012s |
Таким образом, для выполнения вычислений требуется около 6,6 секунд . Теперь давайте посмотрим на программу Java:
Test.java
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
public class Test { private static int test( int i) { return i % 3 ; } private static int compute( int i) { return i + 1 ; } private static int exec() { int sum = 0 ; for ( int l = 0 ; l < 1000 ; l++) { int i = 0 ; while (i < 2000000 ) { if (test(i) != 0 ) { sum += compute(i); } i++; } } return sum; } public static void main(String[] args) { exec(); } } |
Теперь давайте скомпилируем и выполним это:
1
2
3
4
5
6
7
|
> javac Test.java > time java Test real 0m3.411s user 0m3.395s sys 0m0.030s |
Таким образом, для выполнения этой простой задачи Java занимает 3,4 секунды (и это даже включает медленный запуск JVM). Вопрос почему? И ответ, конечно, в том, что JIT может выполнять оптимизацию кода, чего не может компилятор C. В нашем случае это функция встраивания. Поскольку мы определили наши две крошечные функции в их собственном модуле компиляции, компилятор не может встроить их при компиляции test.c — с другой стороны, JIT имеет все методы под рукой и может выполнять агрессивное встраивание, и, следовательно, скомпилированный код работает намного быстрее.
Так это совершенно экзотический и вымышленный пример, который никогда не встречается в реальной жизни? Да и нет. Конечно, это крайний случай, но подумайте обо всех библиотеках, которые вы включаете в свой код. Все эти методы нельзя рассматривать для оптимизации в C, тогда как в Java не имеет значения, откуда байт-код. Поскольку все это присутствует в работающей JVM, JIT может оптимизировать в глубине души. Конечно, в С есть хитрость, чтобы уменьшить эту боль: Маркос. Это, на мой взгляд, одна из главных причин, почему так много библиотек в C до сих пор используют макросы вместо надлежащих функций — со всеми проблемами и головной болью, которые с ними связаны.
Прежде чем начнутся огненные войны: оба эти языка имеют свои сильные и слабые стороны, и оба имеют место в мире разработки программного обеспечения. Этот пост был написан только для того, чтобы открыть вам глаза на волшебство и чудеса, которые современные JVM делают каждый день.
Ссылка: | Код на C всегда работает намного быстрее, чем Java, верно? Неправильно! от нашего партнера JCG Андреаса Хауфлера в блоге Andy’s Software Engineering Corner . |