Performance: NHibernate versus ADO.NET

Comments
adhoc ado.net cache nhibernate performance stored procedures
31 August 2010

Disclaimer

Os testes neste post apresentados não representam uma amostra exata ou fidedigna que represente uma condição real de acesso a dados de uma aplicação a fim de comparar a performance das duas tecnologias. A idéia é apenas mostrar alguns dados e exemplos com o objetivo de dismistificar a idéia de que ORM é uma ferramenta lenta e estes testes são apenas um exercício.

Sou usuário de NHibernate e defendo o uso de ORM’s portanto as conclusões apresentadas com certeza não são as mais imparciais possíveis.

Motivado por esta thread no DNA hoje decidi fazer alguns testes de perfomance pra comparar acesso a dados utilizando ADO.NET nativo (queries AdHoc e Stored Procedures) versus NHibernate.

Sempre rolam diversas discussões a respeito do assunto e a conclusão que geralmente se chega é de que qualquer ORM vai ser mais lento do que uma chamada nativa. É uma conclusão óbvia já que utilizar um ORM é adicionar uma camada de abstração a mais dentro da nossa aplicação. No entanto, até então eu nunca havia efetuado nenhuma medição pra ver qual a diferença de performance.

Todos os testes foram executados na minha máquina com código compilado em modo Release com banco de dados Sql Server 2008 também local.

Vamos aos testes.

Cenário de testes

Criei uma tabela (Product) e populei com cerca de 500 registros vindos do AdventureWorks (banco de dados exemplo do SqlServer).

Modelo de Dados

Contra essa tabela executei duas categorias de testes:

  1. Carregar todos os registros da tabela em um List<>

  2. Carregar apenas um registro

Pra ficar mais interessante fiz algumas variações dos testes:

  • Query AdHoc/Inline

  • Query AdHoc/Inline com hidratação[1] via reflection

  • Query utilizando uma procedure

  • NHibernate com LINQ

  • NHibernate com HQL

  • NHibernate com Criteria

  • Nhibernate com Projections

Como rodando uma única vez não foi possível obter dados suficientes, executei cada teste dentro de um loop com 1000 (numberOfIterations = 1000) iterações que resultou no código abaixo:

Método principal:

static void Main(string[] args)
{
    for (int i = 1; i <= 3; i++)
    {
        Console.WriteLine("Test " + i.ToString());
        Console.WriteLine("======================");

        SqlAdHocAllProducts();
        SqlAdHocAllProductsWithReflection();
        SProcAllProducts();
        NHibernateAllProductsWithLinq();
        NHibernateAllProductsWithHql();
        NHibernateAllProductsWithCriteria();
        NHibernateAllProductsWithProjections();

        SqlAdHocOneProduct();
        SqlAdHocOneProductWithReflection();
        SProcOneProduct();
        NHibernateOneProduct();

        Console.WriteLine();
    }

    Console.ReadLine();
}

Um dos métodos usando Sql AdHoc e outro usando NHibernate:

private static void SqlAdHocAllProducts()
{
    List<Product> allProducts = null;

    var connectionString = "Data Source=(local);Integrated Security=SSPI;Database=TDC2010;";
    var select = @"SELECT P.Id, P.Description, P.Name, P.Price FROM dbo.Product P";

    var connection = new SqlConnection(connectionString);
    connection.Open();

    Stopwatch watch = new Stopwatch();
    watch.Start();

    for (int i = 0; i < numberOfIterations; i++)
    {
        allProducts = new List<Product>();
        var command = new SqlCommand(select, connection);
        var reader = command.ExecuteReader();

        while (reader.Read())
        {
            allProducts.Add(new Product()
                {

                    Id = Convert.ToInt32(reader["Id"]),
                    Name = Convert.ToString(reader["Name"]),
                    Description = Convert.ToString(reader["Description"]),
                    Price = Convert.ToDecimal(reader["price"])
                });
        }

        reader.Close();
    }

    watch.Stop();

    connection.Close();
    connection.Dispose();

    Console.WriteLine(
        "Loading " + allProducts.Count + " Products with Sql AdHoc took " + watch.ElapsedMilliseconds + " ms");
}

private static void NHibernateAllProductsWithLinq()
{
    List<Product> allProducts = null;

    var session = CreateForSqlServer().OpenSession();

    Stopwatch watch = new Stopwatch();
    watch.Start();

    for (int i = 0; i < numberOfIterations; i++)
    {
        allProducts = session.Linq<Product>().ToList();
    }

    watch.Stop();

    session.Close();

    Console.WriteLine(
        "Loading " + allProducts.Count + " Products with NHibernate took " + watch.ElapsedMilliseconds + " ms");
}

Vou omitir o restante dos métodos para evitar duplicações já que eles são apenas variações dos dois exemplos acima.

Resultado Geral

Resultados dos Testes

Análise dos Resultados

Stored Procedures versus Queries AdHoc/Inline [500 registros]

A diferença entre o uso de stored procedures e queries AdHoc é praticamente inexistente. A diferença média medida foi de menos de 1%.

Isso acontece, pois não existe complexidade suficiente neste tipo de query para que a armazenagem do plano de execução no banco de dados faça diferença.

Portanto, na grande maioria dos cenários Stored Procedures não são necessárias.

NHibernate versus NHibernate [500 registros]

Na média, todas as variações de consulta utilizando NHibernate também tiveram mais ou menos o mesmo resultado (~1500ms) com exceção do uso de Projections, que levou o dobro (~3000ms) do tempo.

Não conheço o NHibernate suficiente pra afirmar com 100% de certeza o motivo, mas acredito que seja pelo fato de esse tipo de query retornar Arrays bidimensionais que são criados e redimensionados em runtime até que todos os itens possam ser acomodados.

NHibernate versus ADO.NET [500 registros]

Este e o comparativo mais importante. Carregando (e hidratando) 500 registros o ADO.NET é cerca de 30% mais rapido do que o NHibernate. A causa dessa discrepância é uma só e se chama reflection.

Apesar de todas as otimizações o NHibernate utiliza-se de reflection para efetuar a hidratação[1] de todos os objetos e é dai que vem a queda de performance que fica clara quando executamos queries AdHoc usando hidratação via reflection conforme o código abaixo.

private static void SqlAdHocAllProductsWithReflection()
{
    List<Product> allProducts = null;

    var connectionString = "Data Source=(local);Integrated Security=SSPI;Database=TDC2010;";
    var select = @"SELECT P.Id, P.Description, P.Name, P.Price FROM dbo.Product P";

    var connection = new SqlConnection(connectionString);
    connection.Open();

    Stopwatch watch = new Stopwatch();
    watch.Start();

    for (int i = 0; i < numberOfIterations; i++)
    {
        allProducts = new List<Product>();
        var command = new SqlCommand(select, connection);
        var reader = command.ExecuteReader();

        while (reader.Read())
        {
            var newProduct = Activator.CreateInstance<Product>();
            SetProperty(newProduct, "Id", Convert.ToInt32(reader["Id"]));
            SetProperty(newProduct, "Description", Convert.ToString(reader["Description"]));
            SetProperty(newProduct, "Name", Convert.ToString(reader["Name"]));
            SetProperty(newProduct, "Price", Convert.ToDecimal(reader["Price"]));
            allProducts.Add(newProduct);
        }

        reader.Close();
    }

    watch.Stop();

    connection.Close();
    connection.Dispose();

    Console.WriteLine(
"Loading " + allProducts.Count + " Products with Sql AdHoc and Reflection took " + watch.ElapsedMilliseconds + " ms");
}

private static void SetProperty(object instance, string property, object val)
{
    Type t = instance.GetType();
    var prop = t.GetProperty(property, BindingFlags.Instance | BindingFlags.Public);
    prop.SetValue(instance, val, null);
}

Este código quando executado demora cerca de 6000ms, ou 4 vezes mais, do que o código executado pelo NHibernate.

Mas por que a diferença não é de 30%? Porque o NHibernate possui otimizações quanto ao modo de hidratar um objeto via reflection. No meu código acima podemos ver, por exemplo, que toda vez que chamo o método SetProperty o Type da propriedade a ser refletida ainda não está criado. Provavelmente o NHibernate deve manter cache deste tipo de informação (e de outras) a fim de otimizar o processo de hidratação das entidades.

Stored Procedures versus Queries AdHoc/Inline [1 registro]

Mesmo resultado do cenário onde 500 registros são carregados. Não há diferença.

NHibernate versus ADO.NET [1 registro]

Aqui a diferença é gigantesca sendo de quase 4000% a favor do NHibernate. Isso acontece, pois o NHibernate implementa cache nível 1 nativamente então dentro de uma mesma ISession o objeto é carregado apenas uma vez enquanto com ADO.NET é necessário ir ao banco e carregar o objeto diversas vezes.

Conclusões

Olhando para os números apenas, em casos onde diversos registros precisam ser carregados, ADO.NET nativo oferece uma performance superior e parece ser a escolha óbvia. No entanto:

  • Os testes com NHibernate foram executados utilizandos exemplos simples e sem qualquer tipo de otimização.

  • NHibernate oferece nativamente cache de resultados de queries e cache nível 2 que se utilizados iriam exibir um resultado bem próximo ao cenário “NHibernate versus ADO.NET [1 registro]” onde o NHibernate é 4 vezes mais rápido.

  • ADO.NET oferece um custo de desenvolvimento e manutenção altissimo e este custo torna-se ainda maior se utilizado com Stored Procedures que transformam o cenário em algo totalmente caótico de gerenciar devido a dificuldade de manter a rastreabilidade desses diabinhos malignos.

No caso de sistemas OLTP onde as transações carregam unidades individuais e/ou pequenas coleções de entidades por sessão, NHibernate não só é mais rápido como também oferece muito mais flexibilidade pra lidar com praticamente todos os cenários existentes. Este post do Ayende mostra 25 funcionalidades importantes (cache, gerenciamento de concorrência, etc) que você vai precisar quando estiver lidando com dados e que custariam muito caro (tempo e complexidade) caso você queira escreve-las “na mão”.

Em cenários de aplicações de internet onde o número de leituras é infinitamente superior ao número de escritas no banco de dados deve se utilizar cache no front end (IIS), portanto o tempo que se leva pra montar uma página é irrelevante. Mesmo que você carregasse os dados de um servidor remoto via conexão discada não faria diferença uma vez que os dados estivessem em cache.

Por último, se você se encontrar em uma situação utilizando NHibernate onde o acesso a dados é o gargalo da sua aplicação seu problema não é o NHibernate (a não ser que você tenha usado-o de maneira totalmente absurda, mas a probabilidade de você fazer o mesmo com ADO.NET é grande também). Nestes cenários o problema não é o acesso a dados em si, mas o resto da arquitetura que não escala de maneira adequada seja por meio de cache, processamento assincrono, filas, etc.

Sendo assim, acredito que podemos concluir que não faz sentido algum no meio do ano de 2010 utilizar queries AdHoc ou Stored Procedures em aplicações LoB. :)

O código completo utilizado está no GitHub e pode ser acessado online ou baixado em formato zip.

[1] – Hidratação é o processo de preenchimento (filling) das propriedades de uma entidade


<< Material da palestra no TDC2010 - ORM: Por que isso te interessa?
Lendo e consumindo XML com dynamic em C# 4>> 
comments powered by Disqus
tucaz

tucaz

.NET Software Developer
About
All Posts
RSS
@tucaz
GitHub