Saltar al contenido principal

Rendimiento

Cuánto tarda una ejecución y cuánta memoria pide. Cada número de esta página se midió en una máquina ejecutando las cinco líneas de comandos publicadas sobre la misma configuración: ni estimado, ni recordado de una versión anterior.

La respuesta corta, si es lo único que necesita: dos millones de registros de seis campos tardan entre nueve y quince segundos, y en el motor de flujo la memoria no crece con el número de filas. Python es la excepción, con unos noventa segundos.

Qué se midió y cómo

La prueba ejecuta la línea de comandos que entrega cada registro, instalada en un directorio desechable. Nada aquí lee una copia del repositorio, que es también lo que permite a un tercero repetirla.

Cada regla existe para que esto siga siendo una medición y no un anuncio:

  • El mismo archivo de configuración, byte a byte, sustituyendo solo count y engine.
  • El motor se elige en la configuración, no en la línea de comandos, así se pregunta a las cinco de la única forma que todas entienden con seguridad.
  • --now fijado, para que un generador de fechas no se desvíe entre ejecuciones.
  • --jobs 1 en todas. TypeScript es la única que reparte una ejecución entre procesos; medir eso contra cuatro implementaciones de un solo hilo mediría una funcionalidad, no un motor.
  • El reloj y la memoria máxima salen de /usr/bin/time -l, fuera del proceso, para que ninguna se mida a sí misma.
  • Cada salida se resume con un hash y los hashes deben coincidir. Un número de velocidad para una ejecución que produjo datos distintos no vale nada, así que una discrepancia tumba la fila en vez de aparecer en el informe. Todos los números de abajo vienen de ejecuciones que coincidieron byte a byte.

La máquina

ProcesadorApple M2 Max, 12 núcleos (8 de rendimiento, 4 de eficiencia)
Memoria32 GB
AlmacenamientoApple SSD AP1024Z, 1 TB, APFS, TRIM activo — al 94% de ocupación
SistemamacOS 26.5.1

El almacenamiento importa menos de lo que parece, y conviene demostrarlo en vez de afirmarlo. La escritura secuencial en este volumen mide 810 MB/s en frío y unos 1,3 GB/s en caliente. La ejecución más grande de esta página produce un archivo de 141 MB, y escribir 141 MB y volcarlos al disco cuesta 0,24 segundos medidos directamente: dentro de una ejecución de nueve a quince segundos, entre un dos y un tres por ciento, y un cuarto de uno por ciento para Python. Estos números los limita el procesador, así que puede escalarlos a su máquina por la velocidad de núcleo y casi olvidarse del disco. No se lee nada grande: los paquetes de datos pesan kilobytes y quedan en caché tras el primer uso.

Las versiones

Todas las cifras se tomaron sobre la 0.1.4 publicada, exactamente como la instala un usuario, con una excepción: Rust se compiló desde el código, porque su motor de flujo publicado aún retenía la ejecución entera al escribir a un archivo. Esa corrección salió en la 0.1.5, así que las cifras de Rust son las de la 0.1.5 y las otras cuatro las de la 0.1.4.

Entre ambas versiones el motor cambió en una recuperación de enrutado y tres mensajes de diagnóstico — nada de eso afecta a la velocidad de una fila ni a la memoria que retiene una ejecución. Las cifras valen para la 0.1.5.

Los tres motores, en breve

No hay nada que elegir: TDC toma el motor de su configuración, de forma determinista, y la misma configuración obtiene el mismo motor en cualquier máquina. Las tablas se separan por motor solo porque eso explica la forma de los números.

MotorQué haceQué cuesta
1 — en memoriaGuarda columnas enteras y responde al instanteLa memoria crece con el número de filas
2 — de flujoResuelve una fila cada vezLa memoria se mantiene plana; aquí corre casi todo
3 — exacto en discoCumple promesas sobre una columna terminada, como uniqLa memoria queda acotada, a cambio de una ordenación externa

La diferencia entre el 2 y el 3 es la que conviene retener, y no es de grado. La unicidad es una promesa sobre el conjunto terminado, no sobre una fila cualquiera, así que no puede resolverse fila a fila: el motor de flujo tendría que saber qué viene después. Por eso la segunda tabla compara el motor 1 con el motor 3: el 2 ni siquiera es candidato para esa configuración.

Salidas grandes tiene el relato completo, incluidas las cinco formas de configuración que devuelven una ejecución al motor 1.

Una configuración corriente

Seis campos elegidos para costar cosas distintas, no para parecer realistas: un contador, dos sorteos ponderados de un paquete de datos, un reparto porcentual exacto, un número con decimales, una fecha y un valor montado a partir de otros dos.

<sequence name="Id"><gen type="increment" value="1"/></sequence>
<sequence name="First"><gen type="template" value="person.male.firstName"/></sequence>
<sequence name="Last"><gen type="template" value="person.lastName"/></sequence>
<sequence name="Status"><gen type="text" value="active,trial,closed" percent="70,20,10"/></sequence>
<sequence name="Balance"><gen type="number" value="0..99999" decimals="2"/></sequence>
<sequence name="Joined"><gen type="date" range="2015-01-01..2025-12-31" format="YYYY-MM-DD"/></sequence>

Conviene tener una intuición de los tres tamaños: un archivo que abriría en un editor, uno que no, y uno que tarda un momento en copiarse:

FilasEl CSV que escribe
pequeño10 0000,7 MB
mediano200 00014 MB
grande2 000 000141 MB

Unos 74 bytes por fila, así que cualquier tamaño se deduce de ahí: un gigabyte son unos catorce millones de filas de esta forma.

Tiempo

Segundos, el mejor de tres ejecuciones (de dos en el tamaño mayor). Menos es mejor.

10 000 filas
0,7 MB
200 000 filas
14 MB
2 000 000 filas
141 MB
Rust0,05 / 0,040,87 / 0,898,97 / 8,82
Java0,30 / 0,291,21 / 1,199,62 / 9,50
Node.js0,22 / 0,231,21 / 1,4112,97 / 14,37
C#0,30 / 0,291,78 / 1,7614,37 / 15,34
Python0,55 / 0,668,35 / 10,2491,30 / 112,11

Cada celda es motor 1 / motor 2. La misma ejecución, en el tamaño mayor, con las barras dibujadas:

2 000 000 filas141 MBmotor 1 — en memoriamotor 2 — de flujo
Rustcrates.io8,97s8,82s
JavaMaven Central9,62s9,50s
Node.jsnpm12,97s14,37s
C#NuGet14,37s15,34s
PythonPyPI91,30s112,11s
Segundos para el archivo de 141 MB. Ambas columnas comparten una escala, así que las barras son comparables en toda la tabla; el verde es la medición más rápida y el rojo la más lenta.

Con diez mil filas se mide sobre todo el arranque: una JVM levantándose, un intérprete de Python importando. Por debajo de unas cien mil filas, la implementación elegida apenas importa.

Memoria

Memoria residente máxima, en megabytes. Menos es mejor.

10 000 filas
0,7 MB
200 000 filas
14 MB
2 000 000 filas
141 MB
Rust10,6 / 3,7146 / 3,71322 / 3,7
C#53,6 / 48,5187 / 49,41375 / 49,4
Python40,0 / 32,1197 / 32,21529 / 32,3
Node.js97,6 / 98,0190 / 1541188 / 190
Java147 / 120885 / 3954140 / 397
2 000 000 filas141 MBmotor 1 — en memoriamotor 2 — de flujo
Node.jsnpm1188MB190MB
Rustcrates.io1322MB3,7MB
C#NuGet1375MB49,4MB
PythonPyPI1529MB32,3MB
JavaMaven Central4140MB397MB
Memoria máxima para el mismo archivo de 141 MB, en una escala. La columna derecha es aquello para lo que existe el motor de flujo: la barra de Rust son 3,7 MB frente a sus propios 1322 MB de la izquierda.

Si solo va a leer una tabla, lea esta. En el motor 1 la memoria sigue al número de filas: diez veces más filas, unas diez veces más memoria, en todas las implementaciones. En el motor 2 no se mueve en absoluto: Rust ocupa 3,7 MB tanto con diez mil filas como con dos millones, C# unos 49 MB y Python unos 32 MB.

Ese es el trato que ofrece el motor de flujo, y no es «más rápido». A veces es incluso algo más lento. Lo que compra con esas fracciones de segundo es una ejecución cuya memoria puede predecir antes de lanzarla.

Una configuración con uniq

Combinaciones que no se repiten en toda la ejecución: 150 × 150 × 150 posibilidades, y 200 000 filas ocupan alrededor del seis por ciento del espacio.

<sequence name="Pair" uniq="true">
<gen type="text" name="City" value="C000,C001,…,C149"/>
<gen type="text" name="Grade" value="G000,G001,…,G149"/>
<gen type="text" name="Slot" value="S000,S001,…,S149"/>
</sequence>

Un archivo más pequeño que el de arriba: tres códigos cortos por fila en vez de seis campos.

200 000 filas4,1 MBmotor 1 — en memoriamotor 3 — exacto en disco
JavaMaven Central0,96s1,32s
Rustcrates.io1,03s1,22s
Node.jsnpm1,26s1,91s
C#NuGet1,35s1,60s
PythonPyPI4,79s8,23s
Segundos. El motor 2 no aparece porque no puede ejecutar esta configuración.
200 000 filas4,1 MBmotor 1 — en memoriamotor 3 — exacto en disco
C#NuGet188MB113MB
PythonPyPI209MB76,0MB
Rustcrates.io216MB138MB
Node.jsnpm264MB204MB
JavaMaven Central793MB637MB
Memoria máxima de la misma ejecución. Todas las implementaciones devuelven algo en el motor 3, y las dos que más devuelven acaban más ligeras que cualquier cosa de la izquierda.

Aquí el motor 3 es más lento y más ligero, que es justamente el trato para el que existe. Su coste además crece más deprisa que el del motor 1 a medida que suben las filas, porque verifica con una ordenación externa. Por eso una ejecución muy grande con uniq es el único caso de esta página en el que conviene medir su propia configuración en vez de leer una tabla.

Los dos motores producen datos distintos — y no pasa nada

Con una configuración uniq, los motores 1 y 3 disponen los valores de forma distinta. Ambos resultados son válidos, ambos son exactamente reproducibles y, dentro de cada motor, las cinco implementaciones coinciden byte a byte. Difieren entre sí porque llegan a la unicidad por caminos distintos.

En la práctica no puede sorprenderle: el motor se elige a partir de la configuración, así que una configuración obtiene siempre un motor y por tanto una respuesta. Tendría que forzar el motor a mano para ver la diferencia, que es también la razón para no forzarlo a la ligera.

Repetirlo usted mismo

El arnés está en el repositorio e instala por sí solo las líneas de comandos publicadas:

python3 bench/cli_bench.py --config customers --tier all --repeats 3
python3 bench/cli_bench.py --config customers --tier medium
=== customers medium: 200 000 rows
npm        e1     1.21s     189.9 MB  3bca9c07410bf117
pypi       e1     8.35s     196.9 MB  3bca9c07410bf117
crates.io  e1     0.87s     146.1 MB  3bca9c07410bf117
nuget      e1     1.78s     187.0 MB  3bca9c07410bf117
maven      e1     1.21s     885.2 MB  3bca9c07410bf117

every implementation produced identical bytes, on every engine it ran

El hash al final de cada línea es lo importante: es el mismo en las cinco, así que los tiempos son comparables porque el trabajo lo era.

Vea también