Почему сериализация json намного быстрее, чем сериализация yaml в Python?

У меня есть код, который сильно зависит от yaml для межязыковой сериализации, и, работая над ускорением некоторых вещей, я заметил, что yaml был безумно медленным по сравнению с другими методами сериализации (например, pickle, json).

Итак, что действительно поражает, так это то, что json намного быстрее, чем yaml, когда результат почти идентичен.

>>> import yaml, cjson; d={'foo': {'bar': 1}}
>>> yaml.dump(d, Dumper=yaml.SafeDumper)
'foo: {bar: 1}\n'
>>> cjson.encode(d)
'{"foo": {"bar": 1}}'
>>> import yaml, cjson;
>>> timeit("yaml.dump(d, Dumper=yaml.SafeDumper)", setup="import yaml; d={'foo': {'bar': 1}}", number=10000)
44.506911039352417
>>> timeit("yaml.dump(d, Dumper=yaml.CSafeDumper)", setup="import yaml; d={'foo': {'bar': 1}}", number=10000)
16.852826118469238
>>> timeit("cjson.encode(d)", setup="import cjson; d={'foo': {'bar': 1}}", number=10000)
0.073784112930297852

CSafeDumper и cjson в PyYaml написаны на C, поэтому не похоже, что это проблема скорости C и Python. Я даже добавил некоторые случайные данные, чтобы посмотреть, выполняет ли cjson какое-либо кэширование, но это все же намного быстрее, чем PyYaml. Я понимаю, что yaml - это расширенный набор json, но как может сериализатор yaml быть на 2 порядка медленнее при таком простом вводе?

5 ответов

Решение

Как правило, скорость анализа зависит не от сложности вывода, а от сложности принятого ввода. Грамматика JSON очень лаконична. Парсеры YAML сравнительно сложны, что приводит к увеличению накладных расходов.

Главная цель дизайна JSON - простота и универсальность. Таким образом, JSON является тривиальным для генерации и анализа за счет снижения читабельности. В нем также используется информационная модель с наименьшим общим знаменателем, обеспечивающая легкую обработку любых данных JSON любой современной средой программирования.

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

Я не являюсь разработчиком синтаксического анализатора YAML, поэтому я не могу говорить конкретно на порядки без каких-либо данных профилирования и большого количества примеров. В любом случае, обязательно проверяйте большое количество входных данных, прежде чем почувствовать уверенность в результатах тестов.

Обновите Ой, неправильно прочитал вопрос.:-(Сериализация все еще может быть невероятно быстрой, несмотря на большую входную грамматику, однако, просматривая исходный код, похоже, что сериализация PyYAML на уровне Python создает граф представления, тогда как simplejson кодирует встроенные типы данных Python непосредственно в текстовые блоки.

В приложениях, над которыми я работал, вывод типов между строками и числами (float/int) - это то место, где самые большие затраты на синтаксический анализ yaml связаны с тем, что строки можно писать без кавычек. Поскольку все строки в json заключены в кавычки, при синтаксическом анализе строк не происходит возврата. Отличным примером, где это может замедлиться, является значение 0000000000000000000s. Вы не можете сказать, что это значение является строкой, пока не прочитаете до конца.

Другие ответы верны, но это конкретная деталь, которую я обнаружил на практике.

Говоря об эффективности, какое-то время я использовал YAML, и меня привлекла простота, которую выполняют некоторые назначения имени / значения на этом языке. Тем не менее, в процессе, о котором я так часто и странно рассказывал, об одном из направлений YAML - тонких вариациях грамматики, которые позволяют писать особые случаи в более лаконичном стиле и тому подобное. В конце концов, хотя грамматика YAML почти наверняка формально последовательна, она оставила у меня определенное чувство "неопределенности". Затем я ограничил себя тем, чтобы не трогать существующий работающий код YAML и писать все новое в более окольном, отказоустойчивом синтаксисе, что заставило меня отказаться от всего YAML. В результате YAML пытается выглядеть как стандарт W3C и выпускает небольшую библиотеку трудно читаемой литературы, касающейся ее концепций и правил.

Я чувствую, что это намного больше интеллектуальных затрат, чем необходимо. Посмотрите на SGML/XML: разработанный IBM в бурные 60-е годы, стандартизированный ISO, известный (в упрощенном и измененном виде) как HTML для бесчисленных миллионов людей, документированный и задокументированный и вновь задокументированный во всем мире. Приходит маленький JSON и убивает этого дракона. Как JSON может стать настолько широко используемым за такое короткое время, имея всего один скудный веб-сайт (и javascript luminary для его поддержки)? Его простота, полное отсутствие сомнений в его грамматике, простота изучения и использования.

XML и YAML сложны для людей и для компьютеров. JSON довольно дружелюбен и прост как для людей, так и для компьютеров.

Беглый взгляд на python-yaml предполагает, что его дизайн намного сложнее, чем у cjson:

>>> dir(cjson)
['DecodeError', 'EncodeError', 'Error', '__doc__', '__file__', '__name__', '__package__', 
'__version__', 'decode', 'encode']

>>> dir(yaml)
['AliasEvent', 'AliasToken', 'AnchorToken', 'BaseDumper', 'BaseLoader', 'BlockEndToken',
 'BlockEntryToken', 'BlockMappingStartToken', 'BlockSequenceStartToken', 'CBaseDumper',
'CBaseLoader', 'CDumper', 'CLoader', 'CSafeDumper', 'CSafeLoader', 'CollectionEndEvent', 
'CollectionNode', 'CollectionStartEvent', 'DirectiveToken', 'DocumentEndEvent', 'DocumentEndToken', 
'DocumentStartEvent', 'DocumentStartToken', 'Dumper', 'Event', 'FlowEntryToken', 
'FlowMappingEndToken', 'FlowMappingStartToken', 'FlowSequenceEndToken', 'FlowSequenceStartToken', 
'KeyToken', 'Loader', 'MappingEndEvent', 'MappingNode', 'MappingStartEvent', 'Mark', 
'MarkedYAMLError', 'Node', 'NodeEvent', 'SafeDumper', 'SafeLoader', 'ScalarEvent', 
'ScalarNode', 'ScalarToken', 'SequenceEndEvent', 'SequenceNode', 'SequenceStartEvent', 
'StreamEndEvent', 'StreamEndToken', 'StreamStartEvent', 'StreamStartToken', 'TagToken', 
'Token', 'ValueToken', 'YAMLError', 'YAMLObject', 'YAMLObjectMetaclass', '__builtins__', 
'__doc__', '__file__', '__name__', '__package__', '__path__', '__version__', '__with_libyaml__', 
'add_constructor', 'add_implicit_resolver', 'add_multi_constructor', 'add_multi_representer', 
'add_path_resolver', 'add_representer', 'compose', 'compose_all', 'composer', 'constructor', 
'cyaml', 'dump', 'dump_all', 'dumper', 'emit', 'emitter', 'error', 'events', 'load', 
'load_all', 'loader', 'nodes', 'parse', 'parser', 'reader', 'representer', 'resolver', 
'safe_dump', 'safe_dump_all', 'safe_load', 'safe_load_all', 'scan', 'scanner', 'serialize', 
'serialize_all', 'serializer', 'tokens']

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

Несмотря на то, что у вас есть принятый ответ, к сожалению, он только делает некоторые шаги в направлении документации PyYAML и цитирует утверждение в этой документации, которое является неправильным: PyYAML не создает граф представления во время дампа, он создает поток lineair (и просто лайк json хранит корзину идентификаторов, чтобы увидеть, есть ли рекурсии).


Прежде всего вы должны понимать, что в то время как cjson dumper - это C-код, созданный вручную, CSafeDumper от YAML разделяет два из четырех этапов дампа (Representer а также Resolver) с обычным чистым Python SafeDumper и тем, что два других этапа (Serializer и Emitter) написаны не полностью вручную в C, а состоят из модуля Cython, который вызывает библиотеку C libyaml для излучения.


Помимо этой важной части, простой ответ на ваш вопрос, почему это занимает больше времени, заключается в том, что дамп YAML делает больше. Это не так много, потому что YAML сложнее, чем утверждает @flow, а потому, что это дополнительное, что может сделать YAML, делает его намного более мощным, чем JSON, а также более удобным для пользователя, если вам нужно обработать результат с помощью редактора. Это означает, что в библиотеке YAML тратится больше времени даже при применении этих дополнительных функций, а во многих случаях также просто проверяется, применяется ли что-то.

Вот пример: даже если вы никогда не проходили через код PyYAML, вы заметили, что дампер не цитирует foo а также bar, Это не потому, что эти строки являются ключами, поскольку в YAML нет ограничений, которые есть у JSON, ключ для отображения должен быть строкой. Например, строка Python, являющаяся значением в отображении, также может быть заключена в кавычки (т. Е. Просто).

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

Как PyYAML знает, когда заключать в кавычки строку, а когда нет? При выводе он фактически сначала выводит строку, а затем анализирует результат, чтобы убедиться, что когда он читает этот результат обратно, он получает исходное значение. И если это не так, то применяются кавычки.

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

он сбрасывает строку, а затем анализирует результат

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


В любом реальном приложении со сложными данными дампер / загрузчик на основе JSON слишком прост для непосредственного использования, и в вашей программе должно быть гораздо больше интеллекта, чем для выгрузки тех же сложных данных непосредственно в YAML. Упрощенный пример - когда вы хотите работать с метками даты и времени, в этом случае вам нужно преобразовать строку назад и вперед в datetime.datetime самостоятельно, если вы используете JSON. Во время загрузки вы должны сделать это либо на основании того факта, что это значение связано с каким-то (мы надеемся, узнаваемым) ключом:

{ "datetime": "2018-09-03 12:34:56" }

или с позицией в списке:

["FirstName", "Lastname", "1991-09-12 08:45:00"]

или на основе формата строки (например, с помощью регулярных выражений).

Во всех этих случаях в вашей программе необходимо выполнить гораздо больше работы. То же самое относится и к демпингу, и это означает не только дополнительное время разработки.

Давайте восстановим ваше время на том, что я получаю на моей машине, чтобы мы могли сравнить его с другими измерениями. Я немного переписал твой код, потому что он был неполным (timeit?) и импортировал другие вещи дважды. Также было невозможно просто вырезать и вставить из-за >>> подсказки.

from __future__ import print_function

import sys
import yaml
import cjson
from timeit import timeit

NR=10000
ds = "; d={'foo': {'bar': 1}}"
d = {'foo': {'bar': 1}}

print('yaml.SafeDumper:', end=' ')
yaml.dump(d, sys.stdout, Dumper=yaml.SafeDumper)
print('cjson.encode:   ', cjson.encode(d))
print()


res = timeit("yaml.dump(d, Dumper=yaml.SafeDumper)", setup="import yaml"+ds, number=NR)
print('yaml.SafeDumper ', res)
res = timeit("yaml.dump(d, Dumper=yaml.CSafeDumper)", setup="import yaml"+ds, number=NR)
print('yaml.CSafeDumper', res)
res = timeit("cjson.encode(d)", setup="import cjson"+ds, number=NR)
print('cjson.encode    ', res)

и это выводит:

yaml.SafeDumper: foo: {bar: 1}
cjson.encode:    {"foo": {"bar": 1}}

yaml.SafeDumper  3.06794905663
yaml.CSafeDumper 0.781533956528
cjson.encode     0.0133550167084

Теперь давайте дамп простой структуры данных, которая включает в себя datetime

import datetime
from collections import Mapping, Sequence  # python 2.7 has no .abc

d = {'foo': {'bar': datetime.datetime(1991, 9, 12, 8, 45, 0)}}

def stringify(x, key=None):
    # key parameter can be used to dump
    if isinstance(x, str):
       return x
    if isinstance(x, Mapping):
       res = {}
       for k, v in x.items():
           res[stringify(k, key=True)] = stringify(v)  # 
       return res
    if isinstance(x, Sequence):
        res = [stringify(k) for k in x]
        if key:
            res = repr(res)
        return res
    if isinstance(x, datetime.datetime):
        return x.isoformat(sep=' ')
    return repr(x)

print('yaml.CSafeDumper:', end=' ')
yaml.dump(d, sys.stdout, Dumper=yaml.CSafeDumper)
print('cjson.encode:    ', cjson.encode(stringify(d)))
print()

Это дает:

yaml.CSafeDumper: foo: {bar: '1991-09-12 08:45:00'}
cjson.encode:     {"foo": {"bar": "1991-09-12 08:45:00"}}

Для сроков вышесказанного я создал модуль myjson, который оборачивает cjson.encode и имеет выше stringify определены. Если вы используете это:

d = {'foo': {'bar': datetime.datetime(1991, 9, 12, 8, 45, 0)}}
ds = 'import datetime, myjson, yaml; d=' + repr(d)
res = timeit("yaml.dump(d, Dumper=yaml.CSafeDumper)", setup=ds, number=NR)
print('yaml.CSafeDumper', res)
res = timeit("myjson.encode(d)", setup=ds, number=NR)
print('cjson.encode    ', res)

давая:

yaml.CSafeDumper 0.813436031342
cjson.encode     0.151570081711

Этот по-прежнему довольно простой вывод уже возвращает вас от разницы в скорости на два порядка до менее чем на один порядок.


Простые скаляры YAML и форматирование блочного стиля обеспечивают лучшую читаемость данных. То, что у вас может быть запятая в последовательности (или отображение), делает меньше ошибок при ручном редактировании данных YAML, как с теми же данными в JSON.

Теги YAML позволяют в ваших данных указывать ваши (сложные) типы. При использовании JSON вы должны позаботиться в своем коде о чем-либо более сложном, чем отображения, последовательности, целые числа, числа с плавающей запятой, логические значения и строки. Такой код требует времени разработки и вряд ли будет так быстро python-cjson (Вы, конечно, также можете написать свой код на C.

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

Такая мощность и гибкость достигается за счет более низкой скорости. Если вы выбрасываете много простых вещей, JSON - лучший выбор, вы вряд ли будете редактировать результат вручную. Для всего, что связано с редактированием или сложными объектами, или и тем, и другим, вы все равно должны рассмотреть возможность использования YAML


Force Можно принудительно сбросить все строки Python как скаляры YAML с (двойными) кавычками, но установка стиля недостаточна для предотвращения повторного чтения.

Другие вопросы по тегам