sexta-feira, 12 de novembro de 2010

PLINQ é sempre a melhor alternativa?

Essa semana resolvi fazer um comparativo entre LINQ e PLINQ, porém ao final da execução do projeto percebi que o PLINQ sempre levou cerca de 1 segundo a mais de processamento. Bem ao parar para analisar direito e pesquisar no MSDN 1  percebi onde estava meu erro.
Existem alguns pontos que devem ser observados quanto ao PLINQ:
1.       Ele sempre melhora a desempenho? Não!
2.       PLINQ sempre processa em paralelo? Não!
O PLINQ realmente tende a ser mais performático que o LINQ, porém deve-se observar que nem sempre será. Mas então, onde eu errei no meu teste?
O meu erro foi executar um teste com uma instrução de LINQ normal que não exigia muitos cálculos nem sequer permitia a sua execução em paralelo. O PLINQ nem sempre irá melhorar a desempenho do código, para tal é necessário estudar sua viabilidade, um exemplo prático de onde isso se aplica é na utilização de Entity FrameWork e LINQ to SQL, porque tentar utilizar processamento paralelo no programa se o real processamento está no banco? O SGBD quem deve ter paralelismo neste caso.
O PLINQ é extremamente útil na utilização em objetos, ai sim temos um poderoso aliado para ganho de desempenho na execução das aplicações em computadores multiprocessados. Ainda assim, para que o PLINQ execute em paralelo, será necessário que obedeça algumas regras básicas. A documentação1 do PLINQ descreve alguns itens que são levados em consideração como Queries com Where indexado, ou Skip, Take, concat, além de outros.
Para demonstrar de maneira rápida a utilização do PLINQ iremos implementar um PLINQ em uma classe LINQ to Sql e em um DataSet. Ao consumir a DataContext (Linq to SQL) veremos que o processamento tanto em LINQ quanto em PLINQ será muito proximo. No segundo, como o DataSet é um objeto na memória veremos certa diferença.
O banco de exemplo foi criado para este artigo e sua tabela terá 3 campos: um integer, um string e um DateTime. Como nosso objetivo é teste de performance, é bom que a base tenha uma quantidade considerável de registros, para tal, nosso script realiza o insert de 1.000.000 registros na base conforme listagem abaixo.

--Criação do banco
Create database TestePLINQ
GO
--Uso do DataBase
use TestePLINQ
GO

--Tabela de Conteúdo
create table tabelaTeste (
    id int not null
    ,dataCriacao datetime null
    ,texto varchar(200) null
)
GO

--Procedure para popular a tabela
Create procedure FcInclui
AS
BEGIN
    declare @incremental as integer
   
    select @incremental = (select IsNull(MAX(id),1) from tabelaTeste)
   
    while @incremental < 1000000
    BEGIN
         insert into TestePLINQ.dbo.tabelaTeste(id,dataCriacao,texto) values (@incremental,GETDATE(),NEWID())
         print @incremental
         set @incremental = @incremental + 1
    END;
END;
GO
--execução da procedure
exec FcInclui

Nosso primeiro teste será com o LinqToSql2 para comparação entre LINQ e PLINQ, o que nesse caso o PLINQ não deve trazer ganho de desempenho. Utilizaremos 2 cenários neste teste, 1) realizando consulta PLINQ simples com um count, o que nos permite paralelismo; 2) utilizando Take que, segundo a documentação oficial no MSDN por padrão leva o PLINQ a executar em modo linear.
O projeto utilizado será simples, um Windows Form Application com um botão “Iniciar teste” e 3 Labels, 1 com a hora de inicio da execução, 1 com a hora final e o ultimo com o tempo total de execução em segundos.
Ao utilizar o PLINQ com o count obtivemos cerca de 775 milissegundos de execução (Fig 1), enquanto que, com o LINQ tivemos aproximadamente 363 milissegundos (Fig 2). A escolha do count neste caso se dá pelo fato de ele exigir o processamento dos dados diretamente na query, além de permitir que o PLINQ processe em paralelo.
    //PLINQ com Count
            var dadosLinqToSql = (from t in testePlinq.tabelaTestes.AsParallel()
                                  select t).Count();

            //LINQ com Count
            var dadosLinqToSql = (from t in testePlinq.tabelaTestes
                                  select t).Count();




Figura 1 Tempo de execução com PLINQ



Figura 2 Tempo de execução com LINQ


Já ao utilizar o PLINQ com o Take, temos um tempo de processamento mais próximo entre LINQ e PLINQ, uma vez que o PLINQ tende a processar a requisição de forma linear. Ao se observar as figuras 3 e 4 podemos ver a diferença menor de processamento.

                     //PLINQ com Take
            var dadosLinqToSql = (from t in testePlinq.tabelaTestes.AsParallel()
                                  select t).Take(50000);

            //LINQ com Take
            var dadosLinqToSql = (from t in testePlinq.tabelaTestes
                                  select t).Take(50000);

Figura 3 PLINQ com Take


Figura 4 LINQ com Take


Agora nosso teste será com um DataSet, por se tratar de um objeto na memória o PLINQ tende a nos oferecer muito mais performance. A idéia é simples, fazer uma classe que execute o select na base e retorne um DataSet para ser consumido por LINQ, para isso foi criada a classe abaixo.

public class TabelaTeste
    {
        public static DataSet GetTabelaTeste() {
            using (SqlConnection sqlConn = new SqlConnection(@"Data Source=MESSASMACHINE\MESSASSQL;Initial Catalog=TestePLINQ;Integrated Security=True"))
            {
                //declaração DataSet Retorno
                DataSet dataSetTeste = new DataSet();
                //declaração DataAdapter
                SqlDataAdapter sqlAdapter = new SqlDataAdapter();

                //Objeto SqlCommand
                SqlCommand sqlCommand = new SqlCommand();
                //Adiciona o objeto de conexão ao sqlCommand
                sqlCommand.Connection = sqlConn;
                //Adiciona o a query ao commandText
                sqlCommand.CommandText = @"select id,texto,dataCriacao
                                           from tabelaTeste";

                //adiciona a query ao Adapter
                sqlAdapter.SelectCommand = sqlCommand;
                //Preenche o DataSet
                sqlAdapter.Fill(dataSetTeste);

                return dataSetTeste;
            }
        }
    }


Ao analisarmos o resultado da execução com o DataSet veremos um ganho de desempenho com o PLINQ, além de vermos o balanceamento de carga entre os núcleos, enquanto o LINQ sobrecarrega somente um núcleo, veja imagens 5 e 6.


Figura 5 PLINQ com DataSet



Figura 6 Linq com DataSet

Novamente, ao utilizarmos o PLINQ com o Take, vemos que as diferenças entre sua execução e o LINQ diminuem, por sua opção de processamento linear, conforme figuras 7 e 8.










Figura 7 PLINQ com Take no dataSet




Figura 8 LINQ com Take no DataSet





Como até mesmo a própria Microsoft afirma, é necessário avaliar a situação em que pretende empregar o PLINQ, nem sempre ele será satisfatório, se antes era coisa de DBA estudar performance de query, agora cabe ao desenvolvedor também pensar no assunto, vale a pena acessar o MSDN e revisar sobre o PLINQ, como qualquer recurso feito para agregar qualidade ao software, se mal empregado poderá não servir de nada, ou até piorar a situação.

                   2 trabalhando com LINQ to SQL:  http://msdn.microsoft.com/en-us/library/bb384396.aspx

Um comentário: