Pull to refresh

Почему C быстрее Java (с точки зрения Java-разработчика)

Reading time 4 min
Views 7.8K
Original author: Шон Пирс
В листе рассылки Git развернулась дискуссия о том, как язык программирования высокого уровня снижает производительность приложения, в связи с обсуждением JGit. Дискуссия особенно интересна, потому что в ней принимали участие программисты, эксперты высочайшего уровня как в C, так и в Java. Один из них — Шон Пирс (Shawn O. Pearce), известный Java-программист из компании Google, активный коммитер в Eclipse, соавтор Git и автор Java-имплементации Git под названием JGit. В своём сообщении он назвал реальные ограничения, с которыми сталкивается высококвалифицированный разработчик, пытаясь написать эффективный Java-код, сравнимый по производительности с максимально оптимизированным кодом C. Хотя письмо датируется апрелем 2009 года, но некоторые аргументы Шона до сих пор не потеряли актуальность.

List: git
Subject: Re: Why Git is so fast (was: Re: Eric Sink's blog — notes on git,
From: «Shawn O. Pearce» <spearce () spearce! org>


Как было сказано ранее, мы сделали много маленьких оптимизаций в коде Git на C, чтобы добиться реально высокой производительности. 5% здесь, 10% там, и внезапно ты уже на 60% быстрее, чем был раньше. Нико [Питре], Линус [Торвальдс] и Джунио [Хамано] — все они потратили определённое время в последние три-четыре года для оптимизации отдельных фрагментов Git, исключительно для того, чтобы он работал максимально быстро.

Языки программирования высокого уровня в определённой степени скрывают машину, так что мы не можем проводить все эти оптимизации.

Например, JGit страдает от отсутствия mmap(), а при использовании Java NIO MappedByteBuffer, нам всё ещё нужно делать копию во временный массив byte[], чтобы получить возможность реальной обработки данных. В Git на C нет такого копирования. Конечно, в других языках высокого уровня метод mmap может быть поудобнее, но все они также склоняются к сборке мусора, и большинство языков пытаются связать управление mmap со сборщиком мусора «для безопасности и простоты».

JGit страдает также от отсутствия unsigned типов данных в Java. Есть много мест в JGit, где нам действительно нужен unsigned int32_t или unsigned long (машинное слово максимального размера) или unsigned char, но эти типы данных просто отсутствуют в Java. Преобразование байта в int, просто чтобы представить его как unsigned, требует дополнительной операции & 0xFF для обнуления sign extension.

JGit страдает от отсутствия эффективного способа представить SHA-1. В коде C можно просто написать unsigned char[20] и сразу скопировать строку в память к контейнеру. В Java byte[20] будет стоить дополнительно 16 байт памяти, и доступ к ним будет дольше, потому что сами эти байты находятся в другой области памяти от контейнера. Мы пробуем обойти это за счёт преобразования из byte[20] в пять int’ов, но это стоит дополнительных машинных инструкций.

Git на C принимает за данность, что операция memcpy(a, b, 20) предельно дёшева при копировании содержимого памяти из дерева (inflated tree) в объект структуры. В JGit приходится платить большой штраф за копирование этих 20 байтов в пять int’ов, потому что позже эти пять int'ов обходятся дешевле.

В других языках программирования высокого уровня тоже отсутствует возможность пометить тип как unsigned. Или заставляют платить похожие штрафы за хранение 20-байтного бинарного массива.

Нативные для Java коллекции (collection types) стали для нас настоящей ловушкой в JGit. Мы использовали типы java.util.* в удобных случаях, и вроде бы почти решили проблему со структурой данных, но они, как правило, работали гораздо хуже, чем запись специализированной структуры данных.

К примеру, у нас был ObjectIdSubclassMap для того, что должно было выглядеть как Map<ObjectId,Object>. Только он требовал, чтобы тип Object, который вы используете как «значение», происходил от ObjectId, поскольку данное представление объекта работает одновременно как ключ и как значение. Это вызывает настоящий кошмар при использовании на HashMap<ObjectId,Object>. (Если кто не знает, ObjectId — это JGit'овский unsigned char[20] для SHA-1).

Как раз пару дней назад я написал LongMap, более быстрый вариант HashMap<Long,Object>, для хэширования объектов по индексам в упакованном файле. Здесь то же самое, стоимость упаковки в Java для конвертации long (самого большого целого) в объект, пригодный для стандартного HashMap типа, была довольно высока.

И сейчас JGit по-прежнему работает медленнее, когда речь идёт об обработке коммита или объекта дерева, где нужно следить за связями объекта (object links). Или когда происходит вызов inflate(). Мы тратим гораздо больше времени на эти процедуры, чем делает git на C, хотя мы пытаемся спуститься как можно на более низкий уровень, насколько вообще позволяет byte[], избегая копирования чего бы то ни было и избегая выделения памяти, когда только возможно.

Что характерно, JGit выполняет операцию rev-list --objects –all примерно вдвое дольше, чем это делает Git, на проекте вроде ядра Linux, а index-pack для файла размером около 270 МБ тоже длится примерно вдвое дольше.

Обе части JGit настолько хороши, насколько у меня хватает знаний для их оптимизации, но мы реально находимся во власти JIT, и любые изменения в JIT могут привести к ухудшению (или улучшению) наших показателей. В отличие от Git на C, где Линус Торвальдс может менять целые фрагменты кода на ассемблере и пробовать разные подходы.

Так что да, есть практический смысл в создании Git на языке высокого уровня, но вы просто не сможете получить там такую же производительность или строгий расход памяти, как у Git на C. Вот чего вам стоят абстракции высокоуровневого языка. Однако, JGit работает вполне нормально; достаточно быстро для того, чтобы мы использовали его как как сервер git внутри Google.

P.S. Сообщение Шона Пирса написано в 2009 году и автор не учитывает изменений, сделанных в Java 1.7. Например, Java сейчас использует escape analysis, чтобы избежать резервирования памяти в куче, когда это возможно.
Tags:
Hubs:
+61
Comments 138
Comments Comments 138

Articles