DEV Community

Marcelo Matz
Marcelo Matz

Posted on

Benchmark Go: Marshal x Encoder com requisição http

Este artigo tem a obrigação de começar explicando que sim, eu sei que sistematicamente realizar requisições de rede dentro de um benchmark não é uma prática recomendada devido à variabilidade e à falta de controle sobre a rede, que pode levar a resultados de benchmark inconsistentes. E provavelmente vai. Mas eu vou fazer mesmo assim.

Faz um Bench!

Nos meus estudos em programação eu aprendi que, quando eu tenho dúvida sobre o que usar, testar é a melhor forma de entender o que faz mais sentido usar.

Neste artigo eu vou falar sobre testes usando Marshal e Encoder, e também vou fazer a deserialização de um JSON usando Unmarshal e Decoder a partir de uma requisição HTTP.

Um pouco de história

Quem é melhor, Marshal ou Encoder?

Em Go, json.Marshal e json.NewEncoder são usados para converter a estrutura de dados em Go para JSON, mas eles são usados de maneiras diferentes.

A função json.Marshal retorna uma representação JSON da estrutura de dados Go na forma de uma []byte e um error caso ocorra algum.

Por outro lado, json.NewEncoder pega um io.Writer (como um os.File ou http.ResponseWriter) e retorna um novo *json.Encoder que pode ser usado para transmissão de stream da estrutura de dados Go para o io.Writer em formato JSON.

Aqui um exemplo usando json.Marshal:

package main

import (
    "encoding/json"
    "fmt"
    "log"
)

type Person struct {
    Name string
    Age  int
}

func main() {
    p := &Person{"Ada", 36}
    b, err := json.Marshal(p)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(b))
}
Enter fullscreen mode Exit fullscreen mode

Outro exemplo usando json.NewEncoder:

package main

import (
    "encoding/json"
    "log"
    "os"
)

type Person struct {
    Name string
    Age  int
}

func main() {
    p := &Person{"Ada", 36}
    err := json.NewEncoder(os.Stdout).Encode(p)
    if err != nil {
        log.Fatal(err)
    }
}
Enter fullscreen mode Exit fullscreen mode

Ambas as amostras de código produzirão a mesma saída JSON {"Name":"Ada","Age":36}.

Em resumo

A principal diferença de desempenho entre json.Marshal e json.NewEncoder está na alocação de memória.

json.Marshal converte a estrutura de dados em Go em uma sequência JSON completamente na memória e, em seguida, retorna essa sequência em formato []byte.

Esta abordagem é rápida e simples, mas pode ser um problema se você estiver tentando processar uma grande quantidade de dados de uma vez - você pode facilmente ficar sem memória.

Por outro lado, json.NewEncoder escreve a saída JSON diretamente para um io.Writer como um fluxo, por isso é mais eficiente em termos de memória. Ele não precisa alocar espaço para a estrutura de dados inteira em memória de uma só vez, então você pode processar volumes de dados muito maiores sem se preocupar com problemas de memória.

Se você está manipulando uma pequena quantidade de dados e precisa de uma sequência JSON para uso no programa, json.Marshal é uma escolha adequada.

Se você estiver processando uma grande quantidade de dados ou quer enviar o JSON diretamente para um http.ResponseWriter ou para um arquivo, json.NewEncoder é geralmente uma opção melhor devido ao seu menor uso de memória.

No entanto, a diferença no desempenho e no uso de memória entre os dois provavelmente só será perceptível para estruturas de dados muito grandes.

Para a maioria dos usos diários, você pode usar o que achar mais conveniente e quando alguém perguntar para você qual é melhor, responda: depende!

Além do Go

Pacotes de terceiros que fazem a mão do JSON

Existem outras opções e pacotes de terceiros disponíveis na linguagem Go para manipulação de JSON. Neste caso eu vou falar de uma em especial.

jsoniter: jsoniter é uma biblioteca que afirma ser compatível com encoding/json mas 4x a 6x mais rápida.

O link para o repo do jsoniter.

Usar o JsonIter em Go é simples como usar qualquer outro pacote externo.

Aqui um exemplo:

package main

import (
    "fmt"
    "github.com/json-iterator/go"
)

type Person struct {
    Name string
    Age  int
}

func main() {
    var json = jsoniter.ConfigCompatibleWithStandardLibrary
    p := &Person{"Ada", 36}
    b, err := json.Marshal(p)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(b))
}
Enter fullscreen mode Exit fullscreen mode

Se o desempenho for um problema crítico, você pode eventualmente considerar a escrita de suas próprias funções de serialização / des-serialização. No entanto, isso normalmente só é necessário em circunstâncias muito específicas e as diferenças de desempenho entre os pacotes prontos para uso são geralmente pequenas para a maioria dos casos de uso.

Em resumo nós temos os recursos nativos do Go onde podemos tanto usar o Marshal quanto o Encoder e podemos usar o jsoniter também com Marshal e Encoder.

Vamos escrever um teste para comparar eles, fazendo uma requisição http para a API do ViaCep.

Como eu vou fazer uma requisição http, vou utilizar a deserialização com Unmarshal e Decoder. Bora!


Deserializando o JSON de um request HTTP

O exemplo de código que vamos usar:

package main

import (
    "bytes"
    "encoding/json"
    "io/ioutil"
    "net/http"
    "testing"

    jsoniter "github.com/json-iterator/go"
)

type Info struct {
    Cep         string `json:"cep"`
    Logradouro  string `json:"logradouro"`
    Complemento string `json:"complemento"`
    Bairro      string `json:"bairro"`
    Localidade  string `json:"localidade"`
    Uf          string `json:"uf"`
    Unidade     string `json:"unidade"`
    Ibge        string `json:"ibge"`
    Gia         string `json:"gia"`
}

func fetchData(url string) ([]byte, error) {
    resp, err := http.Get(url)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    return ioutil.ReadAll(resp.Body)
}

func BenchmarkEncodingJsonUnmarshal(b *testing.B) {
    data, _ := fetchData("https://viacep.com.br/ws/01001000/json/")
    info := &Info{}
    for i := 0; i < b.N; i++ {
        if err := json.Unmarshal(data, &info); err != nil {
            b.Fatal(err)
        }
    }
}

func BenchmarkEncodingJsonDecoder(b *testing.B) {
    data, _ := fetchData("https://viacep.com.br/ws/01001000/json/")
    info := &Info{}
    for i := 0; i < b.N; i++ {
        if err := json.NewDecoder(bytes.NewBuffer(data)).Decode(&info); err != nil {
            b.Fatal(err)
        }
    }
}

func BenchmarkJsoniterUnmarshal(b *testing.B) {
    data, _ := fetchData("https://viacep.com.br/ws/01001000/json/")
    info := &Info{}
    var json = jsoniter.ConfigCompatibleWithStandardLibrary
    for i := 0; i < b.N; i++ {
        if err := json.Unmarshal(data, &info); err != nil {
            b.Fatal(err)
        }
    }
}

func BenchmarkJsoniterDecoder(b *testing.B) {
    data, _ := fetchData("https://viacep.com.br/ws/01001000/json/")
    info := &Info{}
    var json = jsoniter.ConfigCompatibleWithStandardLibrary
    for i := 0; i < b.N; i++ {
        if err := json.NewDecoder(bytes.NewBuffer(data)).Decode(&info); err != nil {
            b.Fatal(err)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Em cada um desses benchmarks, estamos fazendo uma chamada GET para a API, obtendo os dados em um formato JSON, e em seguida deserializando esses dados JSON para uma instância do struct Info.

Lembre-se de cuidar com carinho dos erros. Aqui estou ignorando o erro do fetchData para manter o foco na comparação de desempenho entre Unmarshal e Decoder. O erro que erre!

Você pode acessar o repositório do código no GitHub e rodar o teste na sua máquina. Na minha máquina funcionou!

Os testes são:

Nos benchmarks que estão no código do teste que foi feito, existem quatro operações sendo testadas:

  • a deserialização do JSON usando json.Unmarshal (BenchmarkEncodingJsonUnmarshal)
  • a deserialização usando json.NewDecoder (BenchmarkEncodingJsonDecoder)
  • a deserialização usando jsoniter.Unmarshal (BenchmarkJsoniterUnmarshal)
  • deserialização usando jsoniter.NewDecoder (BenchmarkJsoniterDecoder)

Detalhe: As operações estão rodando em uma máquina Apple M1 (goos:darwin, goarch: arm64).

Os resultados mostram que jsoniter (tanto Unmarshal quanto Decoder) é mais rápido que o pacote encoding/json padrão.

Isso é consistente com a reputação de jsoniter de ser uma biblioteca de JSON mais rápida, já que foi projetada com o desempenho em mente.

Alguns dados do bench

BenchmarkEncodingJsonUnmarshal: A operação de deserialização usando json.Unmarshal foi concluída em 2233 nanossegundos (ns) por operação. Foram realizadas 496503 operações.

BenchmarkEncodingJsonDecoder: A operação de deserialização usando json.NewDecoder foi concluída em 2678 ns por operação. Foram realizadas 484986 operações. Esse número é maior do que Unmarshal possivelmente porque Decoder é mais adequado para fluxos de dados.

BenchmarkJsoniterUnmarshal: A operação de deserialização usando jsoniter.Unmarshal foi concluída em 538.3 ns por operação. Foram realizadas 2000696 operações.

BenchmarkJsoniterDecoder: A operação de deserialização usando jsoniter.NewDecoder foi concluída em 676.8 ns por operação. Foram realizadas 1633261 operações.

Observe que, apesar de jsoniter ser mais rápido que json, existem outros aspectos a considerar ao escolher qual usar.

Alguns dos aspectos mais notáveis seriam a compatibilidade da API (a API do json é estável desde Go 1, enquanto a de jsoniter pode não ser) e a quantidade de recursos disponíveis (há provavelmente mais tutoriais, exemplos de código, e respostas do StackOverflow para json do que para jsoniter).

É isso! Se você gostou (ou não) considere deixar um comentário.

Top comments (0)