10 puntos por GN⁺ 2025-08-05 | 1 comentarios | Compartir por WhatsApp
  • En el motor V8, mejoraron el rendimiento de la función JSON.stringify a más del doble, logrando una serialización de datos más rápida
  • Introdujeron una ruta de optimización para objetos sin efectos secundarios, omitiendo gran parte de la lógica defensiva de verificación y consiguiendo una mejora importante de velocidad en objetos de datos comunes
  • En el procesamiento de cadenas, aplicaron métodos más avanzados a nivel de hardware y memoria, como la distinción entre 1 byte/2 bytes, uso de SIMD y cambios en la estructura del búfer temporal
  • En el proceso de conversión numérica, reemplazaron el algoritmo Grisu3 por Dragonbox, lo que también permite conversiones más rápidas en las llamadas a Number.toString()
  • En algunos argumentos o formas vuelve a la ruta de serialización general, pero en la mayoría de los escenarios de desarrollo web se puede aprovechar la optimización automáticamente

Resumen general

  • JSON.stringify es una función clave para convertir datos en cadenas dentro de JavaScript
  • Mejorar el rendimiento de esta función también impacta positivamente tareas muy importantes en la web, como solicitudes de red o almacenamiento en localStorage
  • Gracias al trabajo de ingeniería más reciente en V8, la velocidad de esta función mejoró a más del doble, y aquí se presentan en detalle las principales optimizaciones

Ruta Fast Path sin efectos secundarios

  • La clave de la optimización es aplicar una ruta rápida de serialización que solo puede usarse en situaciones sin efectos secundarios (side effects)
  • En estos casos, el recorrido del objeto se hace con una estructura iterativa en lugar de recursiva, por lo que no se necesitan verificaciones de stack overflow y también es posible intentar serializar objetos más profundos
  • Cuando el objeto de datos es simple, V8 usa este Fast Path en lugar de la lógica general más lenta, omitiendo muchas verificaciones y acelerando el proceso

Manejo de distintas representaciones de cadenas

  • V8 almacena las cadenas de forma diferente según sean de 1 byte o 2 bytes (ASCII/no ASCII), y si aparece aunque sea un carácter no ASCII, todo se maneja como 2 bytes
  • Para mejorar el rendimiento de la serialización de cadenas, compilaron versiones separadas del algoritmo según el tipo de cadena
  • Como durante el procesamiento hay que verificar el tipo de instancia de la cadena, si se detecta una cadena de 2 bytes, el serializador apropiado de 2 bytes toma el control del estado
  • Gracias a esto, la sobrecarga por cambiar entre rutas según la codificación de la cadena es prácticamente nula
  • El resultado se genera creando por separado búferes de 1 byte y 2 bytes, que luego simplemente se combinan al final

Optimización de serialización de cadenas con SIMD

  • Las cadenas de JavaScript pueden incluir caracteres que necesitan escape al serializarse como JSON
  • Las cadenas largas se revisan varios bytes a la vez usando instrucciones SIMD de hardware (como ARM64 Neon)
  • Las cadenas cortas usan un enfoque SWAR, procesando varios caracteres al mismo tiempo mediante operaciones de bits en registros de propósito general
  • En cualquiera de los dos casos, la mayoría de las veces es posible copiar toda la cadena rápidamente sin transformaciones adicionales

Se agregó Express Lane (ruta ultrarrápida)

  • Incluso dentro del Fast Path, se preparó una Express Lane para poder serializar copiando solo las claves, sin trabajo repetitivo como verificaciones de propiedades
  • Aprovechando los flags de hidden class del objeto, si las claves no tienen Symbol, todas son enumerable y ya pueden serializarse sin necesidad de escape, se marca como fast-json-iterable
  • Al serializar otros objetos con la misma hidden class, las claves se copian directamente sin verificaciones adicionales
  • Esta técnica también se aplica en JSON.parse para comparaciones rápidas de claves

Un algoritmo más rápido de double-to-string

  • El proceso de convertir números a cadenas también es frecuente y complejo
  • Se reemplazó el algoritmo Grisu3 por Dragonbox, lo que mejora el rendimiento en las llamadas generales a Number.prototype.toString()

Optimización de la estructura del búfer temporal

  • Antes, al construir cadenas se usaba un único búfer continuo, lo que provocaba una sobrecarga por copiar todo el contenido cada vez que faltaba espacio
  • El nuevo enfoque usa una estructura de búfer segmentado, uniendo varios búferes pequeños según sea necesario
  • Gracias a esto, cuando no hay suficiente espacio, solo se asigna un nuevo búfer sin necesidad de copiar todo

Limitaciones

  • El Fast Path solo funciona para serialización simple de datos
  • Si no se cumplen las siguientes condiciones, se usa la ruta general
    • No se puede usar el argumento replacer ni space (sin Pretty-Print ni transformaciones)
    • Debe ser un objeto simple sin método personalizado toJSON
    • Si hay propiedades basadas en índice, pasa a la ruta lenta
    • No maneja cadenas especiales como ConsString
  • En la mayoría de usos comunes, como serialización de datos, generación de respuestas de API o caché de configuración, la optimización se aplica automáticamente

Conclusión

  • Al replantear el enfoque en todas las áreas, desde el diseño básico de JSON.stringify hasta el manejo de memoria y caracteres, lograron un aumento de velocidad de más de 2x en el benchmark JetStream2
  • Estas mejoras ya pueden experimentarse en V8 versión 13.8 (Chrome 138) o superior

1 comentarios

 
GN⁺ 2025-08-05
Opiniones en Hacker News
  • Se siente que la codificación JSON es un gran obstáculo para la comunicación entre procesos en NodeJS

    • Al final, la mayoría intenta mover el trabajo a otros hilos para reducir la latencia del event loop, pero terminan descubriendo que la carga de CPU del hilo principal acaba siendo 3 veces mayor
    • También he visto muchos ejemplos de hacer stringify de los arreglos uno por uno, y parece que internamente también se aplica este enfoque
    • Ojalá el equipo de V8 reforzara más esta parte
    • Me pregunto si hay forma de procesar algunos conjuntos de datos sin bail out, o qué pasa con el manejo de CString; también me interesa saber si volverá la función faststr
    • Cuando hice mi primer análisis de rendimiento de Node el año pasado, JSON.stringify fue uno de los mayores factores que bloqueaban el rendimiento de un servicio Node
      • Hay que usar stringify para las claves de diccionario, y apollo/express serializa toda la respuesta como una sola cadena en vez de hacer streaming
      • Viniendo de JVM o Go, esa parte en Node me pareció bastante amateur
    • Python tiene exactamente el mismo problema
      • Creo que sería bueno tener primitivas de IPC eficientes por encima de APIs de alto nivel para patrones comunes
    • Coincido con la idea de que la codificación JSON es una gran barrera para la comunicación
      • Me pregunto qué tan grande es, a escala global, la sobrecarga computacional del procesamiento JSON en las comunicaciones, y si sería mejor simplemente enviar bytes en formatos fijos u otros más eficientes de parsear, por ejemplo ASN.1
    • Estoy en contra de que el equipo de V8 invierta más agresivamente en esto, y recomendaría a los desarrolladores que tengan este problema buscar otras herramientas
      • No creo que Node/V8 sea especialmente adecuado para backend o problemas de cómputo de alto rendimiento
      • JavaScript está hecho para la web y seguirá así por mucho tiempo, así que no hace falta que el equipo de V8 resuelva este tipo de problemas
      • El equipo de TypeScript también se pasó a Go, y además es posible automatizar conversiones entre lenguajes
    • Casi solo hubo una vez en la que descargar trabajo a un Worker me ahorró más tiempo del que costó serializar/deserializar
      • Si los datos son grandes, el costo del paso de mensajes se vuelve tan alto que iguala la ganancia del paralelismo
  • Me sorprendió muchísimo cuánto ha mejorado en la última década el rendimiento de la serialización de números de punto flotante

    • Convertir valores IEEE de punto flotante a cadenas decimales UTF-8 y luego volver a convertirlos no solo es lento, también es muy inestable
      • Como los valores que pueden representarse exactamente en binario y en decimal son distintos, pueden aparecer pequeños errores
  • Dicen que si JSON.stringify recibe los argumentos replacer o space, no se aplica el fast path

    • Entonces me pregunto si JSON.stringify(data, null, 0) también puede usar el fast path, o si los argumentos tienen que ser undefined
  • El algoritmo de escaping SWAR[1] es muy parecido al que implementé en Folly JSON[2]

  • No dudo del valor del trabajo en sí, pero me gustaría ver más problemas o datos concretos donde JSON.stringify realmente dominara el runtime dentro del ecosistema real de V8

    • No hace falta que represente la mayor parte del tiempo de ejecución; si se llama cientos de millones de veces al día en páginas de todo el mundo, el ahorro global de energía igual sería considerable
  • Creo que el rendimiento de v8 no recibe suficiente reconocimiento; hoy en día JS se ha vuelto increíblemente rápido

    • De verdad es impresionante; me parece un buen ejemplo de “con mil millones de dólares puedes resolver cualquier problema”
      • Ojalá en el futuro JS evolucione más, como con “strict” y luego “stricter”, para volverse un lenguaje más fácil y simple para compilar/JIT
    • Por otro lado, v8 está optimizado tan al extremo que probablemente solo unas 100 personas en todo el mundo entienden bien su interior, y la mayoría de los desarrolladores debe quedarse pensando “¿por qué mi JS no corre más rápido?”
  • Me pregunto qué tan sobresaliente es esto en comparación con otros ecosistemas

    • Llevo más de 10 años serializando JSON y casi nunca me he preocupado por ello porque siempre ha sido demasiado rápido
    • simdjson puede procesar GB por segundo por núcleo, y considerando optimizaciones como prefetching y branch prediction, creo que la serialización JSON suele ser despreciable en la mayoría de las cargas reales
    • La mayor desventaja de JSON es el overhead de I/O; por más rápido que sea el serializer, si igual tienes que guardar un blob de 100MB en almacenamiento cada vez, no sirve de mucho
  • “No indexed properties on objects” — dicen que el fast path solo está optimizado para objetos comunes con claves de tipo string, y que si hay propiedades indexadas tipo array vuelve al slow path

    • Me pregunto cuál es la razón
    • ¿Eso significa que un objeto con claves que parecen enteros se serializa como arreglo JSON? No puede ser... ¿o sí?
  • Me gusta el enfoque de segmented buffer; antes había que armar a mano trucos con rope usando librerías userland como fast-json-stringify, pero ahora al ser nativo está mucho mejor

    • Me pregunto si se encuentran mucho con condiciones de bailout, como replacer, space o .toJSON() personalizado, y si en esos casos hace fallback inmediato a la ruta lenta
  • V8 es excelente, pero por JS en sí, o quizá por eso mismo, queda por debajo de LuaJIT o la JVM en rendimiento

    • La JVM sí tiene tiempos de calentamiento más largos, pero aun así sigue siendo mejor que JS
    • El problema es JS; creo que V8 está mucho más avanzado que LuaJIT y la JVM
      • Java tiene menos restricciones de tiempo real (porque tiene compilador), y esa es una ventaja
    • Gran parte del overhead de JS viene de su naturaleza dinámica
      • asm.js podía saltarse muchas verificaciones porque prohibía comportamientos dinámicos como cambiar el shape de los objetos
    • No estoy de acuerdo con la expresión “incluso la JVM”; la JVM es de primer nivel