В листе рассылки Git развернулась дискуссия о том, как язык программирования высокого уровня снижает производительность приложения, в связи с обсуждением JGit. Дискуссия особенно интересна, потому что в ней принимали участие программисты, эксперты высочайшего уровня как в C, так и в Java. Один из них — Шон Пирс (Shawn O. Pearce), известный Java-программист из компании Google, активный коммитер в Eclipse, соавтор Git и автор Java-имплементации Git под названием JGit. В своём сообщении он назвал реальные ограничения, с которыми сталкивается высококвалифицированный разработчик, пытаясь написать эффективный Java-код, сравнимый по производительности с максимально оптимизированным кодом C. Хотя письмо датируется апрелем 2009 года, но некоторые аргументы Шона до сих пор не потеряли актуальность.
P.S. Сообщение Шона Пирса написано в 2009 году и автор не учитывает изменений, сделанных в Java 1.7. Например, Java сейчас использует escape analysis, чтобы избежать резервирования памяти в куче, когда это возможно.
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]
и сразу скопировать строку в память к контейнеру. В Javabyte[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, чтобы избежать резервирования памяти в куче, когда это возможно.