2013-02-28 11 views
6

Ho alcune espressioni che generano codice da passare come l'istruzione "where" in un database letto, e sto cercando di accelerare un po 'le cose.Modifica del valore di un'espressione costante in un'espressione binaria preesistente

Questo esempio sotto fa una cui dichiarazione per abbinare il PK di un tavolo con un passato di valore:

private Expression MakeWhereForPK(int id) 
{ 
    var paramExp = Expression.Parameter(typeof(Brand),"b"); 

    //Expression to get value from the entity 
    var leftExp = Expression.Property(paramExp,"ID"); 

    //Expression to state the value to match (from the passed in variable) 
    var rightExp = Expression.Constant(id,typeof(int)); 

    //Expression to compare the two 
    var whereExp = Expression.Equal(leftExp,rightExp); 

    return Expression.Lambda<Func<Brand,bool>>(whereExp,paramExp); 
} 
Quanto sopra

è una semplificazione per la domanda - la cosa reale include il codice per prendere il tavolo per interrogare su e trovare il suo PK ecc e 'efficace facendo la stessa cosa come si potrebbe fare nel codice di solito:

ctx.Brands.Where(b => b.ID = id); 

Questo funziona bene, ma, mentre si fa a test per ottimizzare le cose un po' ho trovato la sua piuttosto lento - fare il precedente 1000000 richiede circa 25 secondi. È meglio se ometto l'ultima riga sopra (ma ovviamente è inutile!), Quindi sembra che Expression.Lamba stia prendendo circa i 2/3 del tempo, ma il resto non è eccezionale.

Se tutte le query dovessero accadere contemporaneamente, potrei trasformarle in un'espressione di stile IN e generare una volta, ma sfortunatamente non è possibile, quindi quello che spero è di risparmiare su gran parte della generazione di cui sopra, e riusare semplicemente l'espressione generata, ma passando un diverso valore di id.

Si noti che, poiché questo verrà passato a Linq, non posso compilare l'espressione per avere un parametro intero che posso passare in invocazione - deve rimanere come albero delle espressioni.

Quindi la seguente potrebbe essere una versione semplice ai fini di fare l'esercizio tempistica:

Expression<Func<Brand,bool>> savedExp; 

private Expression MakeWhereForPKWithCache(int id) 
{ 
    if (savedExp == null) 
    { 
     savedExp = MakeWhereForPK(id); 
    } 
    else 
    { 
     var body = (BinaryExpression)savedExp.Body; 
     var rightExp = (ConstantExpression)body.Right; 

     //At this point, value is readonly, so is there some otherway to "inject" id, 
     //and save on compilation? 
     rightExp.Value = id; 
    } 

    return savedExp; 
} 

Come potrei riutilizzare l'espressione, solo con un diverso valore di id?

+0

sarebbe un 'idea di usare un [ 'ParameterExpression'] (http://msdn.microsoft.com/en-us/library/system.linq.expressions.parameterexpression.aspx) invece di un' ConstantExpression'? (A proposito, se le prestazioni sono un problema, perché usare LINQ affatto? LINQ scambia le prestazioni per non dover imparare SQL.) – Andomar

+2

@Andomar: il secondo 'ParameterExpression' renderà' Func '. Non può essere usato come un predicato per 'Where'. – Dennis

risposta

4

Non è possibile modificare gli alberi di espressione: sono immutabili. Ma è possibile sostituire l'espressione costante via a fare il proprio ospite:

class MyVisitor : ExpressionVisitor 
{ 
    private readonly ConstantExpression newIdExpression; 

    public MyVisitor(int newId) 
    { 
     this.newIdExpression = Expression.Constant(newId); 
    } 

    public Expression ReplaceId(Expression sourceExpression) 
    { 
     return Visit(sourceExpression); 
    } 

    protected override Expression VisitConstant(ConstantExpression node) 
    { 
     return newIdExpression; 
    } 
} 

Usage:

  var expr = MakeWhereForPK(0); // p => p.ID == 0 
      var visitor = new MyVisitor(1); 
      var newExpr = visitor.ReplaceId(expr); p => p.ID == 1 

Nota, che questo fa una copia di albero esistente. e non ho provato questo per le prestazioni. Non sono sicuro, sarà più veloce o meno.

Questo codice:

  // warming up 
      var visitor = new MyVisitor(1); 
      var expr = MakeWhereForPK(0); 
      visitor.ReplaceId(MakeWhereForPK(0)); 

      var sw = new System.Diagnostics.Stopwatch(); 
      sw.Start(); 

      for (var i = 0; i < 1000000; i++) 
      { 
       MakeWhereForPK(i); 
      } 

      sw.Stop(); 
      Console.WriteLine("Make expression: {0}", sw.Elapsed); 

      sw.Restart(); 

      for (var i = 0; i < 1000000; i++) 
      { 
       visitor.Visit(expr); 
      } 

      sw.Stop(); 
      Console.WriteLine("Replace constant expression: {0}", sw.Elapsed); 

      Console.WriteLine("Done.");  

produce questi risultati sulla mia macchina:

un'espressione: 00: 00: 04.1714254
Sostituire espressione costante: 00: 00: 02.3644953
Done .

Sembra che il visitatore sia più veloce della creazione di una nuova espressione.

+0

Nice - esaminerà l'adozione di questo - potrebbe fare una differenza ragionevole. Grazie –

+0

BTW, il tuo codice è un po 'sbagliato, dal momento che sostituisce sempre 0 con 1 (e non 'i'). Ma non credo che ciò farà alcuna differenza nelle prestazioni. – svick

6

È possibile utilizzare il fatto che l'albero delle espressioni non deve contenere solo costanti semplici, ma può anche contenere proprietà a cui è possibile accedere.Quindi, quello che dovresti fare è creare una singola espressione che acceda al valore di alcune proprietà, e ogni volta cambi solo quella proprietà, non l'albero delle espressioni.

Qualcosa di simile:

class ExpressionHolder 
{ 
    public int Value { get; set; } 

    public Expression<Func<Brand, bool>> Expr { get; private set; } 

    public ExpressionHolder() 
    { 
     Expr = MakeWhereForPK(); 
    } 

    private Expression<Func<Brand, bool>> MakeWhereForPK() 
    { 
     var paramExp = Expression.Parameter(typeof(Brand), "b"); 

     var leftExp = Expression.Property(paramExp, "ID"); 

     var rightExp = Expression.Property(Expression.Constant(this), "Value"); 

     var whereExp = Expression.Equal(leftExp, rightExp); 

     return Expression.Lambda<Func<Brand, bool>>(whereExp, paramExp); 
    } 
} 

Si tratta di circa 500 × più velocemente di quanto il tuo codice: utilizzando il codice di misurazione di Dennis, ottengo i seguenti risultati:

Make expression: 00:00:02.9869921 
Replace constant expression: 00:00:02.3332857 
Set property: 00:00:00.0056485 
+0

Questa è un'alternativa interessante - richiederà qualche rielaborazione, ma sembra che ne varrà la pena. –

+0

Oltre all'eccellente perf, questo è molto più versatile perché ti permette di cambiare le costanti anche se non hai la possibilità di cambiare l'albero delle espressioni che qualcosa sta usando. Questo è quello che uso, con una classe 'Proxy ' che ha 'espressione pubblica MakeAccessExpression() => Expression.Property (Expression.Constant (this), nameof (Proxy .Value));' – jnm2

Problemi correlati