web-dev-qa-db-ja.com

Pythonでのjsonシリアライゼーションがyamlシリアライゼーションよりもはるかに速いのはどうしてですか?

言語をまたがるシリアル化を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

PyYamlのCSafeDumperとcjsonはどちらもCで記述されているため、これはC vs Pythonの速度の問題ではないようです。 cjsonがキャッシュを実行しているかどうかを確認するために、ランダムデータを追加することさえしましたが、それでもPyYamlよりもはるかに高速です。私はyamlがjsonのスーパーセットであることを理解していますが、このような単純な入力を使用すると、yamlシリアライザーを2桁遅くすることができますか?

58
guidoism

一般に、解析の速度を決定するのは出力の複雑さではなく、受け入れられる入力の複雑さです。 JSON文法は 非常に簡潔 です。 YAMLパーサーは 比較的複雑 であり、オーバーヘッドが増加します。

JSONの最も重要な設計目標は、シンプルさと普遍性です。したがって、JSONの生成と解析は簡単ですが、人間の可読性が低下します。また、最も一般的な分母情報モデルを使用して、あらゆるJSONデータをすべての最新のプログラミング環境で簡単に処理できるようにします。

対照的に、YAMLの最も重要な設計目標は、人間による読みやすさと任意のネイティブデータ構造のシリアル化のサポートです。したがって、YAMLは非常に読みやすいファイルを許可しますが、生成と解析がより複雑になります。さらに、YAMLは、共通の最低限のデータ型を超えて、さまざまなプログラミング環境間を行き来する際により複雑な処理を必要とします。

私はYAMLパーサーの実装者ではないので、プロファイリングデータと例の大きなコーパスがないと、桁違いに具体的に話すことはできません。いずれの場合でも、ベンチマークの数値に自信を感じる前に、大量の入力をテストしてください。

更新おっと、質問を読み間違えました。 : 大量の入力文法にもかかわらず、シリアライゼーションは非常に高速ですが、ソースを参照すると、PyYAMLのPythonレベルのシリアライゼーションのように見えます---(表現グラフを作成します 一方で、simplejsonは組み込みPythonデータ型を直接テキストチャンクに入れます。

58
cdleary

私が取り組んだアプリケーションでは、文字列から数値への型推論(float/int)は、引用符なしで文字列を書き込むことができるため、yamlを解析するための最大のオーバーヘッドです。 jsonのすべての文字列は引用符で囲まれているため、文字列を解析するときにバックトラックはありません。これが遅くなる良い例は、値0000000000000000000sです。この値が文字列であることは、最後まで読み取るまでわかりません。

他の答えは正しいですが、これは実際に発見した特定の詳細です。

28
twosnac

効率性について言えば、私はしばらくYAMLを使用していましたが、この言語では名前と値の割り当てが簡単に行えることに魅力を感じました。ただし、その過程で、YAMLの巧妙さの1つについて何度もトリップしました。文法に微妙なバリエーションがあり、より簡潔なスタイルで特別なケースを記述できるようになっています。結局、YAMLの文法はほぼ一定の形式的に一貫していますが、私には「曖昧さ」のある感覚が残りました。次に、既存の機能しているYAMLコードに触れないように制限し、すべてを新しいラウンドアバウトでフェイルセーフな構文で記述しました。これにより、YAMLをすべて放棄しました。結局のところ、YAMLはW3C標準のように見えようとし、その概念と規則に関する読みにくい文献の小さなライブラリを作成します。

これは、私が思うに、必要以上にはるかに多くの知的オーバーヘッドです。 SGML/XMLを見てください。60年代にIBMによって開発され、ISOによって標準化され、数え切れないほどの何百万もの人々がHTMLとして認識し、(簡略化および変更された形式で)HTMLとして知られ、世界中で文書化および文書化されています。小さなJSONが表示され、そのドラゴンを倒します。 JSONがわずかなWebサイト(およびそれを裏付けるJavaScriptの発光体)だけで、どうしてこんなに短時間でそれほど広く使われるようになったのでしょうか。それは、その単純さ、文法に疑いがないこと、習得と使用が容易なことです。

XMLとYAMLは人間にとって難しいものであり、コンピューターにとっては難しいものです。 JSONは非常に友好的で、人間にもコンピューターにも簡単です。

20
flow

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

より複雑な設計は、ほぼ常に遅い設計を意味し、これはほとんどの人が必要とするよりもはるかに複雑です。

12
Glenn Maynard

受け入れられた回答がありますが、残念ながら、PyYAMLドキュメントの方向にいくつかの手振りを行っており、そのドキュメント内の正しくないステートメントを引用しています。PyYAMLはnotを実行して、ダンプ中に表現グラフを作成します。 lineairストリームを作成します(jsonと同様に、IDのバケットを保持して再帰があるかどうかを確認します)。


まず最初に、cjsonダンパーは手作りのCコードのみですが、YAMLのCSafeDumperは4つのダンプステージのうち2つ(RepresenterResolver)を通常の純粋なPython SafeDumperであり、他の2つのステージ(シリアライザとエミッタ)は完全にCで手作りされたものではなく、Cライブラリlibyamlを呼び出すCythonモジュールで構成されています。放出するため。


その重要な部分は別として、なぜ長くかかるのかという質問に対する簡単な答えは、YAMLをダンプするとより多くのことができるということです。 YAMLは@flowの主張のように難しいので、これはそれほどではありませんが、YAMLが実行できる追加の機能により、JSONよりもはるかに強力になり、エディターで結果を処理する必要がある場合はユーザーフレンドリーになります。つまり、これらの追加機能を適用する場合でも、YAMLライブラリで多くの時間が費やされ、多くの場合、何かが適用されるかどうかを確認するだけです。

次に例を示します。PyYAMLコードを一度も実行したことがなくても、ダンパーがfoobarを引用していないことに気付くでしょう。 YAMLにはJSONのような制限がないため、これらの文字列はキーであるため、マッピングのキーは文字列である必要があります。例えば。 a Python文字列canも引用符で囲まれていません(つまり、プレーン)。

canに重点が置かれています。たとえば、数字のみで構成される文字列12345678を考えます。これは引用符で書き出す必要があります。そうしないと、数字のように見えます(解析時にそのように読み戻すことができます)。

PyYAMLは文字列を引用するタイミングとしないタイミングをどのようにして知るのですか?ダンプすると、実際には最初に文字列がダンプされ、次に結果が解析されて、その結果が読み取られたときに元の値が取得されることを確認します。そして、それが当てはまらないことが判明した場合は、引用符が適用されます。

前の文の重要な部分をもう一度繰り返しますので、もう一度読む必要はありません。

文字列をダンプし、結果を解析します

これは、読み込み時に一致するすべての正規表現を適用して、結果のスカラーが整数、浮動小数点数、ブール値、日時などとして読み込まれるかどうかを確認し、引用符を適用する必要があるかどうかを判断することを意味します。


複雑なデータを使用する実際のアプリケーションでは、JSONベースのダンパー/ローダーは直接使用するには単純すぎるため、同じ複雑なデータをYAMLに直接ダンプする場合と比較して、プログラムに多くのインテリジェンスを組み込む必要があります。簡単な例は、日付と時刻のスタンプを操作する場合です。その場合、JSONを使用している場合は、文字列を自分でdatetime.datetimeに変換する必要があります。ロード中に、これがいくつかの(うまくいけば認識できる)キーに関連付けられた値であるという事実に基づいて、それを行う必要があります。

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

またはリスト内の位置:

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

または文字列の形式に基づいて(たとえば、正規表現を使用して)。

これらすべてのケースで、プログラムで実行する必要がある作業はさらに多くなります。ダンプについても同じことが言え、それは追加の開発時間を意味するだけではありません。

私が私のマシンで得たものであなたのタイミングを再生成して、それらを他の測定値と比較できるようにしましょう。コードが不完全だった(timeit?)ため、コードを多少書き直し、他のものを2回インポートしました。 >>>プロンプトのため、カットアンドペーストすることも不可能でした。

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

上記のタイミングのために、cjson.encodeをラップし、上記のstringifyが定義されているモジュールmyjsonを作成しました。それを使用する場合:

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

それでもかなり単純な出力ですが、速度の2桁の差から1桁未満の差にすでに戻っています。


YAMLのプレーンスカラーとブロックスタイルの書式設定により、データが読みやすくなります。シーケンス(またはマッピング)に末尾のコンマを含めることができるため、JSONの同じデータと同じようにYAMLデータを手動で編集するときの失敗が少なくなります。

YAMLタグは、(複雑な)タイプのデータ内表示を可能にします。 JSONを使用する場合youは、コード内で、マッピング、シーケンス、整数、浮動小数点数、ブール値、および文字列よりも複雑なものに注意する必要があります。そのようなコードは開発時間を必要とし、python-cjsonほど速くなることはほとんどありません(もちろん、コードをCで自由に書くこともできます)。

再帰的なデータ構造(トポロジデータなど)や複雑なキーなどの一部のデータのダンプは、PyYAMLライブラリで事前定義されています。そこでJSONライブラリはエラーを出し、そのための回避策を実装することは簡単ではなく、速度の違いがあまり重要ではないため、速度が遅くなる可能性があります。

このようなパワーと柔軟性には、低速という代償が伴います。多くの単純なものをダンプする場合、JSONの方が適していますが、とにかく結果を手動で編集することはほとんどありません。編集や複雑なオブジェクト、あるいはその両方を伴う場合でも、YAMLの使用を検討する必要があります。


¹ すべてのPython文字列を(二重)引用符を含むYAMLスカラーとして強制的にダンプすることは可能ですが、スタイルを設定するだけではすべてのリードバックを防ぐのに十分ではありません。

2
Anthon