Статьи

Код на C всегда работает намного быстрее, чем Java, верно?

Таким образом, мы все знаем предубеждение, что 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 делают каждый день.