quarta-feira, 3 de dezembro de 2008

Parte 3: Concatenando Strings!

Mais um pequeno teste para avaliarmos as melhoras.
A concatenação de strings pode ser feita de diversas maneiras. O problema é: Qual é a melhor maneira?

Existem mil maneiras de preparar Néston, invente uma!

Fizemos um teste com 6 maneiras diferentes de concatenar uma string. São essas:

1 - Executando um Append por texto Ex:
dim MyStr as new StringBuilder
MyStr.Append("Texto1")
MyStr.Append("Texto2")

2 - Utilizando o Append.Format
dim MyStr as new StringBuilder
MyStr.AppendFormat("{0}{1}","Texto1","Texto2"

3 - Dando um apend e concatenando os textos com &
dim MyStr as New StringBuilder
MyStr.Append("Texto1" & "Texto2")

4 - Concatenando diretamente utilizando o operador +
dim MyStr as String = "Texto1" + "Texto2"

5 - Concatenando diretamente utilizando o operador &
dim MyStr as String = "Texto1" & "Texto2"

6 - E por fim, utilizando diretemante o String.Format para concatenar
Dim MyStr as string = String.Format("{0}{1}", "Texto1", "Texto2")


A operação funciona da seguinte forma:

TempoInicial = Now() ' Definimos o tempo inicial For I as Integer = 0 To 10000 ' Rodamos 10000 vezes o mesmo processo pra medir o tempo MyStr = MyStr CONCATENANDO "Texto1" "Texto2" 'utilizamos aqui, cada vez um processo diferente dos 6 citados acima Next

Ou seja, na segunda iteração, vamos ter "Texto1Texto2" Concatenando com "Texto1" e "Texto2"
Na terceira iteração teremos "Texto1Texto2Texto1Texto2" Concatenando com "Texto1" e "Texto2" e assim consecutivamente.
Isso pra aumentar cada vez mais o tamanho da string que recebe o resultado do concatenamento.

Rodamos o mesmo teste 10 vezes de cada maneira, para pegarmos uma média de tempo que demorava a executar.

Resultado: O resultado era próximo do que eu esperava. o processo 4, 5 e 6 estão fora de cogitação! Eles sempre apresentaram mais de 1 segundo para rodar as 10 mil iterações da concatenação!
O que me assustou, foi o resultado positivo!
Eu realmente acreditava que o Append.Format (nr. 2) era a melhor, porém fui surpreendido!
Ela foi a pior das melhores! Ganhou apenas das outras três que já descartamos.
A maneira 3 apresentou alguns dos resultados idênticos aos da maneira 1! Porém na média final, perdeu!
A maneira 1, utilizando um append por linha, mesmo sendo a que menos me atraia, é a que tem melhor performance!
Os resultados numéricos podem ser vistos na imagem!

O fonte está disponível no meu SkyDrive. Clique aqui para fazer download!

terça-feira, 2 de dezembro de 2008

Parte 2: CType, Convert.To, Directcast e TryCast

No teste de performance e melhores práticas de hoje vamos mostrar a conversão de variáveis de tipo nativo (inteiro, decimal, data, double, etc...) e variáveis de referência (objetos, classes,etc...)

CType x Convert:

A Classe Convert é uma classe utiliátria do .Net Framework, que internamente executa o Ctype ou Directcast após realizar algumas verificações.

Para executar o teste, criamos uma aplicação que possui um Datagridview que mostra o tempo que demorou para executar 1 milhão de vezes o mesmo processo.
Convertemos, em todos os casos, o valor Nothing para o tipo.

Na figura 1 abaixo mostramos um resultado da conversão. A figura esta separada por:

  • Coluna: mostra o tipo para qual o NOTHING foi convertido
  • Linha: Mostra a função utilizada para converter: Convert.ToType ou Ctype


figura 1

Resultado: Podemos notar, que em quase todos os casos o CType é 2x mais rápido que o Convert.To"Type".
O tempo em segundos é mínimo, portanto se você utiliza poucas conversões de cada vez não chega a fazer diferença. Rodamos 1 milhão de vezes cada alteração para conseguir este tempo. Se for ver cada iteração, é instantâneo. Só é possível medir a diferença em grandes escalas mesmo.

Opinião pessoal: Hoje eu já utilizo o Ctype nas conversões e estou mais acostumado. Acredito ficar mais fácil identificar até porquê o próprio Visual Studio marca os CType's com azul, enquanto o Convert.To"Type" fica na mesma cor (na configuração padrão de cores e estilos). Desse jeito, considero melhor visualmente falando e em questões de performance também.


Directcast x TryCast:

O Directcast converte objetos/classes assim como o TryCast. A diferença entre eles, é que se o Directcast não conseguir converter para a classe específicada, ele retorna uma exception, enquanto o TryCast retorna nothing.
No exemplo, criamos um formulário que herda da classe FORM. Disparamos 3000 vezes o DirectCast(MeuFormulario,Form) e o TryCast(MeuFormulario,Form)
O Cast sempre é executado com sucesso (nossa classe MeuFormulario herda da classe FORM), e por isso nenhum exceção foi disparada e o cast nunca retornou nothing.

Resultado: O tempo de cast é exatamente o mesmo. Fizemos 3 testes e os 3 retornaram igual. Isso porquê o teste sempre foi executado com sucesso. Caso o cast fosse impossível ou desse erro, o Directcast dispararia uma Exception e o TryCast retornaria nothing. Em que ponto chegamos disso? Que depende da aplicação do cast. Se você precisa essêncialmente que o seu Cast seja valido, você pode utilizar o directcast dentro de um bloco Try... Catch e tratar a exceção, ou então utilizar o TryCast e verificar se o resultado foi nothing, se for validar da forma certa.

Opinião Pessoal: Como os tempos foram exatamente iguais, é indiferente você utilizar um ou outro, a não ser que sua aplicação necessite de um determinado tratamento. Dificilmente eu preciso que execute um Exception, por isso prefiro tratar com TryCast's, mas isso não passa de opinião pessoal. Façam bom proveito pessoal.

PS; Agradecimentos: Gostaria de agradecer a galera que da um apoio ao blog, principalmente ao Caio Proiete do blog http://www.caioproiete.com/blogs/pontonet
Ele me ensinou bastante coisa sobre performance e melhorias de código, e dessa maneira eu repasso a quem interessa. Da mesma maneira, quem quizer deixar sua ajuda, fique a vontade! Utilize os comentários para nos ajudar

Obrigado a todos!

segunda-feira, 1 de dezembro de 2008

.Net: Testes de Performance e Melhores Práticas - Parte 1

Iniciando nosso bloco "Testes de Performance e Melhores Práticas" vamos apresentar a utilização do DataGridView e do SqlDataReader em alguns pontos.
Pra quem já conhece bem os procedimentos, fique ligado nos tópicos e vá direto ao assunto sem perder seu tempo. Quem ainda esta começando, montamos um cenário, caso queira reproduzir.

1º Teste do dia:
Vamos executar alguns testes de inserção de linhas em um DataGrid e verificar sua perforamance.

No exemplo utilizaremos:
Um DataGridView que iremos chamar de Grid.
O Grid possui 3 Colunas dentro dele. Seus nomes são: Coluna01, Coluna02 e Coluna03.
Estão colunas possuem indíces, que representam sua atual posição no Grid. São respectivamente 0,1 e 2.
O IndiceDaLinha representa o número da linha atual, que estamos inserindo no grid.

Primeiro, vamos fazer o Grid inserir 20.000 (vinte mil) linhas nas suas 3 colunas utilizando indíce numérico para definir a coluna que receberá o valor. Desta forma, o comando fica +- assim:
Grid.Rows(IndiceDaLinha).Cells(0).Value = "ValorNaColuna1"
Notem que o índice em negrito está representando a coluna de nome Coluna01, e estou definindo para esta linha e coluna do Grid, o valor texto "ValorNaColuna1", apenas para teste. Vamos inserir um valor parecido para as demais colunas.
Grid.Rows(IndiceDaLinha).Cells(1).Value = "ValorNaColuna2"
Grid.Rows(IndiceDaLinha).Cells(2).Value = "ValorNaColuna3"

No segundo teste, utilizamos o mesmo Grid, a mesma quantidade de linhas e colunas com seus respectivos nomes, mas desta vez ao invés de utilizarmos o índice da coluna (0,1,2) vamos utilizar o nome da coluna. O comando fica +- assim:
Grid.Rows(IndiceDaLinha).Cells("Coluna01").Value = "ValorNaColuna1"
Notem que agora passamos o nome da coluna entre "" para
Grid.Rows(IndiceDaLinha).Cells("Coluna02").Value = "ValorNaColuna2"
Grid.Rows(IndiceDaLinha).Cells("Coluna03").Value = "ValorNaColuna3"

Resultado: A resposta de tudo isso, foi uma melhor performance em 0,97 segundos para inserir todas as 20.000 (vinte mil) linhas, se usarmos por índices numéricos (exemplo 01). Algo parecido com 0,0000485 segundos por linha.

Opinião pessoal: para uma aplicação comun, do dia a dia, utilizar um código elegante ajuda os desenvolvedores e não piora tanto a situação. Quem convive diariamente com relatórios de 20.000 linhas? Este 1 segundo é tão importante para gerar o relatório? Por isso, eu fico com a utilização do índice pelo nome da coluna. Mas isso é só opinião minha, nada demais! ;D

Foi sem querer, mas valeu: Até deixar os dois ambientes iguais, eu verifiquei um terceiro teste de performance!
Ao solicitar a ultima linha do datagrid, eu selecionei assim "me.Grid.Rows.GetLastRow(Displayed)"
No outro grid, sem querer eu coloquei assim "me.Grid.Rows.GetLastRow(None)"
A diferença? Com NONE, eu consegui mais de 5 SEGUNDOS mais rapido do que Displayed! O motivo exato eu ainda não sei, mas os dois métodos trouxeram as linhas de forma correta e consegui manipular também. Ou seja, 5 segundos mais rapido e nenhum problema "aparente" (não fui a fundo para descobrir, não era esse o mérito) Mais tarde faço outros testes e dou uma pesquisada, dai sim posto para vocês o motivo disso.

2º Teste do dia:
No segundo teste, vamos executar uma consulta de banco de dados em um SqlDataReader. Após isso, vamos verificar quais pontos podem fazer efeito na melhoria de performance da aplicação.

Assim como DataGridView, podemos buscar dados de um DataReader pelo índice da coluna no DataReader ou pelo nome da Coluna.
O índice da coluna no reader, é equivalente a posição desta coluna no banco no comando Select.
O nome da coluna no reader, é o mesmo nome da coluna usado no banco de dados ou o apelido da coluna, caso ela tenha algum.

No exemplo utilizaremos:
Um SqlDataReader que chamaremos de MeuReader
Para gerarmos um Reader, precisamos de um SqlCommand que guarda o comando texto e dispara o método executereader. O retorno deste método do SqlCommand, é o conteudo do MeuReader.
Ex:
Dim sqlConn as new SqlConnection(SuaStringdeConexãocomoBanco)
Dim sqlComando as new SqlCommand("Select Coluna1 as Col1, Coluna2 as Col2, Coluna3 as Col3 From Colunas", sqlConn)
Dim MeuReader as SqlDataReader = sqlComando.ExecuteReader()
Pronto, populamos o nosso DataReader com o resultado de uma consulta Sql.
Esta consulta Sql é um teste e vai me retornar 10000 registros.
Note que eu utilizei apelidos nas colunas, desta maneira não vou me referir a Coluna1 pelo nome, e sim por "Col1" etc...

No primeiro teste, vamos utilizar os índices das colunas. Como citei antes, o índice é a posição da coluna no comando Sql.

While MeuReader.Read()
Grid.Rows(IndiceLinha).Cells(IndiceColuna).Value = MeuReader.Item(0).ToString
Grid.Rows(IndiceLinha).Cells(IndiceColuna).Value = MeuReader.Item(1).ToString
Grid.Rows(IndiceLinha).Cells(IndiceColuna).Value = MeuReader.Item(2).ToString
End While
o .ToString após o item do Reader é porquê o resultado seria um objeto, e no meu caso, este objeto é String. Este objeto pode ser de qualquer valor (string, integer,etc...) cabe a você definir.

No segundo teste, vamos utilizar o nome das colunas no banco. Vamos utilizar o mesmo comando, o mesmo procedimento, só mudando na hora de pegar o valor do banco, alteramos o índice numérico para nome da coluna.

While MeuReader.Read()
Grid.Rows(IndiceLinha).Cells(IndiceColuna).Value = MeuReader.Item("col1").ToString
Grid.Rows(IndiceLinha).Cells(IndiceColuna).Value = MeuReader.Item("col2").ToString
Grid.Rows(IndiceLinha).Cells(IndiceColuna).Value = MeuReader.Item("col3").ToString
End While
Note que utilizei o alias (apelido) da coluna e não o nome dela. Se na criação do comando Select eu não tivesse nenhum apelido pras colunas, eu usaria no lugar do col1, coluna1 por exemplo.

Resultado: A resposta de tudo isso, foi 0,11 segundos mais rápido se utilizarmos o exemplo novamente, o índice das colunas por número.

Opinião pessoal: Mais uma vez, a pergunta é: Até onde vale a performance? Lógico que buscamos excelência sempre, porém 10.000 linhas, não é toda aplicação que necessita de 10.000 linhas sempre que vai sentir-se prejudicada por 0,11 segundos. Veja bem, não estamos falando nem de 1 segundo! é 1 décimo de segundo. Muito pouco! Para os desenvolvedores, é muito mais visível durante o código se utilizarmos o nome da coluna. Dessa forma sabemos sem precisar procurar o comando disparado, o valor que a coluna deve retornar e o tipo do valor. Fácil e prático.

Melhor ainda: Para dar um "boost" na performance, existe no SqlCommand um ExecuteReader() que usa por parâmetro um System.Data.CommandBehavior. Para deixar realmente mais rápido, podemos utilizar o parâmetro CommandBehavior.SequentialAccess desta forma SqlCommand.ExecuteReader(CommandBehavior.SequentialAccess).
Ele torna o acesso as colunas, sequêncial e único. Ou seja, você é obrigado a usar as colunas na ordem que foi criado o select. No exemplo, você só pode usar as colunas na ordem Coluna1, Coluna2 e Coluna3, e somente uma vez por coluna. Caso você precisasse utilizar a Coluna2 antes da Coluna1, ou precisasse utilizar mais de uma vez a mesma coluna, um exception ia ser disparado. Este comando torna mais rápido porém é restrito. De qualquer forma, caso você precise alterar as ordens ou utilizar mais de uma vez, basta jogar em variáveis na ordem, e depois utilizar estas variáveis. Dai fica tudo rápido e certo! Com isso, tivemos um ganho de mais de 0,6 segundos. O que em 10 consultas gera 6 segundos e assim por diante.

Bom, é isso pessoal. O post saiu maior do que eu esperava mas acredito que deu pra ajudar um pouco. Em breve, iremos executar mais testes e postar aqui. Caso tenha vontade de conhecer alguma coisa mais a fundo comente. Vamos verificar, pode ter certeza.

abraços e até a próxima

Próximos Posts: Testes de performance e praticidade

Em breve, estarei executando alguns testes de performance e praticidade de código que podem ajudar a gente no dia a dia. Tipo um "best pratices", principalmente para VB.NET.

Dessa forma, vamos ver algumas coisas como:
DataReader: utilizar reader.item(index) é melhor ou pior que utilizar reader.item(columnname)?
Tipos de dados: Ctype, Directcast, TryCast, ou Convert.To"Type"

entre outras coisas.

Quem quizer ter um teste executado, post aqui em comentário que faremos
até a proxima