No meu projeto atual, trabalho com protobuf não apenas para GRPC, mas também como formato de mensagem RabbitMQ . Embora as vantagens do protobuf não estejam limitadas à sua velocidade, eu queria saber se ele é realmente tão rápido em comparação com as bibliotecas JSON , especialmente no mundo ruby. Resolvi fazer alguns benchmarks para checar, mas antes quero acrescentar uma breve introdução a cada formato.
É um sistema de mensagens multiplataforma rápido e compacto, projetado tendo em mente a compatibilidade com versões anteriores e posteriores. Consiste em uma linguagem de definição e compiladores específicos da linguagem.
Ele funciona perfeitamente para pequenos dados semelhantes a objetos, tem grande compatibilidade com versões anteriores e futuras, é rápido (ainda não temos certeza) e é mais compacto que JSON, por exemplo, mas tem algumas limitações como não oferecer suporte a comparação direta (você precisa desserializar objetos para comparar).
Não é compactado e alguns formatos específicos podem funcionar melhor para seus dados (por exemplo, JPEG). Não é autodescritivo.
Consulte os documentos oficiais para obter mais detalhes.
JSON é uma abreviação de notação de objeto JavaScript . Um formato de dados baseado em texto, que foi originalmente usado em JavaScript, mas depois se espalhou amplamente como um formato de comunicação não apenas entre aplicativos JS e back-end, mas também entre microsserviços e tem vários outros usos.
Ele usa strings como chaves e tem uma string, número, booleano, objeto, array e nul como tipos disponíveis para valor. A principal vantagem disso é que é legível por humanos e muito fácil de serializar e analisar a linguagem de programação.
Veja o site para mais detalhes.
Peguei três bibliotecas JSON ruby populares. Eles são Oj, Yajl e a biblioteca JSON padrão. Para o protobuf, eu uso o google protoc padrão com o google ruby gem.
Medirei diferentes tipos específicos de carga útil para ver qual tipo de dados mostraremos a maior diferença, desde carga complexa com uma mistura de tipos de campo.
Você pode ver todo o código aqui https://github.com/alexstaro/proto-vs-json .
Como hardware utilizo notebook com AMD Ryzen 3 PRO 5450U e 16gb de ram ddr4.
Como sistema operacional, uso o Ubuntu 22.10 cinético.
Ruby versão 3.2.1 foi instalado via asdf.
Para benchmarking, eu uso benchmark/ips gem ( https://github.com/evanphx/benchmark-ips )
A configuração fica assim:
Benchmark.ips do |x| x.config(time: 20, warmup: 5) x.report('Yajl encoding') do Yajl::Encoder.encode(data) end ... x.compare! end
Começaremos apenas com números inteiros. Os números são muito difíceis para o JSON, então esperamos que o protobuf esteja longe de outros concorrentes.
Os dados do teste:
data = { field1: 2312345434234, field2: 31415926, field3: 43161592, field4: 23141596, field5: 61415923, field6: 323423434343443, field7: 53141926, field8: 13145926, field9: 323423434343443, field10: 43161592 }
Resultados de referência:
protobuf encoding: 4146929.7 i/s Oj encoding: 1885092.0 i/s - 2.20x slower standard JSON encoding: 505697.5 i/s - 8.20x slower Yajl encoding: 496121.7 i/s - 8.36x slower
Não há dúvida de que o protobuf é um vencedor absoluto, mas e se tornarmos o teste mais próximo do cenário do mundo real - quase sempre criamos mensagens proto apenas para serialização.
Aqui estão os resultados:
protobuf encoding: 4146929.7 i/s Oj encoding: 1885092.0 i/s - 2.20x slower standard JSON encoding: 505697.5 i/s - 8.20x slower Yajl encoding: 496121.7 i/s - 8.36x slower protobuf with model init: 489658.0 i/s - 8.47x slower
O resultado não é tão óbvio. Eu esperava que a codificação com a inicialização da mensagem fosse mais lenta, mas não a mais lenta.
Vamos verificar a desserialização:
protobuf parsing: 737979.5 i/s Oj parsing: 448833.9 i/s - 1.64x slower standard JSON parsing: 297127.2 i/s - 2.48x slower Yajl parsing: 184361.1 i/s - 4.00x slower
Não há surpresas aqui.
Em termos de tamanho de carga útil, o protobuf é quase 4 vezes mais compacto em comparação com o json:
JSON payload bytesize 201 Protobuf payload bytesize 58
Espera-se que os doubles sejam os payloads mais difíceis para JSON, vamos verificar isso.
Nossa carga útil:
data = { field1: 2312.345434234, field2: 31415.926, field3: 4316.1592, field4: 23141.596, field5: 614159.23, field6: 3234234.34343443, field7: 53141.926, field8: 13145.926, field9: 323423.434343443, field10: 43161.592 }
Resultado:
protobuf encoding: 4814662.9 i/s protobuf with model init: 444424.1 i/s - 10.83x slower Oj encoding: 297152.0 i/s - 16.20x slower Yajl encoding: 160251.9 i/s - 30.04x slower standard JSON encoding: 158724.3 i/s - 30.33x slower
Protobuf é muito mais rápido mesmo com a inicialização do modelo. Vamos verificar a desserialização:
Comparison: protobuf parsing: 822226.6 i/s Oj parsing: 395411.3 i/s - 2.08x slower standard JSON parsing: 241438.7 i/s - 3.41x slower Yajl parsing: 157235.7 i/s - 5.23x slower
Ainda sem surpresas aqui.
e o tamanho da carga útil:
JSON payload bytesize 211 Protobuf payload bytesize 90
Não quatro vezes, mas ainda perceptível.
Espera-se que strings sejam mais fáceis para JSON, vamos verificar isso.
carga útil:
data = { field1: "2312.345434234", field2: "31415.926", field3: "4316.1592", field4: "23141.596", field5: "614159.23", field6: "3234234.34343443", field7: "53141.926", field8: "13145.926", field9: "323423.434343443", field10: "43161.592" }
Resultados de bancada:
Comparison: protobuf encoding: 3990298.3 i/s oj encoder: 1848941.3 i/s - 2.16x slower yajl encoder: 455222.0 i/s - 8.77x slower standard JSON encoding: 444245.6 i/s - 8.98x slower protobuf with model init: 368818.3 i/s - 10.82x slower
Desserialização:
Comparison: protobuf parser: 631262.5 i/s oj parser: 378697.6 i/s - 1.67x slower standard JSON parser: 322923.5 i/s - 1.95x slower yajl parser: 187593.4 i/s - 3.37x slower
O tamanho da carga útil:
JSON payload bytesize 231 Protobuf payload bytesize 129
Apesar de termos separado o banco de inteiros, é interessante como o protobuf lida com as coleções.
Aqui estão os dados:
data = { field1: [ 2312345434234, 31415926, 43161592, 23141596, 61415923, 323423434343443, 53141926, 13145926, 323423434343443, 43161592 ] }
Bancada de serialização:
Comparison: protobuf encoding: 4639726.6 i/s oj encoder: 2929662.1 i/s - 1.58x slower standard JSON encoding: 699299.2 i/s - 6.63x slower yajl encoder: 610215.5 i/s - 7.60x slower protobuf with model init: 463057.9 i/s - 10.02x slower
Bancada de desserialização:
Comparison: oj parser: 1190763.1 i/s protobuf parser: 760307.3 i/s - 1.57x slower standard JSON parser: 619360.4 i/s - 1.92x slower yajl parser: 414352.4 i/s - 2.87x slower
Para ser honesto, os resultados da desserialização são bastante inesperados aqui.
Vamos verificar o tamanho da carga útil:
JSON payload bytesize 121 Protobuf payload bytesize 50
Decidi verificar se um array de doubles compartilha o mesmo comportamento.
dados:
data = { field1: [ 2312.345434234, 31415.926, 4316.1592, 23141.596, 614159.23, 3234234.34343443, 53141.926, 13145.926, 323423.434343443, 43161.592 ] }
Serialização:
Comparison: protobuf encoding: 7667558.9 i/s protobuf with model init: 572563.4 i/s - 13.39x slower Oj encoding: 323818.1 i/s - 23.68x slower Yajl encoding: 183763.3 i/s - 41.73x slower standard JSON encoding: 182332.3 i/s - 42.05x slower
Desserialização:
Comparison: Oj parsing: 953384.6 i/s protobuf parsing: 883899.0 i/s - 1.08x slower standard JSON parsing: 452799.0 i/s - 2.11x slower Yajl parsing: 356091.2 i/s - 2.68x slower
Obtivemos resultados semelhantes aqui. Parece que o protobuf tem alguns problemas com arrays.
Tamanho da carga útil:
JSON payload bytesize 131 Protobuf payload bytesize 82
Como uma carga útil "complexa", eu zombei de alguns dados do usuário com postagens e comentários para essas postagens para torná-los mais parecidos com aplicativos da vida real.
data = { user_id: 12345, username: 'johndoe', email: '[email protected]', date_joined: '2023-04-01T12:30:00Z', is_active: true, profile: { full_name: 'John Doe', age: 30, address: '123 Main St, Anytown, USA', phone_number: '+1-555-123-4567' }, posts: [ { post_id: 1, title: 'My first blog post', content: 'This is the content of my first blog post.', date_created: '2023-04-01T14:00:00Z', likes: 10, tags: ['blog', 'first_post', 'welcome'], comments: [ { comment_id: 101, author: 'Jane', content: 'Great first post!', date_created: '2023-04-01T15:00:00Z', likes: 3 }, ... ] }, ... ] }
Os resultados:
Comparison: protobuf encoding: 1038246.0 i/s Oj encoding: 296018.6 i/s - 3.51x slower Yajl encoding: 125909.6 i/s - 8.25x slower protobuf with model init: 119673.2 i/s - 8.68x slower standard JSON encoding: 115773.4 i/s - 8.97x slower Comparison: protobuf parsing: 291605.9 i/s Oj parsing: 76994.7 i/s - 3.79x slower standard JSON parsing: 64823.6 i/s - 4.50x slower Yajl parsing: 34936.4 i/s - 8.35x slower
E tamanho da carga útil:
JSON payload bytesize 1700 Protobuf payload bytesize 876
Vemos aqui o comportamento esperado com a codificação protobuf pura em primeiro lugar, no entanto, se olharmos para o nosso exemplo do “mundo real”, veremos que não é mais rápido que a codificação JSON padrão.
Se você está mudando de JSON para Protobuf apenas pela velocidade, pode não valer a pena.
O motivo para usar o Protobuf deve ser a incrível definição de esquema entre linguagens para troca de dados - não um aumento de desempenho.
A imagem principal deste artigo foi gerada peloAI Image Generator do HackerNoon por meio do prompt "linguagem de programação".