Jak to jest, że serializacja json jest o wiele szybsza niż serializacja yaml w Pythonie?

Mam kod, który w dużej mierze opiera się na yaml dla serializacji między językami i podczas pracy nad przyspieszeniem niektórych rzeczy zauważyłem, że yaml był szalenie powolny w porównaniu do innych metod serializacji (np. pickle, json).

Więc to, co naprawdę rozwala mój umysł, to to, że json jest o wiele szybszy niż yaml, gdy wyjście jest prawie identyczne.

>>> 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

PyYaml ' s CSafeDumper i cjson są zarówno napisane w języku C, więc to nie jest tak, że jest to kwestia prędkości C vs Python. Dodałem nawet niektóre losowe DANE do niego, aby zobaczyć, czy cjson robi jakieś buforowanie, ale nadal jest znacznie szybszy niż PyYaml. Zdaję sobie sprawę, że yaml jest supersetem json, ale jak serializator yaml może być o 2 rzędy wielkości wolniejszy przy tak prostym wejściu?

Author: Martin Thoma, 2010-03-16

5 answers

Ogólnie rzecz biorąc, to nie złożoność wyjścia decyduje o szybkości parsowania, ale złożoność akceptowanego wejścia. Gramatyka JSON jest bardzo zwięzła . Parsery YAML są stosunkowo złożone, co prowadzi do wzrostu kosztów ogólnych.

Głównym celem projektu JSON jest prostota i uniwersalność. Tak więc, JSON jest trywialny do generowania i analizowania, kosztem zredukowanej liczby ludzi czytelność. Wykorzystuje również najniższy wspólny mianownik informacje model, zapewnienie, że dowolne dane JSON mogą być łatwo przetwarzane przez każde nowoczesne programowanie środowisko.

Dla kontrastu najważniejsza konstrukcja YAML cele to czytelność człowieka i obsługa serializacji dowolnych natywne struktury danych. Tak więc YAML pozwala na niezwykle czytelne pliki, ale jest bardziej złożone do generowania i parse. Ponadto YAML ventures poza NAJNIŻSZYM wspólnym mianownikiem typy danych, wymagające bardziej złożonych przetwarzanie podczas przechodzenia między różne Środowiska programistyczne.

Nie jestem implementatorem parsera YAML, więc nie mogę mówić konkretnie o rzędach wielkości bez pewnych danych profilujących i dużej ilości przykładów. W każdym razie należy przetestować dużą liczbę wejść, zanim poczuje się pewnie w liczbach wzorcowych.

Update UPS, źle odczytałem pytanie. :- (Serializacja nadal może być niesamowicie szybka pomimo dużej gramatyki wejściowej; jednak przeglądając źródło, wygląda to jak PyYAML Serializacja na poziomie Pythona tworzy wykres reprezentacji , podczas gdy simplejson koduje wbudowane typy danych Pythona bezpośrednio do fragmentów tekstu.

 66
Author: cdleary,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-03-16 16:58:47

W aplikacjach, nad którymi pracowałem, wnioskowanie typu między łańcuchami znaków a liczbami (float/int) jest tam, gdzie największy narzut jest do parsowania yaml, ponieważ łańcuchy mogą być pisane bez cudzysłowów. Ponieważ wszystkie łańcuchy w json są w cudzysłowach, nie ma cofania podczas parsowania łańcuchów. Świetnym przykładem, gdzie to spowolni, jest wartość 000000000000000000s. nie możesz powiedzieć, że ta wartość jest ciągiem znaków, dopóki nie przeczytasz go do końca.

Inne odpowiedzi są poprawne, ale to jest konkretny szczegół, który odkryłem w praktyce.

 32
Author: twosnac,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-02-17 16:25:50

Mówiąc o wydajności, używałem YAML przez pewien czas i czułem się przyciągnięty prostotą, jaką niektóre nazwy/wartości przyjmują w tym języku. Jednak w tym procesie tak i tak często potknąłem się o jednej z finezji Yamla, subtelnych wariacji gramatyki, które pozwalają pisać specjalne przypadki w bardziej zwięzłym stylu i tym podobne. W końcu, chociaż gramatyka Yamla jest prawie na pewno formalnie spójna, pozostawiła mnie z pewnym poczuciem "niejasności". Wtedy ograniczyłem się do nie dotykać istniejącego, działającego kodu YAML i pisać wszystkiego nowego w bardziej okrężnej, niezawodnej składni-co zmusiło mnie do porzucenia całego YAML. Rezultatem jest to, że YAML stara się wyglądać jak standard W3C i tworzy małą bibliotekę trudnej do odczytania literatury dotyczącej jego pojęć i zasad.

To, jak sądzę, jest o wiele bardziej intelektualne niż potrzeba. Spójrz na SGML / XML: opracowany przez IBM w latach 60-tych, standaryzowany przez ISO, znany (w formie dumbed-down i zmodyfikowanej) jako HTML do niezliczone miliony ludzi, udokumentowane i udokumentowane ponownie na całym świecie. Podchodzi mały JSON i zabija smoka. Jak JSON mógł być tak szeroko stosowany w tak krótkim czasie, z tylko jedną skromną stroną internetową (i wsparciem dla javascript)? Jest w swojej prostocie, braku wątpliwości w gramatyce, łatwości uczenia się i korzystania z niej.

XML i YAML są trudne dla ludzi i są trudne dla komputerów. JSON jest dość przyjazny i łatwy zarówno dla ludzi, jak i Komputery.

 22
Author: flow,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-07-15 09:26:36

Pobieżne spojrzenie na Pythona-yaml sugeruje, że jego konstrukcja jest znacznie bardziej złożona niż cjsona:

>>> 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']

Bardziej złożone projekty prawie zawsze oznaczają wolniejsze projekty, a to jest o wiele bardziej złożone, niż większość ludzi kiedykolwiek będzie potrzebować.

 12
Author: Glenn Maynard,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-03-16 07:07:52

Chociaż masz akceptowaną odpowiedź, niestety, że tylko trochę ręcznego oszczędzania w kierunku dokumentacji PyYAML i cytuje stwierdzenie w tej dokumentacji, które nie jest poprawne: PyYAML czy Nie tworzy wykres reprezentacji podczas zrzutu, tworzy lineair stream (i tak jak json trzyma wiadro identyfikatorów, aby sprawdzić, czy są rekursje).


Po pierwsze musisz zdać sobie sprawę, że podczas gdy cjson wywrotka jest ręcznie robiony tylko kod C, YAML ' s CSafeDumper dzieli dwa z czterech etapów zrzutu (Representer i Resolver) z normalnym czystym Pythonem SafeDumper i że pozostałe dwa etapy (Serializer i emiter) nie są napisany całkowicie ręcznie w C, ale składa się z modułu Cython która wywołuje bibliotekę C libyaml do emisji.


Oprócz tej znaczącej części, prosta odpowiedź na twoje pytanie dlaczego trwa to dłużej, jest to, że dumping YAML robi więcej. To nie jest tak dużo, bo YAML jest trudniejszy jak twierdzi @ flow, ale ponieważ ten dodatkowy to YAML może zrobić, sprawia, że jest o wiele mocniejszy niż JSON, a także więcej przyjazny dla użytkownika, jeśli chcesz przetworzyć wynik za pomocą edytora. Że oznacza to, że więcej czasu spędzamy w bibliotece YAML nawet przy stosowaniu tych dodatkowych funkcji, a w wielu przypadkach również po prostu sprawdzanie, czy coś ma zastosowanie.

Oto przykład: nawet jeśli nigdy nie przeszedłeś przez PyYAML kod, zauważyłeś, że wywrotka nie cytuje foo i bar. To nie dlatego, że te struny są klucze, tak jak YAML nie mieć ograniczenie jakie ma JSON, że klucz do mapowania musi bądź sznurkiem. Np. ciąg Pythona, który jest wartością w mapowaniu Może również być nienotowane (tj. zwykły).

Nacisk położony jest na Może , ponieważ nie zawsze tak jest. Take for instancja ciągu, który składa się tylko ze znaków liczbowych: 12345678. Trzeba to napisać cytatami, bo inaczej to wyglądałaby dokładnie jak liczba (i odczytywana jako taka podczas parsowania).

Skąd PyYAML wie, kiedy zacytować łańcuch, a kiedy nie? W sprawie dumpingu w rzeczywistości najpierw zrzuca łańcuch, a następnie parsuje wynik, aby oczywiście, że kiedy odczytuje ten wynik z powrotem, dostaje oryginalną wartość. A jeśli okaże się, że tak nie jest, stosuje się cytaty.

Powtórzę jeszcze raz ważną część poprzedniego zdania, więc nie musisz tego ponownie czytać:

Zrzuca łańcuch, a następnie parsuje wynik

Oznacza to, że stosuje się wszystkie regex pasujące to robi, gdy Ładowanie, aby sprawdzić, czy wynikowy Skalar załaduje się jako liczba całkowita, float, boolean, datetime itp., aby ustalić, czy cytaty muszą być stosowane czy nie.¹


W każdej rzeczywistej aplikacji ze złożonymi danymi, oparty na JSON wywrotka / ładowarka jest zbyt prosta w użyciu bezpośrednio i wiele więcej inteligencja musi być w twoim programie w porównaniu do tego samego złożone dane bezpośrednio do YAML. Uproszczonym przykładem jest, gdy chcesz pracować z datownikiem, w w tym przypadku musisz przekonwertować łańcuch z powrotem i dalej do datetime.datetime siebie, jeśli używasz JSON. Podczas ładowania trzeba to zrobić albo w oparciu o fakt, że jest to wartość związane z jakimś (miejmy nadzieję rozpoznawalnym) kluczem:

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

Lub z pozycją na liście:

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

Lub na podstawie formatu ciągu znaków (np. za pomocą regex).

We wszystkich tych przypadkach dużo więcej pracy musi być wykonane w programie. To samo chwyta za dumping i to nie tylko oznacza dodatkowy czas rozwoju.

Pozwala zregenerować czas z tym, co dostaję na mojej maszynie możemy więc porównać je z innymi pomiarami. Przepisałem Twój kod nieco, bo był niekompletny (timeit?) i importowane inne dwa razy. Nie można było również po prostu wyciąć i wkleić z powodu podpowiedzi >>>.

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)

A to wychodzi:

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

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

Teraz pozwala zrzut prostej struktury danych, która zawiera 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()

Daje to:

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

Dla czasu powyższego stworzyłem moduł myjson, który owija cjson.encode i ma zdefiniowaną powyższą stringify. Jeśli używasz tego:

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)

Dając:

yaml.CSafeDumper 0.813436031342
cjson.encode     0.151570081711

To wciąż dość proste wyjście, już sprowadza cię z dwóch rozkazów o różnicy wielkości prędkości do mniej niż jednego rzędu wielkości.


Proste Skalary Yamla i formatowanie w stylu blokowym sprawiają, że dane są lepiej czytelne. Że można mieć końcowy przecinek w sekwencji (lub mapowanie) sprawia, że mniej awarie podczas ręcznej edycji danych YAML, tak jak w przypadku tych samych danych w JSON.

Znaczniki YAML pozwalają na in-data wskazanie Twoich (złożonych) typów. Kiedy używając JSON musisz zadbać w swoim kodzie o wszystko więcej złożone niż mapowania, sekwencje, liczby całkowite, pływaki, booleany i struny. Taki kod wymaga czasu rozwoju i jest mało prawdopodobne, aby był tak szybko jak python-cjson (oczywiście możesz napisać swój kod również w C.

Wyrzucanie niektórych danych, np. rekurencyjnych struktur danych (np. topologiczne data), lub złożone klucze są predefiniowane w Bibliotece PyYAML. Tam Biblioteka JSON tylko błędy, i wdrożyć obejście dla tego jest nietrywialne i najprawdopodobniej spowalnia rzeczy, które różnice prędkości są mniej istotne.

[23]}taka moc i elastyczność jest w cenie niższej prędkości. Kiedy wyrzucanie wielu prostych rzeczy JSON jest lepszym wyborem, jest mało prawdopodobne i tak będę edytować wynik ręcznie. Dla każdego, kto wymaga edycji lub złożonych obiektów lub obu, ty należy jeszcze rozważyć użycie YAML.

1 można wymusić dumping wszystkich łańcuchów Pythona jako YAML Skalary z (podwójnymi) cudzysłowami, ale ustawienie stylu nie wystarczy, aby zapobiegaj wszystkim powrotom.

 8
Author: Anthon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-09-08 03:47:41