- 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.parsepara 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
Comentários no Hacker News
Sinto que a codificação JSON é um grande gargalo na comunicação entre processos no NodeJS
JSON.stringifyfoi um dos maiores fatores que limitavam o desempenho em serviços NodeFiquei muito impressionado com o quanto o desempenho da serialização de números de ponto flutuante melhorou nos últimos 10 e poucos anos
Dizem que, se
JSON.stringifyreceber os argumentos replacer ou space, o fast path não é aplicadoJSON.stringify(data, null, 0)ainda permite o fast path ou se os argumentos precisam serundefinedO 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.stringifyrealmente dominava o runtime no ecossistema real do V8Acho que a performance do v8 não recebe elogios suficientes; hoje em dia o JS ficou absurdamente rápido
Tenho curiosidade sobre quão extraordinário isso é em comparação com outros ecossistemas
“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
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
.toJSON()customizado) aparecem muito na prática e se, nesses casos, há fallback imediato para o caminho lentoO V8 é excelente, mas por causa do próprio JS ele fica atrás de LuaJIT ou da JVM em desempenho