Embora eu possa ter emoções mistas em relação ao LINQ to SQL, tivemos muito sucesso com ele no Stack Overflow. É por isso que eu estava surpreso ao ler o seguinte:
Se o senhor estiver criando um aplicativo Web ASP.NET que receberá milhares de acessos por hora, a sobrecarga de execução das consultas Linq consumirá muita CPU e tornará seu site lento. Há um custo de tempo de execução associado a cada consulta Linq que o senhor escreve. As consultas são analisadas e convertidas em uma boa instrução SQL no a cada hit. Isso não é feito no momento da compilação porque não há como descobrir o que o senhor pode estar enviando como parâmetros nas consultas durante o tempo de execução.
Portanto, se o senhor tiver instruções comuns de Linq para Sql como a seguinte…
var query = from widget in dc.Widgets where widget.ID == id && widget.PageID == pageId select widget; var widget = query.SingleOrDefault();… em todo o seu aplicativo Web em crescimento, o senhor logo terá pesadelos de escalabilidade.
J.D. Conley vai além:
Então, examinei um pouco o gráfico de chamadas e descobri que o código que causava, de longe, o maior dano era a criação do objeto de consulta LINQ para cada chamada! A viagem de ida e volta real para o banco de dados não é nada em comparação.
Devo admitir que esses resultados parecem … inacreditáveis. A consulta ao banco de dados é tão lenta (em relação à execução típica do código) que, se o senhor tiver que perguntar quanto tempo levará.., o senhor não tem dinheiro para isso. Tenho muita dificuldade em aceitar a ideia de que o senhor analisar dinamicamente uma consulta Linq levaria mais tempo do que a ida e volta ao banco de dados. Finja que sou do Missouri: mostre-me. Porque eu não estou convencido.
Tudo isso é muito curioso, porque o Stack Overflow usa consultas Linq ingênuas e não compiladas em todas as páginas, e somos um dos 1.000 principais sites da Internet pública, segundo a maioria dos relatos, atualmente. Recebemos uma quantidade considerável de tráfego; da última vez que verifiquei, eram cerca de 1,5 milhão de visualizações de página por dia. Fazemos um grande esforço para garantir que tudo seja o mais rápido possível. Ainda não somos tão rápidos quanto gostaríamos, mas acho que estamos fazendo um trabalho razoável até agora. A jornada ainda está em andamento, e sabemos que o o sucesso da noite para o dia leva anos.
Enfim, O Stack Overflow tem dezenas ou centenas de consultas Linq to SQL simples e não compiladas em todas as páginas. O que nós não tem é “pesadelos de escalabilidade”. O uso da CPU tem sido uma de nossas restrições menos relevantes nos últimos dois anos, à medida que o site cresce. Também ouvimos de outras equipes de desenvolvimento, várias vezes, que o Linq to SQL é “lento”. Mas nunca conseguimos reproduzir isso, mesmo com um profiler.
Um grande mistério.
Agora, é absolutamente verdade que o Linq to SQL tem a peculiaridade de desempenho que ambos os autores estão descrevendo. Sabemos que isso é verdade porque o Rico nos diz isso, e Rico … bem, Rico’s o senhor.
Em resumo, o problema é que a construção básica do Linq (não é preciso usar uma consulta complexa para ilustrar) resulta em avaliações repetidas da consulta se o usuário executar a consulta mais de uma vez.
Cada execução constrói a árvore de expressões e, em seguida, constrói o SQL necessário. Em muitos casos, tudo o que será diferente de uma invocação para outra é um único parâmetro de filtragem de número inteiro. Além disso, qualquer código de vinculação de dados que precisarmos emitir por meio de reflexão leve terá de ser jeditado sempre que a consulta for executada. O armazenamento em cache implícito desses objetos parece problemático porque nunca saberemos qual é a boa política para esse cache – somente o usuário tem o conhecimento necessário.
É um material fascinante; o senhor deveria ler a série completa.
O que é lamentável sobre o Linq nesse cenário é que o senhor está sacrificando intencionalmente algo que qualquer banco de dados SQL antigo e estragado oferece ao senhor gratuitamente. Quando o senhor envia uma consulta SQL parametrizada de tipo comum para um banco de dados SQL tradicional, ela é transformada em hash e, em seguida, comparada com os planos de consulta em cache existentes. O custo computacional do pré-processamento de uma determinada consulta só é pago na primeira vez que o banco de dados vê a nova consulta. Todas as execuções subsequentes dessa mesma consulta usam o plano de consulta em cache e pulam a avaliação da consulta. Não é assim no Linq to SQL. Como disse Rico, cada execução da consulta Linq é totalmente analisada sempre que ocorre.
Agora, há está uma maneira de compilar suas consultas Linq, mas eu pessoalmente acho a sintaxe meio… feia e contorcida. Diga-me o senhor:
Func<Northwinds, IQueryable<Orders>, int> q = CompiledQuery.Compile<Northwinds, int, IQueryable<Orders>> ((Northwinds nw, int orderid) => from o in nw.Orders where o.OrderId == orderid select o ); Northwinds nw = new Northwinds(conn); foreach (Orders o in q(nw, orderid)) { }
De qualquer forma, isso não vem ao caso; podemos confirmar a penalidade de desempenho de não compilarmos nossas consultas por conta própria. Recentemente, escrevemos um trabalho de conversão único em uma tabela simples de três colunas com cerca de 500.000 registros. A essência do trabalho era a seguinte:
db.PostTags.Where(t => t.PostId == this.Id).ToList();
Em seguida, comparamos com a variante SQL; observe que isso também está sendo convertido automaticamente para o prático PostTag
portanto, a única diferença é se a consulta em si é SQL ou não.
db.ExecuteQuery( "select * from PostTags where PostId={0}", this.Id).ToList();
Em um Intel Core 2 Quad rodando a 2,83 GHz, o primeiro levou 422 segundos enquanto o último levou 275 segundos.
A penalidade por não conseguir compilar essa consulta, em 500 mil iterações, foi de 147 segundos. Uau! Isso é 1,5 vez mais lento! Cara, só um programador de BASIC seria burro o suficiente para não compilar todas as suas consultas Linq. Mas espere um segundo, não, espere 147 segundos. Vamos fazer as contas, embora eu seja péssimo nisso. Cada execução não compilada da consulta levou menos de um terço de um milissegundo mais tempo.
No início, eu estava preocupado com o fato de cada página do Stack Overflow ser 1,5 vez mais lenta do que deveria. Mas então percebi que provavelmente é mais realista garantir que qualquer página que geramos não esteja fazendo 500 mil consultas! Será que nos encontramos na a triste tragédia do teatro da micro-otimização … novamente? Acho que sim. Agora estou apenas deprimido.
Embora seja indiscutivelmente correto dizer que toda consulta Linq compilada (ou, nesse caso, qualquer coisa compilada) será mais rápida, suas decisões devem ser um pouco mais sutis do que compilado ou fracassado. A quantidade de benefícios que o senhor obtém com a compilação depende do número de vezes que a faz. Rico seria o primeiro a apontar isso e, de fato, o ele já o fez:
Testing 1 batches of 5000 selectsuncompiled 543.48 selects/sec compiled 925.75 selects/sec
Testing 5000 batches of 1 selects
uncompiled 546.03 selects/sec compiled 461.89 selects/sec
Já mencionei que Rico é o senhor? O senhor está vendo a inversão aqui? Ou o senhor está fazendo um lote de 5.000 consultas ou 5.000 lotes de 1 consulta. Um deles é muito mais rápido quando compilado; o outro é, na verdade, um grande e honroso resultado negativo se o senhor considerar o tempo que o desenvolvedor gasta convertendo todas essas consultas Linq maravilhosamente simples para a sintaxe contorcida necessária para a compilação. Sem mencionar a manutenção implícita do código.
Sou um grande fã de linguagens compiladas. Até mesmo o Facebook dirá ao senhor que O PHP é metade da velocidade que deveria ser em um dia bom com vento de popa. Mas a compilação por si só não é a história completa do desempenho. Nem de perto. Se o senhor estiver compilando algo, seja PHP, uma expressão regular ou uma consulta Linq, não espere que o uma bala de prataou o senhor pode acabar se decepcionando.