Benchmark: ASP.NET 4.8 vs ASP.NET CORE 3.0

Disponível também em inglês

Um benchmark de tempo de resposta que compara as versões de ASP.NET 4.8 e ASP.NET CORE 3.0 só reafirma o que eu já suspeitava.

Benchmark: ASP.NET 4.8 vs ASP.NET CORE 3.0

Quando começei a desenvolver com ASP.NET CORE percebi algumas mudanças na performance de tempo de resposta em algumas funcionalidades comparando com as mesmas no ASP.NET FRAMEWORK.

Pensei comigo, preciso fazer um benchmark sobre isso comparando esses dois frameworks.

Mas eu não queria fazer algo muito complexo, com dados de memória, processador e tal. Queria algo que não fosse muito difícil de entender.

Algumas informações importantes para quem está chegando agora. Se você é um desenvolvedor .NET mas nunca viu nada de .NET Core, sugiro você ler antes o artigo .NET Core para DesenvolvedoreS .NET.

Benchmark: ASP.NET 4.8 vs ASP.NET CORE 3.0

Eu não usei o BenchmarkDotNet!

Conversei com um amigo a respeito desse artigo e que me sugeriu o uso da lib BenchmarkDotNet, no qual você instala em sua aplicação através do NuGet, enche as classes, propriedades e método de atributos.

Ela roda seu código nos CLR/Framework que você escolher e te dá um resultado detalhado. Para conhecer mais sobre ela clique aqui.

Essa lib é muito interessante, mas não é o foco desse artigo. Eu não queria nada automatizado e que não usasse nada já pronto de mercado.

As premissas foram as seguintes:

– Usar máquina de desenvolvedor
– Usar template padrão do Visual Studio 2019
– Aplicação apenas com Web API (excluir todo o resto)
– Foco em apenas tempo de resposta (em milesegundo)
– Todos os pacotes NuGet devem estar atualizados
– Instalar NuGet Newtonsoft JSON
– Instalar NuGet Dapper (SQL Server)
– Testes em IIS Express / Modo debug
– Usar Postman para os requests
– Sem alteração/manipulação do código fonte. Ou seja, desenvolvimento normal e mundo real.
– As tabelas de endereços do SQL Server possuem mais de 1M de registros.

Os tempos de resposta foram colhidos da seguinte forma:

1 – Executar a aplicação web.
2 – Abrir o Postman.
3 – Fazer o request.
4 – Pegar o resultado do tempo de resposta (em milesegundo).
5 – Realizar três requests.

Eu também acrescentei uma aplicação ASP.NET CORE 2.2 como base de comparação.


Código fonte

    // ASP.NET 4.8
    public sealed class AddressSqlRepository
    {
        public async Task<Address> GetAddressAsync(
            string zipCode)
        {
            using (var connection = CreateConnection())
            {
                await connection.OpenAsync();

                var parameters = new
                {
                    zipCode
                };

                var sql = @"SELECT              a.cod_postal AS ZipCode,
                                                b.des_cidade AS City,
                                                c.des_sigla AS State,
                                                (t.des_tipo_logradouro + ' ' + d.des_logradouro) AS Street, 
                                                r.des_bairro AS Neighborhood
                            FROM                dbo.tb_cep AS a
                            LEFT OUTER JOIN     dbo.tb_cidade AS b ON a.cod_cidade = b.cod_cidade
                            LEFT OUTER JOIN     dbo.tb_estado AS c ON a.cod_estado = c.cod_estado
                            LEFT OUTER JOIN     dbo.tb_logradouro AS d ON a.cod_logradouro = d.cod_logradouro
                            LEFT OUTER JOIN     dbo.tb_tipo_logradouro AS t ON a.cod_tipo_logradouro = t.cod_tipo_logradouro
                            LEFT OUTER JOIN     dbo.tb_bairro AS r ON a.cod_bairro = r.cod_bairro
                            WHERE               a.cod_postal = @zipCode";

                var data = await connection.QueryFirstOrDefaultAsync<Address>(
                    sql,
                    parameters);

                connection.Close();

                return data;
            };
        }
        
        public async Task<IEnumerable<Address>> GetAddressRangeAsync(
            string start,
            string end)
        {
            using (var connection = CreateConnection())
            {
                await connection.OpenAsync();

                var parameters = new
                {
                    start,
                    end
                };

                var sql = @"SELECT              a.cod_postal AS ZipCode,
                                                b.des_cidade AS City,
                                                c.des_sigla AS State,
                                                (t.des_tipo_logradouro + ' ' + d.des_logradouro) AS Street, 
                                                r.des_bairro AS Neighborhood
                            FROM                dbo.tb_cep AS a
                            LEFT OUTER JOIN     dbo.tb_cidade AS b ON a.cod_cidade = b.cod_cidade
                            LEFT OUTER JOIN     dbo.tb_estado AS c ON a.cod_estado = c.cod_estado
                            LEFT OUTER JOIN     dbo.tb_logradouro AS d ON a.cod_logradouro = d.cod_logradouro
                            LEFT OUTER JOIN     dbo.tb_tipo_logradouro AS t ON a.cod_tipo_logradouro = t.cod_tipo_logradouro
                            LEFT OUTER JOIN     dbo.tb_bairro AS r ON a.cod_bairro = r.cod_bairro
                            WHERE               a.cod_postal BETWEEN @start AND @end";

                var data = await connection.QueryAsync<Address>(
                    sql,
                    parameters);

                connection.Close();

                return data;
            };
        }

        private SqlConnection CreateConnection()
        {
            return new SqlConnection(ConfigurationManager.ConnectionStrings["Default"].ConnectionString);
        }
    }

    // ASP.NET CORE
    public sealed class AddressSqlRepository
    {
        private readonly IConfiguration _configuration;

        public AddressSqlRepository(
            IConfiguration configuration)
        {
            _configuration = configuration;
        }

        public async Task<Address> GetAddressAsync(
            string zipCode)
        {
            using (var connection = CreateConnection())
            {
                await connection.OpenAsync();

                var parameters = new
                {
                    zipCode
                };

                var sql = @"SELECT              a.cod_postal AS ZipCode,
                                                b.des_cidade AS City,
                                                c.des_sigla AS State,
                                                (t.des_tipo_logradouro + ' ' + d.des_logradouro) AS Street, 
                                                r.des_bairro AS Neighborhood
                            FROM                dbo.tb_cep AS a
                            LEFT OUTER JOIN     dbo.tb_cidade AS b ON a.cod_cidade = b.cod_cidade
                            LEFT OUTER JOIN     dbo.tb_estado AS c ON a.cod_estado = c.cod_estado
                            LEFT OUTER JOIN     dbo.tb_logradouro AS d ON a.cod_logradouro = d.cod_logradouro
                            LEFT OUTER JOIN     dbo.tb_tipo_logradouro AS t ON a.cod_tipo_logradouro = t.cod_tipo_logradouro
                            LEFT OUTER JOIN     dbo.tb_bairro AS r ON a.cod_bairro = r.cod_bairro
                            WHERE               a.cod_postal = @zipCode";

                var data = await connection.QueryFirstOrDefaultAsync<Address>(
                    sql,
                    parameters);

                await connection.CloseAsync();

                return data;
            };
        }

        public async Task<IEnumerable<Address>> GetAddressRangeAsync(
            string start, 
            string end)
        {
            using (var connection = CreateConnection())
            {
                await connection.OpenAsync();

                var parameters = new
                {
                    start,
                    end
                };

                var sql = @"SELECT              a.cod_postal AS ZipCode,
                                                b.des_cidade AS City,
                                                c.des_sigla AS State,
                                                (t.des_tipo_logradouro + ' ' + d.des_logradouro) AS Street, 
                                                r.des_bairro AS Neighborhood
                            FROM                dbo.tb_cep AS a
                            LEFT OUTER JOIN     dbo.tb_cidade AS b ON a.cod_cidade = b.cod_cidade
                            LEFT OUTER JOIN     dbo.tb_estado AS c ON a.cod_estado = c.cod_estado
                            LEFT OUTER JOIN     dbo.tb_logradouro AS d ON a.cod_logradouro = d.cod_logradouro
                            LEFT OUTER JOIN     dbo.tb_tipo_logradouro AS t ON a.cod_tipo_logradouro = t.cod_tipo_logradouro
                            LEFT OUTER JOIN     dbo.tb_bairro AS r ON a.cod_bairro = r.cod_bairro
                            WHERE               a.cod_postal BETWEEN @start AND @end";

                var data = await connection.QueryAsync<Address>(
                    sql,
                    parameters);

                await connection.CloseAsync();

                return data;
            };
        }

        private SqlConnection CreateConnection()
        {
            return new SqlConnection(_configuration.GetConnectionString("Default"));
        }
    }


#1 Benchmark – Lista de Endereços

Trazer 20 registros de endereços em uma consulta a banco de dados SQL Server usando Dapper.

Request #1
Request #2
Request #3
Média
ASP.NET 4.8
12
34
13
19
ASP.NET CORE 3.0
37
50
80
45
ASP.NET CORE 2.2
40
52
43
45

Vencedor: ASP.NET 4.8 (19 ms)


#2 Benchmark – Um Endereço

Trazer 1 registro (PK) de endereço em uma consulta a banco de dados SQL Server usando Dapper.

Request #1
Request #2
Request #3
Média
ASP.NET 4.8
12
8
23
14
ASP.NET CORE 3.0
38
43
81
54
ASP.NET CORE 2.2
42
30
40
37

Vencedor: ASP.NET 4.8 (14 ms)


#3 Benchmark – 97.996 endereços TXT

Trazer 97.996 registros de endereços em uma consulta a banco de dados SQL Server usando Dapper e transformá-los em TXT.

Foram utilizados recursos de String Builder, Reflection e LINQ.

Request #1
Request #2
Request #3
Média
ASP.NET 4.8
1.197
1.171
1.165
1.177
ASP.NET CORE 3.0
1.096
1.225
1.196
1.172
ASP.NET CORE 2.2
1.073
1.087
1.126
1.095

Vencedor: ASP.NET 4.8 e ASP.NET CORE 3.0 (1.172 ms)


#4 Benchmark – 477.397 endereços TXT

Foi realizado o mesmo teste que o anterior, só que nesse caso trazendo metade das tabelas de endereços.

Request #1
Request #2
Request #3
Média
ASP.NET 4.8
5.716
6.215
6.260
6.063
ASP.NET CORE 3.0
5.646
5.276
5.112
5.344
ASP.NET CORE 2.2
7.828
5.404
5.445
6.225

Vencedor: ASP.NET CORE 3.0 (5.344 ms)

        public async Task<string> GetRangeTxtAsync(
            string start,
            string end)
        {
            var addresses = await _addressRepository.GetAddressRangeAsync(
                start,
                end);

            string txt;

            if (addresses == null || addresses.Count() == 0)
            {
                txt = "addresses NULL or EMPTY";
            }
            else
            {
                var sb = new StringBuilder();

                sb.Append(GetColumns<Address>());
                sb.Append("\r\n");

                foreach (var address in addresses)
                {
                    sb.Append(GetColumns(address));
                    sb.Append("\r\n");
                }

                txt = sb.ToString();
            }

            return txt;
        }

        private string GetColumns<T>(
            T data = default)
        {
            var sb = new StringBuilder();
            var type = typeof(T);
            var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);

            foreach (PropertyInfo property in properties)
            {
                if (sb.Length > 0)
                {
                    sb.Append(";");
                }

                if (data == null)
                {
                    sb.AppendFormat(
                        "{0}_{1}",
                        type.Name,
                        property.Name);
                }
                else
                {
                    var val = property.GetValue(data);

                    sb.Append(val == null ? "" : $" {val.ToString()}");
                }
            }

            sb.Append(";");

            return sb.ToString();
        }


#5 Benchmark – 97.996 endereços JSON

Trazer 97.996 registros de endereços em uma consulta a banco de dados SQL Server usando Dapper e usando serialização JSON nativa da versão do framework.

Request #1
Request #2
Request #3
Média
ASP.NET 4.8
1.069
1.186
1.162
1.139
ASP.NET CORE 3.0
1.327
1.165
1.088
1.790
ASP.NET CORE 2.2
1.120
1.047
1.080
1.082

Vencedor: ASP.NET CORE 2.2 (1.082 ms)


#6 Benchmark – 477.397 endereços JSON

Foi realizado mesmo teste que anterior mas nesse caso trazendo metade da tabela de endereços.

Request #1
Request #2
Request #3
Média
ASP.NET 4.8
5.139
5.578
5.568
5.428
ASP.NET CORE 3.0
4.885
4.837
5.347
5.023
ASP.NET CORE 2.2
5.146
5.006
5.127
5.093

Vencedor: ASP.NET CORE 3.0 (5.023 ms)


#7 Benchmark – Ler arquivo PDF 7MB

Foi colocado um arquivo na pasta App_Data com cerca de 7MB e retornado no response como download.

Request #1
Request #2
Request #3
Média
ASP.NET 4.8
429
422
436
429
ASP.NET CORE 3.0
88
94
121
101
ASP.NET CORE 2.2
108
70
75
84

Vencedor: ASP.NET CORE 2.2 (84 ms)

        // ASP.NET 4.8
        public HttpResponseMessage GetFileSystemDownload()
        {
            var path = System.Web.Hosting.HostingEnvironment.MapPath("~/App_Data/Cartilha_do_Idoso.pdf");

            var response = new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new StreamContent(new FileStream(path, FileMode.Open, FileAccess.Read))
            };

            response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
            {
                FileName = Path.GetFileName(path)
            };

            response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");

            return response;
        }

        // ASP.NET CORE
        public IActionResult GetFileSystemDownload()
        {
            var path = $@"{_env.ContentRootPath}\App_Data\Cartilha_do_Idoso.pdf";

            return new FileStreamResult(new FileStream(path, FileMode.Open, FileAccess.Read), "application/pdf");
        }


#8 Benchmark – Ler arquivos c:\windows

Usando System.IO, trazer a lista de arquivos da pasta Windows.

Request #1
Request #2
Request #3
Média
ASP.NET 4.8
6
11
17
11
ASP.NET CORE 3.0
32
79
33
48
ASP.NET CORE 2.2
44
22
22
29

Vencedor: ASP.NET 4.8 (11 ms)


Considerações

Eu repeti esses testes outras três vezes em dias e horários diferentes e todos os valores ficaram na média, ou seja, os resultados finais foram os mesmos.

E se não fosse o bastante, peguei o projeto e rodei os testes em outra máquina de desenvolvimento e os resultados finais foram os mesmos.

O mais interessante desse meu benchmark é que, para alguns casos o ASP.NET 4.8 é melhor que o APS.NET CORE (3.0/2.2) e vice-versa.

Se você conhece algum outro benchmark comenta aí que atualizo esse artigo e fica a pergunta, para você qual o melhor framework?

Obrigado 🙂

Artigos sobre ASP.NET CORE:

.NET Core para Desenvolvedores .NET
IIS: Como Hospedar Aplicação .NET Core em 10 Passos
Crie um Gerenciador de Arquivos do Zero em .NET Core e VueJS
JWT: Customizando o Identity no ASP.NET CORE 3.0
AppSettings: 6 Formas de Ler o Config no ASP.NET CORE 3.0


Índice Benchmark

ASP.NET Core: Saturating 10GbE at 7+ million request/s
Performance Improvements in .NET Core 3.0
Performance Tests / Benchmarking for ASP.NET Core 2.2 Endpoints
Dicas de performance para APIs REST no ASP.NET Core
Teste de performance de aplicações .NET Core com BenchMarkDotNet
Swift vs .NET Core — Benchmark
Dapper vs EF Core Query Performance Benchmarking
Lightweight .NET Core benchmarking with BenchmarkDotNet and dotnet-script
.NET Serialization Benchmark 2019 Roundup
Benchmarking .NET code
Profiling .NET Code with PerfView and visualizing it with speedscope.app
Benchmarking Your .NET Core Code With BenchmarkDotNet
Performance benchmark: gRPC vs. REST in .NET Core 3 Preview 8
The Battle of C# to JSON Serializers in .NET Core 3
C# .NET Core versus Java fastest programs
gRPC performance benchmark in ASP.NET Core 3

Faça download completo do código fonte no github.
Sobre o Autor:
Trabalha como arquiteto de soluções e desenvolvedor, tem mais de 18 anos de experiência em desenvolvimento de software em diversas plataformas sendo mais de 16 anos somente para o mercado de seguros.
Revisado por:
Apaixonado por tecnologia e sempre disposto a encarar novos desafios, atualmente trabalho focado em aplicações web e mobile com a plataforma .NET, e me aventurando nas diversas linguagens, desafios e experiências que a área nos proporciona.


Apaixonado por tecnologia, atualmente trabalho com aplicações web e estou aprofundando meu conhecimento em mobile. Meu objetivo é contribuir com a comunidade ajudando os desenvolvedores que estão iniciando.