¿Cómo es que la serialización json es mucho más rápida que la serialización yaml en Python?

Tengo un código que se basa en gran medida en yaml para la serialización en varios idiomas y, mientras trabajaba en acelerar algunas cosas, noté que yaml era increíblemente lento en comparación con otros métodos de serialización (por ejemplo, pickle, json).

Entonces, lo que realmente me sorprende es que json es mucho más rápido que yaml cuando la salida es casi idéntica.

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

El CSafeDumper y el cjson de PyYaml están escritos en C, por lo que no se trata de un problema de velocidad entre C y Python. Incluso le he agregado algunos datos aleatorios para ver si cjson está haciendo algún almacenamiento en caché, pero aún es mucho más rápido que PyYaml. Me doy cuenta de que yaml es un superconjunto de json, pero ¿cómo podría el serializador yaml ser 2 órdenes de magnitud más lento con una entrada tan simple?

En general, no es la complejidad de la salida lo que determina la velocidad de análisis, sino la complejidad de la entrada aceptada. La gramática de JSON es muy concisa . Los analizadores de YAML son comparativamente complejos , lo que lleva a mayores gastos generales.

El principal objective de diseño de JSON es la simplicidad y la universalidad. Por lo tanto, JSON es trivial para generar y analizar, a costa de una legibilidad humana reducida. También utiliza un modelo de información de denominador común más bajo, lo que garantiza que cualquier entorno de progtwigción moderno pueda procesar fácilmente los datos JSON.

En contraste, los objectives de diseño más importantes de YAML son la legibilidad humana y el soporte para serializar estructuras de datos nativos arbitrarios. Por lo tanto, YAML permite archivos extremadamente legibles, pero es más complejo de generar y analizar. Además, YAML se aventura más allá de los tipos de datos de denominador común más bajo, lo que requiere un procesamiento más complejo al cruzar entre diferentes entornos de progtwigción.

No soy un implementador del analizador YAML, por lo que no puedo hablar específicamente de los órdenes de magnitud sin algunos datos de perfilado y un gran corpus de ejemplos. En cualquier caso, asegúrese de realizar pruebas sobre una gran cantidad de entradas antes de sentirse seguro en los números de referencia.

Actualizar Whoops, malinterpreta la pregunta. 🙁 La serialización puede ser increíblemente rápida a pesar de la gran gramática de entrada; sin embargo, al buscar en la fuente, parece que la serialización de Python a nivel de PyYAML construye un gráfico de representación, mientras que simplejson codifica los tipos de datos Python integrados directamente en fragmentos de texto.

En las aplicaciones en las que he trabajado, la inferencia de tipo entre cadenas a números (float / int) es donde la sobrecarga más grande es para analizar yaml porque las cadenas se pueden escribir sin comillas. Debido a que todas las cadenas en json están entre comillas, no hay retroceso al analizar las cadenas. Un gran ejemplo donde esto podría ralentizarse es el valor 0000000000000000000s. No puedes decir que este valor es una cadena hasta que hayas leído hasta el final.

Las otras respuestas son correctas, pero este es un detalle específico que he descubierto en la práctica.

Hablando de eficiencia, usé YAML por un tiempo y me sentí atraído por la simplicidad que algunas asignaciones de nombre / valor asumen en este idioma. Sin embargo, en el proceso tropecé con una y otra vez sobre una de las finuras de YAML, variaciones sutiles en la gramática que le permiten escribir casos especiales en un estilo más conciso y tal. Al final, aunque la gramática de YAML es casi cierta para cierta formalidad consistente, me ha dejado un cierto sentimiento de “vaguedad”. Luego me limité a no tocar el código YAML existente y funcional, y escribí todo lo nuevo en una syntax más segura y a prueba de fallas, lo que me hizo abandonar todo YAML. El resultado es que YAML trata de parecer un estándar de W3C y produce una pequeña biblioteca de literatura difícil de leer sobre sus conceptos y reglas.

Esto, creo, es mucho más intelectual que necesario. Mire SGML / XML: desarrollado por IBM en la década de los 60, estandarizado por la ISO, conocido (en forma simplificada y modificada) como HTML para millones de personas, documentado y documentado y documentado nuevamente en todo el mundo. Aparece el pequeño JSON y mata a ese dragón. ¿Cómo podría JSON llegar a ser tan ampliamente utilizado en tan poco tiempo, con solo un exiguo sitio web (y una luminaria javascript para respaldarlo)? Es en su simplicidad, la absoluta ausencia de dudas en su gramática, la facilidad de aprendizaje y su uso.

XML y YAML son difíciles para los humanos, y son difíciles para las computadoras. JSON es bastante amigable y fácil tanto para los humanos como para las computadoras.

Una mirada superficial a python-yaml sugiere que su diseño es mucho más complejo que el de 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'] 

Los diseños más complejos casi invariablemente significan diseños más lentos, y esto es mucho más complejo de lo que la mayoría de las personas necesitarán.

Aunque tiene una respuesta aceptada, desafortunadamente eso solo hace un poco de maniobra en la dirección de la documentación de PyYAML y cita una statement en esa documentación que no es correcta: PyYAML no hace un gráfico de representación durante el volcado, crea una stream lineal (y simplemente como json guarda un cubo de identificaciones para ver si hay recursiones).


Primero que todo, debes darte cuenta de que mientras el volcador cjson es un código C hecho a mano solamente, el CSafeDumper de YAML comparte dos de las cuatro etapas de volcado ( Representer y Resolver ) con el PyDon SafeDumper puro normal y que las otras dos etapas (el Serializador y el Emisor) ) no se escriben completamente a mano en C, sino que consisten en un módulo Cython que llama a la biblioteca C libyaml para emitir.


Aparte de esa parte importante, la respuesta simple a tu pregunta de por qué demora más, es que descargar YAML hace más. Esto no es tanto porque YAML es más difícil de lo que dice @flow, sino porque ese extra que YAML puede hacer, lo hace mucho más poderoso que JSON y también más fácil de usar, si necesita procesar el resultado con un editor. Eso significa que se gasta más tiempo en la biblioteca YAML incluso cuando se aplican estas funciones adicionales, y en muchos casos también se comprueba si se aplica algo.

Aquí hay un ejemplo: incluso si nunca ha pasado por el código PyYAML, habrá notado que el dumper no cita a foo y bar . Esto no se debe a que estas cadenas sean claves, ya que YAML no tiene la restricción que tiene JSON, que una clave para una asignación debe ser una cadena. Por ejemplo, una cadena de Python que es un valor en la asignación también puede no estar entre comillas (es decir, sin formato).

El énfasis está en el poder , porque no siempre es así. Tomemos, por ejemplo, una cadena que consta de caracteres numéricos solamente: 12345678 . Esto debe escribirse con comillas, de lo contrario, se vería exactamente como un número (y volver a leer como tal cuando se analiza).

¿Cómo sabe PyYAML cuándo citar una cadena y cuándo no? Al volcar, en realidad, primero descarga la cadena, luego analiza el resultado para asegurarse de que, cuando vuelve a leer ese resultado, obtiene el valor original. Y si eso prueba no ser el caso, aplica cotizaciones.

Permítame repetir la parte importante de la oración anterior nuevamente, para que no tenga que volver a leerla:

vuelca la cadena, luego analiza el resultado

Esto significa que aplica toda la expresión regular que hace al cargar para ver si el escalar resultante se cargaría como un entero, flotante, booleano, fecha y hora, etc., para determinar si es necesario aplicar las comillas o no.


En cualquier aplicación real con datos complejos, un descargador / cargador basado en JSON es demasiado simple de usar directamente y mucha más inteligencia tiene que estar en su progtwig en comparación con descargar los mismos datos complejos directamente a YAML. Un ejemplo simplificado es cuando desea trabajar con sellos de fecha y hora, en ese caso, debe convertir una cadena de ida y vuelta a datetime.datetime usted mismo si está utilizando JSON. Durante la carga, tiene que hacer eso, ya sea por el hecho de que este es un valor asociado con alguna clave (con suerte reconocible):

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

o con una posición en una lista:

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

o basado en el formato de la cadena (por ejemplo, usando expresiones regulares).

En todos estos casos, se necesita mucho más trabajo en su progtwig. Lo mismo se aplica al dumping y eso no solo significa tiempo de desarrollo adicional.

Permite regenerar sus tiempos con lo que tengo en mi máquina para que podamos compararlos con otras mediciones. Reescribí un poco su código, porque estaba incompleto (¿era timeit ?) timeit importó otras cosas dos veces. También fue imposible simplemente cortar y pegar debido a las >>> indicaciones.

 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) 

y esto produce:

 yaml.SafeDumper: foo: {bar: 1} cjson.encode: {"foo": {"bar": 1}} yaml.SafeDumper 3.06794905663 yaml.CSafeDumper 0.781533956528 cjson.encode 0.0133550167084 

Ahora permite volcar una estructura de datos simple que incluye una datetime y 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() 

Esto da:

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

Para la sincronización de lo anterior, creé un módulo myjson que envuelve cjson.encode y tiene la stringify anterior definida. Si usas eso:

 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) 

dando:

 yaml.CSafeDumper 0.813436031342 cjson.encode 0.151570081711 

Esa salida aún bastante simple, ya te regresa de dos órdenes de magnitud en la diferencia de velocidad a menos de un orden de magnitud.


Los escalares planos y el formato de estilo de bloque de YAML hacen que los datos sean más legibles. El hecho de que pueda tener una coma al final de una secuencia (o asignación) genera menos fallas al editar manualmente los datos YAML con los mismos datos en JSON.

Las tags YAML permiten la indicación dentro de los datos de sus tipos (complejos). Cuando use JSON , debe tener cuidado, en su código, de cualquier cosa más compleja que los mapeos, secuencias, enteros, flotantes, booleanos y cadenas. Dicho código requiere tiempo de desarrollo, y es poco probable que sea tan rápido como python-cjson (por supuesto, también puede escribir su código en C).

El volcado de algunos datos, como estructuras de datos recursivas (por ejemplo, datos topológicos), o claves complejas está predefinido en la biblioteca PyYAML. Allí, la biblioteca JSON solo produce errores, y la implementación de una solución para eso no es trivial y lo más probable es que disminuya la velocidad, ya que las diferencias de velocidad son menos relevantes.

Tal poder y flexibilidad tienen un precio de menor velocidad. Cuando descargue muchas cosas simples, JSON es la mejor opción, es poco probable que edite el resultado a mano de todos modos. Para cualquier cosa que implique editar u objetos complejos o ambos, aún debe considerar el uso de YAML.


¹ Es posible forzar el volcado de todas las cadenas de Python como escalas YAML con comillas (dobles), pero establecer el estilo no es suficiente para evitar toda lectura.