Considere a seguinte manipulação simples sobre uma coleção:
static List<int> x = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var result = x.Where(i => i % 2 == 0).Where(i => i > 5);
Agora vamos usar Expressões. O código a seguir é aproximadamente equivalente:
static void UsingLambda() {
Func<IEnumerable<int>, IEnumerable<int>> lambda = l => l.Where(i => i % 2 == 0).Where(i => i > 5);
var t0 = DateTime.Now.Ticks;
for (int j = 1; j < MAX; j++)
var sss = lambda(x).ToList();
var tn = DateTime.Now.Ticks;
Console.WriteLine("Using lambda: {0}", tn - t0);
}
Mas eu quero construir a expressão em tempo real, então aqui está um novo teste:
static void UsingCompiledExpression() {
var f1 = (Expression<Func<IEnumerable<int>, IEnumerable<int>>>)(l => l.Where(i => i % 2 == 0));
var f2 = (Expression<Func<IEnumerable<int>, IEnumerable<int>>>)(l => l.Where(i => i > 5));
var argX = Expression.Parameter(typeof(IEnumerable<int>), "x");
var f3 = Expression.Invoke(f2, Expression.Invoke(f1, argX));
var f = Expression.Lambda<Func<IEnumerable<int>, IEnumerable<int>>>(f3, argX);
var c3 = f.Compile();
var t0 = DateTime.Now.Ticks;
for (int j = 1; j < MAX; j++)
var sss = c3(x).ToList();
var tn = DateTime.Now.Ticks;
Console.WriteLine("Using lambda compiled: {0}", tn - t0);
}
Claro que não é exatamente como o anterior, então, para ser justo, eu modifico ligeiramente o primeiro:
static void UsingLambdaCombined() {
Func<IEnumerable<int>, IEnumerable<int>> f1 = l => l.Where(i => i % 2 == 0);
Func<IEnumerable<int>, IEnumerable<int>> f2 = l => l.Where(i => i > 5);
Func<IEnumerable<int>, IEnumerable<int>> lambdaCombined = l => f2(f1(l));
var t0 = DateTime.Now.Ticks;
for (int j = 1; j < MAX; j++)
var sss = lambdaCombined(x).ToList();
var tn = DateTime.Now.Ticks;
Console.WriteLine("Using lambda combined: {0}", tn - t0);
}
Agora vêm os resultados para MAX = 100000, VS2008, depuração ON:
Using lambda compiled: 23437500
Using lambda: 1250000
Using lambda combined: 1406250
E com depuração DESLIGADA:
Using lambda compiled: 21718750
Using lambda: 937500
Using lambda combined: 1093750
Surpresa . A expressão compilada é cerca de 17x mais lenta do que as outras alternativas. Agora vem a pergunta:
- Estou comparando expressões não equivalentes?
- Existe um mecanismo para fazer o .NET "otimizar" a expressão compilada?
- Como expresso a mesma chamada em cadeia de maneira
l.Where(i => i % 2 == 0).Where(i => i > 5);
programática?
Mais algumas estatísticas. Visual Studio 2010, depuração ATIVADA, otimizações DESATIVADAS:
Using lambda: 1093974
Using lambda compiled: 15315636
Using lambda combined: 781410
Depuração ATIVADA, otimizações ATIVADAS:
Using lambda: 781305
Using lambda compiled: 15469839
Using lambda combined: 468783
Depuração DESATIVADA, otimizações ATIVADAS:
Using lambda: 625020
Using lambda compiled: 14687970
Using lambda combined: 468765
Nova surpresa. Mudar de VS2008 (C # 3) para VS2010 (C # 4) torna o UsingLambdaCombined
mais rápido do que o lambda nativo.
Ok, descobri uma maneira de melhorar o desempenho do lambda compilado em mais de uma ordem de magnitude. Aqui vai uma dica; depois de executar o criador de perfil, 92% do tempo é gasto em:
System.Reflection.Emit.DynamicMethod.CreateDelegate(class System.Type, object)
Hmmmm ... Por que está criando um novo delegado a cada iteração? Não tenho certeza, mas a solução segue em uma postagem separada.
fonte
Stopwatch
para horários em vez deDateTime.Now
.Respostas:
Será que os lambdas internos não estão sendo compilados?!? Aqui está uma prova de conceito:
E agora os horários são:
Uau! Além de ser rápido, é mais rápido que o lambda nativo. ( Cabeça coçada ).
Claro que o código acima é simplesmente muito doloroso de escrever. Vamos fazer uma mágica simples:
E alguns tempos, VS2010, otimizações ativadas, depuração desativada:
Agora você pode argumentar que não estou gerando toda a expressão dinamicamente; apenas as invocações em cadeia. Mas no exemplo acima eu gero toda a expressão. E os horários combinam. Este é apenas um atalho para escrever menos código.
Do meu entendimento, o que está acontecendo é que o método .Compile () não propaga as compilações para lambdas internas e, portanto, a invocação constante de
CreateDelegate
. Mas, para realmente entender isso, adoraria que um guru do .NET comentasse um pouco sobre as coisas internas que estão acontecendo.E por que , oh, por que isso agora é mais rápido do que um lambda nativo !?
fonte
Recentemente, fiz uma pergunta quase idêntica:
Desempenho da expressão compilada para delegar
A solução para mim foi que eu não deveria chamar
Compile
oExpression
, mas deveria chamáCompileToMethod
-lo e compilar oExpression
para umstatic
método em um assembly dinâmico.Igual a:
Porém, não é o ideal. Não tenho certeza de quais tipos isso se aplica exatamente, mas acho que os tipos que são tomados como parâmetros pelo delegado ou retornados pelo delegado precisam ser
public
não genéricos. Tem que ser não genérico porque os tipos genéricos aparentemente acessamSystem.__Canon
que é um tipo interno usado pelo .NET nos bastidores para tipos genéricos e isso viola o "tem que ser umpublic
regra de tipo).Para esses tipos, você pode usar o aparentemente mais lento
Compile
. Eu os detecto da seguinte maneira:Mas, como eu disse, isso não é o ideal e ainda gostaria de saber por que compilar um método para uma montagem dinâmica às vezes é uma ordem de magnitude mais rápida. E digo às vezes porque também vi casos em que um
Expression
compilado comCompile
que um método é tão rápido quanto um método normal. Veja minha pergunta para isso.Ou se alguém souber uma maneira de contornar a
public
restrição " não- tipos" com o assembly dinâmico, isso também é bem-vindo.fonte
Suas expressões não são equivalentes e, portanto, você obtém resultados distorcidos. Eu escrevi uma bancada de teste para testar isso. Os testes incluem a chamada lambda regular, a expressão compilada equivalente, uma expressão compilada equivalente feita à mão, bem como versões compostas. Esses devem ser números mais precisos. Curiosamente, não estou vendo muita variação entre as versões simples e compostas. E as expressões compiladas são mais lentas naturalmente, mas apenas um pouco. Você precisa de uma entrada grande o suficiente e uma contagem de iterações para obter alguns bons números. Faz diferença.
Quanto à sua segunda pergunta, não sei como você conseguiria obter mais desempenho com isso, então não posso ajudá-lo. Parece tão bom quanto vai ficar.
Você encontrará minha resposta para sua terceira pergunta no
HandMadeLambdaExpression()
método. Não é a expressão mais fácil de construir devido aos métodos de extensão, mas é factível.E os resultados na minha máquina:
fonte
O desempenho lambda compilado sobre delegados pode ser mais lento porque o código compilado em tempo de execução pode não ser otimizado, no entanto, o código que você escreveu manualmente e que compilou por meio do compilador C # é otimizado.
Em segundo lugar, várias expressões lambda significam vários métodos anônimos, e chamar cada um deles leva pouco tempo extra para avaliar um método direto. Por exemplo, ligando
e
são diferentes e, com o segundo, um pouco mais de sobrecarga é necessário da perspectiva do compilador, na verdade, são duas chamadas diferentes. Primeiro chamando o próprio x e, em seguida, dentro da declaração de x chamando
Portanto, seu Lambda combinado certamente terá um desempenho pouco lento sobre uma única expressão lambda.
E isso é independente do que está sendo executado internamente, porque você ainda está avaliando a lógica correta, mas está adicionando etapas adicionais para que o compilador execute.
Mesmo após a árvore de expressão ser compilada, ela não terá otimização e ainda preservará sua pequena estrutura complexa, avaliando e chamando pode ter validação extra, verificação nula etc, o que pode diminuir o desempenho de expressões lambda compiladas.
fonte
UsingLambdaCombined
teste está combinando várias funções lambda e seu desempenho está muito próximo deUsingLambda
. Com relação às otimizações, estava convencido de que elas eram manipuladas pelo mecanismo JIT e, portanto, o código gerado em tempo de execução (após a compilação) também seria alvo de quaisquer otimizações JIT.