Google Protocol Buffers – Voando baixo com serializações utilizando o Protobuf-net

Sempre estamos serializando ou deserializando algum objeto, manualmente ou deixando o framework fazer o trabalho “sujo”, não podemos esquecer que isso tem um custo, dependendo do tipo de serialização que escolhemos esse custo pode ser muito alto.

E para nossa alegria o Google sabe isso, e ele criou o Google Protocol Buffers, é simples e rápido, na verdade muito rápido!!! Recomendo a leitura da documentação para entender o funcionamento do Protocol Buffers.

Uma descrição rápida do Protocol Buffers.

protocol buffers is the name of the binary serialization format used by Google for much of their data communications. It is designed to be:

small in size – efficient data storage (far smaller than xml) cheap to process – both at the client and server platform independent – portable between different programming architectures extensible – to add new data to old messages

Já que o Google mostrou pra todo mundo como fazer, uma implementação do Protocol Buffers foi criada em .net, chamada Protobuf-net, ela funciona com praticamente todas as versões do .NET, suporta Silverlight, Mono e está no Google Code, então quem quiser pode ajudar no projeto! ;)

Serializar e deserializar objetos com o Protobuf-net é simples, então não vou ficar mostrando exemplos, mas vou colocar um teste de desempenho que fiz para comparar alguns tipos de serializações nativas do .NET contra o Protobuf-net.

A idéia do teste é simples, vou adicionar em uma lista de Albuns 100.000 albuns, a primeira parte que consistem em serializar vou percorrer essa lista e criar uma nova lista de byte[] com cada album serializado, e a parte da deserialização percorro a lista com byte[] da serialização e crio uma nova lista com Albuns.

A classe Album é simples, e está marcada com nossos conhecidos atributos de serialização e de contratos.

[Serializable]
[DataContract]
public class Album
{
    [DataMember]
    public string Titulo { get; set; }
    [DataMember]
    public int AnoDeLancamento { get; set; }
}

E agora o código que cria a lista com os albuns que serão serializados.

private const int TotalDeAlbuns = 100000;
private static readonly List Albuns = Enumerable.Range(0, TotalDeAlbuns).Select(x => CriarAlbum(x)).ToList(); 

private static Album CriarAlbum(int numeroDoAlbum)
{
    return new Album {AnoDeLancamento = numeroDoAlbum, Titulo = numeroDoAlbum.ToString()};
}

Eu fiz os testes utilizando as seguintes formatos de serialização:

[list_blue]

  • Protobuf-net
  • BinaryFormatter
  • DataContractSerializer
  • XmlSerializer

[/list_blue]

E agora finalmente os códigos, são iguais só alterando o formato da serialização.

[tabs]

[tab title="protobuf-net"]

private static void TestesComProtobufNet()
{
    var albunsSerializados = new List();
    var albunsDesserializados = new List();

    Contador.Iniciar();

    foreach (var album in Albuns)
    {
        using (var memoryStream = new MemoryStream())
        {
            Serializer.Serialize(memoryStream, album);
            albunsSerializados.Add(memoryStream.ToArray());
        }
    }

    Contador.Parar();

    Console.WriteLine("Serializando {0} objetos com Protobuf-net: {1} ms", TotalDeAlbuns, Contador.TempoTotalEmMilisegundos);

    Contador.Iniciar();

    foreach (var album in albunsSerializados)
    {
        using (var memoryStream = new MemoryStream(album))
        {
            albunsDesserializados.Add(Serializer.Deserialize(memoryStream));
        }
    }

    Contador.Parar();

    Console.WriteLine("Deserializando {0} objetos com Protobuf-net: {1} ms", TotalDeAlbuns, Contador.TempoTotalEmMilisegundos);
    Console.WriteLine();
}

[/tab]

[tab title="binaryformatter"]

private static void TestesComProtobufBinaryFormatter()
{
    var albunsSerializados = new List();
    var albunsDesserializados = new List();

    var binaryFormatter = new BinaryFormatter();

    Contador.Iniciar();

    foreach (var album in Albuns)
    {
        using (var memoryStream = new MemoryStream())
        {
            binaryFormatter.Serialize(memoryStream, album);
            albunsSerializados.Add(memoryStream.ToArray());
        }
    }

    Contador.Parar();

    Console.WriteLine("Serializando {0} objetos com BinaryFormatter: {1} ms", TotalDeAlbuns, Contador.TempoTotalEmMilisegundos);

    Contador.Iniciar();

    foreach (var album in albunsSerializados)
    {
        using (var memoryStream = new MemoryStream(album))
        {
            albunsDesserializados.Add((Album)binaryFormatter.Deserialize(memoryStream));
        }
    }

    Contador.Parar();

    Console.WriteLine("Deserializando {0} objetos com BinaryFormatter: {1} ms", TotalDeAlbuns, Contador.TempoTotalEmMilisegundos);
    Console.WriteLine();
}

[/tab]

[tab title="datacontractserializer"]

private static void TestesComDataContractSerializer()
{
    var albunsSerializados = new List();
    var albunsDesserializados = new List();

    var dataContractSerializer = new DataContractSerializer(typeof (Album));

    Contador.Iniciar();

    foreach (var album in Albuns)
    {
        using (var memoryStream = new MemoryStream())
        {
            dataContractSerializer.WriteObject(memoryStream, album);
            albunsSerializados.Add(memoryStream.ToArray());
        }
    }

    Contador.Parar();

    Console.WriteLine("Serializando {0} objetos com DataContractSerializer: {1} ms", TotalDeAlbuns, Contador.TempoTotalEmMilisegundos);

    Contador.Iniciar();

    foreach (var album in albunsSerializados)
    {
        using (var memoryStream = new MemoryStream(album))
        {
            albunsDesserializados.Add((Album) dataContractSerializer.ReadObject(memoryStream));
        }
    }

    Contador.Parar();

    Console.WriteLine("Deserializando {0} objetos com DataContractSerializer: {1} ms", TotalDeAlbuns, Contador.TempoTotalEmMilisegundos);
    Console.WriteLine();
}

[/tab]

[tab title="xmlserializer"]

private static void TestesComXmlSerializer()
{
    var albunsSerializados = new List();
    var albunsDesserializados = new List();

    var xmlSerializer = new XmlSerializer(typeof (Album));

    Contador.Iniciar();

    foreach (var album in Albuns)
    {
        using (var memoryStream = new MemoryStream())
        {
            xmlSerializer.Serialize(memoryStream, album);
            albunsSerializados.Add(memoryStream.ToArray());
        }
    }

    Contador.Parar();

    Console.WriteLine("Serializando {0} objetos com XmlSerializer: {1} ms", TotalDeAlbuns, Contador.TempoTotalEmMilisegundos);

    Contador.Iniciar();

    foreach (var album in albunsSerializados)
    {
        using (var memoryStream = new MemoryStream(album))
        {
            albunsDesserializados.Add((Album)xmlSerializer.Deserialize(memoryStream));
        }
    }

    Contador.Parar();

    Console.WriteLine("Deserializando {0} objetos com XmlSerializer: {1} ms", TotalDeAlbuns, Contador.TempoTotalEmMilisegundos);
    Console.WriteLine();
}

[/tab]

[/tabs]

Execute a aplicação 3 vezes, em Release, segue o gráfico com a média de tempo de cada formato de serialização. E o Protobuf-net realmente da um show.

É interessante analisar esses tempos, o XmlSerializer é muito lento, e o DataContractSerializer manda bem para serializar, mas para deserializar não é tão bom.

É importante ver que mesmo com o .NET disponibilizando vários formatos de serialização, precisamos sair da “zona de conforto” e buscar melhores alternativas, é claro que isso depende do cenário de cada um, mas caso esteja atendendo cenários críticos onde qualquer segundo faz diferença é sempre bom abrir a cabeça e aceitar novidades ;).

O código fonte do projeto está no github.

Abraços.

  • http://fernandovezzali.com/ Fernando Ayrosa Vezzali

    Excelente artigo

    • Márcio Fábio Althmann

      Obrigado! :)

  • AngeloBelchior

    O grande problema do Protobuf-net é quando se tem um objeto complexo.

    • Márcio Fábio Althmann

      Olá Angelo, e qual é o grande problema?
      Fiz testes com alguns objetos e foi tudo sem problemas, tem algo em especial que enfrentou algum problema?

  • http://pensandoazure.wordpress.com/ Fernando Correia

    Boa dica. Eu também achei esse benchmark interessante:
    http://theburningmonk.com/benchmarks/

  • http://juanlopes.net Juan Lopes

    E vale dizer que o formato é compatível com todas as plataformas onde o protobuf é implementado. Estou usando em Java e está valendo *muito* a pena.