У XML появился конкурент

26 август, 2008 - 09:57Виктор Вейтман

Вряд ли кто-то в состоянии назвать хотя бы приблизительное число языков, предназначенных для описания данных. По крайней мере, можно с уверенностью сказать, что таковых немало. В свое время создание еще одного, Protocol Buffers, не только не стало сенсацией, но и вовсе прошло незамеченным. И даже открытие доступа к исходным кодам этого проекта (формировавшегося в недрах Google) тоже было воспринято как рядовое событие. Шум поднялся лишь после заявления специалистов о том, что Protocol Buffers способен заменить язык XML или хотя бы существенно потеснить его позиции.

За десять лет, прошедшие с того момента, как консорциум W3C утвердил первую версию спецификации eXtensible Markup Language, язык XML прочно завоевал репутацию одного из краеугольных камней современных информационных технологий. Он весьма успешно используется для публикации в Веб, применяется в конфигурационных файлах и офисных документах, но основным его назначением, конечно же, является кросс-платформенная передача данных. Сегодня идея заменить XML чем-то другим может показаться посягательством чуть ли не на сами принципы фон Неймана. Но для того чтобы реально оценить перспективы языка Protocol Buffers в качестве конкурента XML, надо хотя бы в общих чертах разобраться в его возможностях. Конечно, в рамках небольшой статьи абсолютно нереально не только полностью описать язык, но даже более или менее детально познакомить читателя с его основными характеристиками. Поэтому мы вынуждены ограничиться скорее намеками, которые, однако, позволяют составить приблизительное представление о «соотношении сил» между XML и Protocol Buffers.

Неожиданный знак равенства

Если полное описание самого языка XML можно уместить в одном небольшом абзаце и успеть ознакомиться с ним меньше чем за пять минут (сложности начнутся потом, когда придется разбираться с XSchema, XPath, XPoint и т. д.), то с Protocol Buffers дело обстоит по-другому. Прежде чем описать хотя бы небольшой фрагмент данных, нужно потратить некоторое время на изучение этого языка. Поэтому давайте по порядку.

Предположим, что в нашем распоряжении есть XML-документ, описывающий возраст, рост и вес человека:

<person>
<name>Иванов</name>
<age>40</age>
<height>180</height>
<weight>75</weight>
<person>

Оставим на долю Иванова радоваться, что в свои сорок лет он пребывает в идеальной форме, а сами переведем ту же информацию в формат Protocol Buffers:

message Person {
required string name = 1;
optional int32 age = 2;
optional int32 height = 3;
optional int32 weight = 4;
}

Уже при первом знакомстве с Protocol Buffers в глаза бросается его крупный недостаток – формат представления данных никак нель-зя назвать интуитивно понятным. Действительно, у любого программиста сразу же возникает ассоциация с инициализацией переменных. И если слишком малые значения age, height и weight лишь вызывают недоумение, то выражение name = 1 кажется явной ошибкой! Однако ее здесь нет. Чтобы использовать Protocol Buffers, надо привыкнуть к тому, что после знака равенства задаются не непосредственные значения элементов, а числовые дескрипторы, призванные идентифицировать их в дальнейшем. Сами же значения появятся позднее, уже после того, как описание данных будет преобразовано в текст программы на целевом языке.

Короче невозможно

Итак, недоразумение с дескрипторами, слишком похожими на значения, вроде бы разъяснено, но пока совершенно непонятно, какие преимущества такой подход дает Protocol Buffers. Впрочем, и не удивительно – ведь заключаются они вовсе не в самом синтаксисе.

Если в XML элемент <age>40</age> определяет имя и соответствующее ему значение и в неизменном виде помещается в файл или передается по сети, то в Protocol Buffers строка optional int32 age = 2 играет лишь роль своеобразного шаблона. Реальные данные записываются гораздо компактнее, к примеру, если в нашем случае элементу age присваивается значение 40, то его двоичное представление займет в памяти всего два байта: 16 40

Согласитесь, существенная экономия по сравнению с тринадцатью символами, которыми те же данные представляются в формате XML. Очевидно, что второй байт содержит собственно значение. В первом же находятся номер дескриптора (именно стоящий справа от знака равенства) и тип элемента. Несмотря на то что в Protocol Buffers определено большое количество типов данных, они удачно объединены в группы и для их кодирования достаточно трех битов. Поэтому значение дескриптора сдвигается влево на три позиции и полученная величина (в данном случае 16) складывается с кодом типа (который для int32, как и для ряда других целочисленных типов, равен нулю).

Казалось бы, присвоив элементу height значение 180, мы также сможем записать его в виде двух байтов, однако на этот раз ситуация не столь проста, и чтобы разобраться в ней, надо сперва рассмотреть формат varint.

Жертва бита во имя оптимизации

Сколько битов достаточно для представления целого числа? Шестнадцать? Тридцать два? Шестьдесят четыре? Удачный ответ на этот вопрос нашли создатели Protocol Buffers. Они предложили выделять ровно столько битов, сколько необходимо. Ну или, может быть, чуть больше, но совсем ненамного. Для этой цели они разработали специальный формат под названием varint, согласно которому в каждом байте 7 бит выделяется для хранения значения, а восьмой сообщает о том, поместилось ли оно. То есть если восьмой бит сброшен (равен нулю), то это означает, что данный байт – последний в цепочке, представляющей значение элемента. И наоборот, единица в восьмом бите говорит о том, что для кодирования значения пришлось задействовать и следующий байт.

Так как число 40, присвоенное ранее age, помещается в семи битах, то старший бит этого байта попросту сбрасывается. Значение же 180 элемента height (двоичный код 10110100) поместится только в двух байтах. Первый по случайному совпадению будет равен 180, но на самом деле представление данных им не исчерпывается. Значащих битов в нем только семь (т. е. 0110100, или 52), а старший разряд переносится в следующий байт. Таким образом, состояние элемента height описывается тремя байтами, а именно 24 180 1

Если бы вместо возраста пришлось хранить год рождения, то, к примеру, значение 1968 было бы записано байтами 176 и 15, и, естественно, перед ними еще был бы помещен байт, содержащий дескриптор и тип данных.

Числовой дескриптор, идентифицирующий элемент, также представляется в формате varint. Учитывая сдвиг влево на три разряда, нетрудно заметить, что дескрипторы от 1 до 15 могут быть представлены одним байтом, а для больших значений приходится использовать дополнительный. Таким образом, значения из первой группы целесообразно применять для наиболее востребованных элементов. Вероятно, именно поэтому в Protocol Buffers дескрипторы задаются вручную, а не генерируются автоматически.

Библиотека on the fly

Каждый, кому приходилось работать с XML-файлами, конечно же, знаком с основными средствами их формирования и разбора. Независимо от того, надо ли строить XML-дерево с помощью DOM или фиксировать события, возникающие при потоковой обработке данных, посредством SAX, в любом случае приходится обращаться к соответствующим библиотекам, которыми снабжен практически каждый серьезный язык программирования. Но где же искать библиотеки для Protocol Buffers? К счастью, об этом позаботились специалисты Google, и их решение нельзя не признать элегантным.

Создав описание данных, подобное приведенному выше, его надо скомпилировать с помощью программы protoc.exe, на выходе которой и будет получена библиотека, предназначенная для работы с ними. В нее помещается структура, определяющая соответствие между именами элементов и числовыми дескрипторами, а также формируется отдельный класс, содержащий методы доступа к конкретным элементам. Так, для приведенного выше документа будет создан класс Person. Поскольку в нем среди прочих определены имена name и age, то в Java-версии библиотеки, сформированной на его основе, будут присутствовать методы getName(), setName(), getAge() и setAge(), а в библиотеке для C++ соответственно – name(), set_name(), age() и set_age(). Именно с помощью set-методов и устанавливаются значения элементов.

В классе определяются и методы для сериализации элементов (в данном случае под этим понимается генерация последовательности байтов, представляющих числовой дескриптор, тип и значение) и разбора данных, полученных из потока, – перевода их в формат, пригодный для дальнейшей работы.

Придется ли XML потесниться?

Скажем сразу, появившиеся в Интернете броские заголовки статей, гласящие о том, что язык Protocol Buffers призван заменить XML, следует воспринимать крайне скептически. Разумеется, в задачах, предполагающих передачу больших объемов информации, Protocol Buffers должен быть эффективнее XML, но в ущерб наглядности и легкости восприятия. На малых же объемах его преимущества сомнительны. Например, вряд ли кому-то придет в голову переводить в формат Protocol Buffers конфигурационный файл приложения.

Как утверждают специалисты Google, Protocol Buffers по сравнению с XML позволит в 3–10 раз уменьшить объем памяти, необходимой для хранения данных, и в 20–100 раз увеличить скорость их обработки. Реальность этих цифр можно легко оценить, сравнивая способы представления информации в XML и Protocol Buffers.

Очевидно, что наибольшую экономию памяти дает замена символьных имен XML-элементов числовыми дескрипторами. Конечно, ограничиться однобайтовыми дескрипторами трудно, 15 элементов – слишком мало для реальных приложений, но два бай-та (2047 вариантов) – уже вполне допустимая цифра. Заменив двумя байтами символьное имя, действительно можно добиться сокращения объема памяти. Но вот насколько – зависит от самих имен. Если отдавать предпочтение развернутым XML-дескрипторам наподобие <number_of_years_that_person_lives>, то экономия будет существенной, а компактным вроде <age> – то не слишком. Если же разработчик сознательно формирует часто повторяющиеся имена так, чтобы документ занимал как можно меньше места (а именно так поступают создатели промышленных форматов на основе XML, к примеру, в Office Open XML для текстовых фрагментов используется <w:t>), то преимущество Protocol Buffers перед XML будет незначительным.

Возможности экономии памяти при представлении значений элементов также весьма ограничены. Максимальное (трехкратное) сокращение объема дадут целые числа в интервале от 100 до 127. Любые другие значения, которые можно представить в формате varint, «сжимаются» хуже, числа фиксированного размера (fixed32, fixed64 и др.) – совсем плохо, а текстовые данные – и вовсе никак. Кстати, сами создатели Protocol Buffers признают, что для чисто текстовой информации формат XML предпочтительнее.

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

На сегодняшний день компилятор Protocol Buffers поддерживает только С++, Java и Python. Очевидно, что на данном этапе выбор целевых языков диктовался исключительно интересами Google (которая уже активно использует данный формат), но расширение их списка не вызовет принципиальных затруднений. Однако произойдет это только в том случае, если Protocol Buffers сумеет выйти за пределы одной (пусть и довольно большой) компании и будет признан во всем мире.

Итак, в настоящее время XML вне опасности. Язык Protocol Buffers, скорее всего, останется нишевым, ориентированным на специфические задачи, предполагающие обработку больших объемов данных. Но сам факт того, что кто-то допускает возможность замены XML, должен стать поводом для серьезных размышлений. Вероятно, XML действительно требует дальнейшего развития. И, в первую очередь, именно в вопросах обеспечения компактного хранения данных и эффективной их обработки.