10 pontos por GN⁺ 2025-08-05 | 1 comentários | Compartilhar no WhatsApp
  • No motor V8, a performance da função JSON.stringify foi aumentada em mais de 2x, melhorando a velocidade de serialização de dados
  • Foi introduzido um caminho de otimização para objetos sem efeitos colaterais, omitindo várias verificações defensivas e trazendo grande ganho de velocidade para objetos de dados comuns
  • No processamento de strings, foram aplicados métodos avançados do ponto de vista de hardware e memória, como distinção entre 1 byte/2 bytes, uso de SIMD e mudança na estrutura de buffers temporários
  • No processo de conversão de números, o algoritmo Grisu3 foi substituído por Dragonbox, abrindo caminho para conversões mais rápidas em chamadas de Number.toString() como um todo
  • Em alguns argumentos e formatos, o processamento volta ao caminho normal de serialização, mas na maioria dos cenários de desenvolvimento web o efeito da otimização é aproveitado automaticamente

Visão geral

  • JSON.stringify é a função central para converter dados em string no JavaScript
  • Melhorar a performance dessa função também impacta positivamente tarefas muito importantes na web, como requisições de rede e salvamento no localStorage
  • Com a engenharia mais recente do V8, a velocidade desse recurso foi mais que dobrada, e as principais estratégias de otimização são apresentadas em detalhe

Fast Path sem efeitos colaterais

  • O núcleo da otimização é aplicar um caminho rápido de serialização que só pode ser usado em situações sem efeitos colaterais (side effects)
  • Nessas situações, em vez de recursão, os objetos são percorridos com uma estrutura iterativa, o que elimina a necessidade de verificação de stack overflow e permite tentar serializar objetos mais profundos
  • Quando o objeto de dados é simples, o V8 usa esse Fast Path no lugar da lógica geral mais lenta, omitindo muitas verificações e aumentando a velocidade

Tratamento de diferentes representações de strings

  • O V8 armazena strings de forma diferente conforme sejam caracteres de 1 byte/2 bytes (ASCII/não ASCII); se houver ao menos um caractere não ASCII, tudo passa a ser tratado em 2 bytes
  • Para melhorar a performance da serialização, são compiladas versões separadas do algoritmo para cada tipo de string
  • Como durante o processamento é preciso verificar o tipo da instância da string, quando uma string de 2 bytes é detectada, o serializador apropriado de 2 bytes assume o estado
  • Com isso, praticamente não há sobrecarga na troca de caminho por codificação de string
  • O resultado final é produzido criando buffers separados de 1 byte e 2 bytes, que são simplesmente combinados no final

Otimização da serialização de strings com SIMD

  • Strings JavaScript podem conter caracteres que precisam ser escapados durante a serialização em JSON
  • Strings longas são verificadas vários bytes de uma vez com instruções de hardware SIMD (como ARM64 Neon)
  • Strings curtas usam a abordagem SWAR, processando vários caracteres simultaneamente com operações de bits em registradores de uso geral
  • Em qualquer dos métodos, na maioria dos casos é possível copiar a string inteira rapidamente sem transformações relevantes

Adição da Express Lane (caminho ultrarrápido)

  • Mesmo dentro do Fast Path, foi criada uma Express Lane para permitir serializar apenas copiando chaves, sem trabalho repetitivo como verificação de propriedades
  • Usando a flag de hidden class do objeto, quando as chaves não têm Symbol, todas são enumerable e já foram serializadas sem necessidade de escape, o objeto é marcado como fast-json-iterable
  • Ao serializar outros objetos com a mesma hidden class, a cópia das chaves é feita imediatamente, sem verificações adicionais
  • Essa técnica também é aplicada em JSON.parse para comparação rápida de chaves

Algoritmo double-to-string mais rápido

  • O processo de converter números em string também é frequente e complexo
  • O algoritmo Grisu3 existente foi substituído por Dragonbox, gerando ganho de performance em chamadas de Number.prototype.toString() como um todo

Otimização da estrutura de buffers temporários

  • Ao montar strings, antes era usado um buffer contínuo único, o que causava a sobrecarga de copiar tudo novamente sempre que o espaço acabava
  • O novo método usa uma estrutura de buffer segmentado, conectando vários buffers menores conforme necessário
  • Assim, quando falta espaço, basta alocar um novo buffer, sem necessidade de copiar tudo

Limitações

  • O Fast Path funciona apenas para serialização de dados simples
  • Se as condições abaixo não forem atendidas, o caminho normal é usado
    • Não é possível usar os argumentos replacer ou space (sem Pretty-Print ou transformação)
    • Deve ser um objeto simples, sem método customizado toJSON
    • Se houver propriedades baseadas em índice, o processamento muda para o caminho lento
    • Strings especiais como ConsString não são tratadas
  • Para a maioria dos usos comuns, como serialização de dados, geração de respostas de API e cache de configurações, o efeito da otimização é aplicado automaticamente

Conclusão

  • Ao reorganizar a abordagem em toda a função JSON.stringify, do design básico ao tratamento de memória e caracteres, foi alcançado um ganho de mais de 2x de velocidade no benchmark JetStream2
  • Essas melhorias já podem ser percebidas a partir do V8 versão 13.8 (Chrome 138) ou superior

1 comentários

 
GN⁺ 2025-08-05
Comentários no Hacker News
  • Sinto que a codificação JSON é um grande gargalo na comunicação entre processos no NodeJS

    • No fim, a maioria tenta passar o trabalho para outra thread para reduzir a latência do event loop, mas acaba descobrindo que a carga de CPU da thread principal termina ficando 3x maior
    • Também já vi muitos exemplos de fazer stringify de arrays um por um, e parece que algo assim também é aplicado internamente
    • Gostaria que a equipe do V8 reforçasse mais essa parte
    • Fico curioso se dá para processar alguns conjuntos de dados sem bail out, como fica a questão do tratamento de CString e se o recurso faststr vai voltar
    • Quando analisei performance do Node pela primeira vez no ano passado, JSON.stringify foi um dos maiores fatores que limitavam o desempenho em serviços Node
      • É preciso usar stringify para chaves de dicionário, e o apollo/express serializa a resposta inteira em uma string de uma vez, em vez de fazer streaming
      • Vindo da JVM ou de Go, isso pareceu bem amador no Node
    • Python tem exatamente o mesmo problema
      • Acho que seria bom ter primitivas eficientes de IPC em cima de APIs de alto nível para padrões comuns
    • Concordo com a opinião de que a codificação JSON é uma grande barreira para comunicação
      • Fico pensando qual é o tamanho do overhead computacional global causado pelo processamento de JSON nas comunicações e se seria melhor simplesmente enviar os bytes em formatos fixos ou mais eficientes de parsear, como ASN.1
    • Sou contra a ideia de a equipe do V8 investir mais agressivamente nisso e recomendaria que desenvolvedores com esse problema procurassem outras ferramentas
      • Não acho que Node/V8 seja muito adequado para backend ou problemas de computação de alto desempenho
      • JavaScript foi moldado para uso na web e vai continuar assim por muito tempo, então a equipe do V8 não precisa resolver isso
      • A equipe do TypeScript também migrou para Go, e até é possível automatizar conversão entre linguagens
    • Houve literalmente só uma vez em que descarregar trabalho para Workers economizou mais tempo do que o gasto com serialização/desserialização
      • Quando os dados ficam grandes, o custo da passagem de mensagens cresce e acaba equivalendo ao ganho do paralelismo
  • Fiquei muito impressionado com o quanto o desempenho da serialização de números de ponto flutuante melhorou nos últimos 10 e poucos anos

    • Converter valores IEEE de ponto flutuante para strings decimais em UTF-8 e depois fazer a conversão de volta não é só lento, mas também muito instável
      • Como os valores representáveis exatamente em binário e em decimal são diferentes, podem surgir pequenos erros
  • Dizem que, se JSON.stringify receber os argumentos replacer ou space, o fast path não é aplicado

    • Então fico na dúvida se usar JSON.stringify(data, null, 0) ainda permite o fast path ou se os argumentos precisam ser undefined
  • O algoritmo de escaping SWAR[1] é muito parecido com o que foi implementado no Folly JSON[2]

  • Não questiono o valor do trabalho em si, mas tenho curiosidade sobre problemas concretos ou dados mostrando em que casos JSON.stringify realmente dominava o runtime no ecossistema real do V8

    • Não precisa necessariamente ocupar uma fatia esmagadora do tempo de execução; como isso é chamado em centenas de milhões de páginas por dia, o efeito global de economia de energia deve ser considerável
  • Acho que a performance do v8 não recebe elogios suficientes; hoje em dia o JS ficou absurdamente rápido

    • É realmente impressionante, acho um bom exemplo de “com um bilhão de dólares dá para resolver qualquer problema”
      • Seria ótimo se o JS evoluísse mais, tipo “strict”, “stricter”, para virar uma linguagem mais simples e fácil para compilação/JIT
    • Por outro lado, o v8 foi otimizado tão ao extremo que talvez existam só umas 100 pessoas no mundo que realmente entendam seu funcionamento interno, e a maioria dos desenvolvedores deve pensar “por que meu JS não é rápido?”
  • Tenho curiosidade sobre quão extraordinário isso é em comparação com outros ecossistemas

    • Trabalho com serialização JSON há mais de 10 anos, mas quase nunca me preocupei com isso porque sempre foi rápida demais para virar problema
    • O simdjson consegue processar GB por segundo por núcleo e, considerando otimizações como prefetching e branch prediction, acho que a serialização JSON pode ser ignorada na maioria dos workloads reais
    • O maior problema do JSON é o overhead de IO; não importa quão rápido seja o serializer se você ainda precisa gravar um blob de 100MB no armazenamento toda vez
  • “No indexed properties on objects” — dizem que o fast path é otimizado apenas para objetos comuns com chaves baseadas em strings; se houver propriedades indexadas, ele cai no slow path

    • Queria entender o motivo
    • Isso quer dizer que objetos com chaves que parecem inteiros são serializados como arrays JSON? Não é possível que seja isso…?
  • Gostei da abordagem de segmented buffer; antes era preciso montar manualmente truques com rope em bibliotecas userland como fast-json-stringify, mas agora, por ser nativo, ficou muito melhor

    • Tenho curiosidade se essas condições de bailout (por exemplo: replacer, space, .toJSON() customizado) aparecem muito na prática e se, nesses casos, há fallback imediato para o caminho lento
  • O V8 é excelente, mas por causa do próprio JS ele fica atrás de LuaJIT ou da JVM em desempenho

    • A JVM tem tempo de aquecimento maior, mas ainda assim é melhor que JS
    • O problema é o JS; acho que o V8 é muito mais avançado do que LuaJIT e JVM
      • Java tem menos restrições de tempo real (há um compilador), e isso é uma vantagem
    • Muito do overhead do JS vem da sua natureza dinâmica
      • O asm.js proibia comportamentos dinâmicos como mudança de shape dos objetos, então dava para pular muitas checagens
    • Contesto a expressão “até a JVM”; a JVM é de primeira linha