Esecuzione di espressioni compilate a delegate

c# dynamically-generated expression-trees performance

Domanda

Sto generando un albero di espressioni che mappa le proprietà da un oggetto di origine a un oggetto di destinazione, che viene quindi compilato in Func<TSource, TDestination, TDestination> ed eseguito.

Questa è la vista di debug della LambdaExpression risultante:

.Lambda #Lambda1<System.Func`3[MemberMapper.Benchmarks.Program+ComplexSourceType,MemberMapper.Benchmarks.Program+ComplexDestinationType,MemberMapper.Benchmarks.Program+ComplexDestinationType]>(
    MemberMapper.Benchmarks.Program+ComplexSourceType $right,
    MemberMapper.Benchmarks.Program+ComplexDestinationType $left) {
    .Block(
        MemberMapper.Benchmarks.Program+NestedSourceType $Complex$955332131,
        MemberMapper.Benchmarks.Program+NestedDestinationType $Complex$2105709326) {
        $left.ID = $right.ID;
        $Complex$955332131 = $right.Complex;
        $Complex$2105709326 = .New MemberMapper.Benchmarks.Program+NestedDestinationType();
        $Complex$2105709326.ID = $Complex$955332131.ID;
        $Complex$2105709326.Name = $Complex$955332131.Name;
        $left.Complex = $Complex$2105709326;
        $left
    }
}

Pulito sarebbe:

(left, right) =>
{
    left.ID = right.ID;
    var complexSource = right.Complex;
    var complexDestination = new NestedDestinationType();
    complexDestination.ID = complexSource.ID;
    complexDestination.Name = complexSource.Name;
    left.Complex = complexDestination;
    return left;
}

Questo è il codice che associa le proprietà a questi tipi:

public class NestedSourceType
{
  public int ID { get; set; }
  public string Name { get; set; }
}

public class ComplexSourceType
{
  public int ID { get; set; }
  public NestedSourceType Complex { get; set; }
}

public class NestedDestinationType
{
  public int ID { get; set; }
  public string Name { get; set; }
}

public class ComplexDestinationType
{
  public int ID { get; set; }
  public NestedDestinationType Complex { get; set; }
}

Il codice manuale per fare questo è:

var destination = new ComplexDestinationType
{
  ID = source.ID,
  Complex = new NestedDestinationType
  {
    ID = source.Complex.ID,
    Name = source.Complex.Name
  }
};

Il problema è che quando compilo LambdaExpression e benchmark il delegate risultante è di circa 10 volte più lento della versione manuale. Non ho idea del perché sia ​​così. E l'idea generale di questo è il massimo delle prestazioni senza la noia della mappatura manuale.

Quando prendo codice da Bart de Smet dal suo post sul blog su questo argomento e benchmark la versione manuale del calcolo dei numeri primi rispetto all'albero delle espressioni compilato, sono completamente identici nelle prestazioni.

Cosa può causare questa enorme differenza quando la vista di debug di LambdaExpression è simile a quella che ti aspetteresti?

MODIFICARE

Come richiesto ho aggiunto il benchmark utilizzato:

public static ComplexDestinationType Foo;

static void Benchmark()
{

  var mapper = new DefaultMemberMapper();

  var map = mapper.CreateMap(typeof(ComplexSourceType),
                             typeof(ComplexDestinationType)).FinalizeMap();

  var source = new ComplexSourceType
  {
    ID = 5,
    Complex = new NestedSourceType
    {
      ID = 10,
      Name = "test"
    }
  };

  var sw = Stopwatch.StartNew();

  for (int i = 0; i < 1000000; i++)
  {
    Foo = new ComplexDestinationType
    {
      ID = source.ID + i,
      Complex = new NestedDestinationType
      {
        ID = source.Complex.ID + i,
        Name = source.Complex.Name
      }
    };
  }

  sw.Stop();

  Console.WriteLine(sw.Elapsed);

  sw.Restart();

  for (int i = 0; i < 1000000; i++)
  {
    Foo = mapper.Map<ComplexSourceType, ComplexDestinationType>(source);
  }

  sw.Stop();

  Console.WriteLine(sw.Elapsed);

  var func = (Func<ComplexSourceType, ComplexDestinationType, ComplexDestinationType>)
             map.MappingFunction;

  var destination = new ComplexDestinationType();

  sw.Restart();

  for (int i = 0; i < 1000000; i++)
  {
    Foo = func(source, new ComplexDestinationType());
  }

  sw.Stop();

  Console.WriteLine(sw.Elapsed);
}

Il secondo è comprensibilmente più lento rispetto a farlo manualmente in quanto implica una ricerca nel dizionario e alcune istanze di oggetti, ma il terzo dovrebbe essere altrettanto veloce in quanto è il delegato non elaborato che viene invocato e il cast da Delegate a Func avviene al di fuori del ciclo continuo.

Ho provato a inserire anche il codice manuale in una funzione, ma ricordo che non ha fatto una notevole differenza. In entrambi i casi, una chiamata di funzione non dovrebbe aggiungere un ordine di grandezza del sovraccarico.

Faccio anche il benchmark due volte per assicurarmi che il JIT non interferisca.

MODIFICARE

Puoi ottenere il codice per questo progetto qui:

https://github.com/JulianR/MemberMapper/

Ho usato l'estensione debugger Sons-of-Strike come descritto in quel post sul blog di Bart de Smet per scaricare l'IL generato del metodo dinamico:

IL_0000: ldarg.2 
IL_0001: ldarg.1 
IL_0002: callvirt 6000003 ComplexSourceType.get_ID()
IL_0007: callvirt 6000004 ComplexDestinationType.set_ID(Int32)
IL_000c: ldarg.1 
IL_000d: callvirt 6000005 ComplexSourceType.get_Complex()
IL_0012: brfalse IL_0043
IL_0017: ldarg.1 
IL_0018: callvirt 6000006 ComplexSourceType.get_Complex()
IL_001d: stloc.0 
IL_001e: newobj 6000007 NestedDestinationType..ctor()
IL_0023: stloc.1 
IL_0024: ldloc.1 
IL_0025: ldloc.0 
IL_0026: callvirt 6000008 NestedSourceType.get_ID()
IL_002b: callvirt 6000009 NestedDestinationType.set_ID(Int32)
IL_0030: ldloc.1 
IL_0031: ldloc.0 
IL_0032: callvirt 600000a NestedSourceType.get_Name()
IL_0037: callvirt 600000b NestedDestinationType.set_Name(System.String)
IL_003c: ldarg.2 
IL_003d: ldloc.1 
IL_003e: callvirt 600000c ComplexDestinationType.set_Complex(NestedDestinationType)
IL_0043: ldarg.2 
IL_0044: ret 

Non sono un esperto di IL, ma questo sembra piuttosto semplice e esattamente quello che ti aspetteresti, no? Allora perché è così lento? Niente strane operazioni di boxe, niente istanze nascoste, niente. Non è esattamente lo stesso albero delle espressioni sopra, dato che c'è anche un controllo null su right.Complex ora.

Questo è il codice per la versione manuale (ottenuta tramite Reflector):

L_0000: ldarg.1 
L_0001: ldarg.0 
L_0002: callvirt instance int32 ComplexSourceType::get_ID()
L_0007: callvirt instance void ComplexDestinationType::set_ID(int32)
L_000c: ldarg.0 
L_000d: callvirt instance class NestedSourceType ComplexSourceType::get_Complex()
L_0012: brfalse.s L_0040
L_0014: ldarg.0 
L_0015: callvirt instance class NestedSourceType ComplexSourceType::get_Complex()
L_001a: stloc.0 
L_001b: newobj instance void NestedDestinationType::.ctor()
L_0020: stloc.1 
L_0021: ldloc.1 
L_0022: ldloc.0 
L_0023: callvirt instance int32 NestedSourceType::get_ID()
L_0028: callvirt instance void NestedDestinationType::set_ID(int32)
L_002d: ldloc.1 
L_002e: ldloc.0 
L_002f: callvirt instance string NestedSourceType::get_Name()
L_0034: callvirt instance void NestedDestinationType::set_Name(string)
L_0039: ldarg.1 
L_003a: ldloc.1 
L_003b: callvirt instance void ComplexDestinationType::set_Complex(class NestedDestinationType)
L_0040: ldarg.1 
L_0041: ret 

Sembra identico a me ..

MODIFICARE

Ho seguito il collegamento nella risposta di Michael B su questo argomento. Ho provato a implementare il trucco nella risposta accettata e ha funzionato! Se si desidera un riepilogo del trucco: crea un assembly dinamico e compila l'albero delle espressioni in un metodo statico in quell'assembly e per qualche motivo è 10 volte più veloce. Uno svantaggio di questo è che le mie classi di benchmark erano interne (in realtà, le classi pubbliche annidate in una interna) e lanciavano un'eccezione quando tentavo di accedervi perché non erano accessibili. Non sembra esserci una soluzione alternativa, ma posso semplicemente rilevare se i tipi a cui si fa riferimento sono interni o meno e decidere quale approccio utilizzare per la compilazione.

Ciò che ancora mi infastidisce è il motivo per cui il metodo dei numeri primi è identico nelle prestazioni alla struttura dell'espressione compilata.

E ancora, accolgo con favore chiunque a eseguire il codice nel repository GitHub per confermare le mie misurazioni e assicurarsi che non sia pazzo :)

Risposta accettata

Questo è abbastanza strano per un così enorme sentimento. Ci sono alcune cose da tenere in considerazione. Innanzitutto il codice compilato VS ha diverse proprietà applicate ad esso che potrebbero influenzare il jitter per ottimizzare in modo diverso.

Stai includendo la prima esecuzione per il delegato compilato in questi risultati? Non dovresti, dovresti ignorare la prima esecuzione di entrambi i percorsi del codice. È necessario convertire il codice normale in un delegato poiché la chiamata del delegato è leggermente più lenta del richiamo di un metodo di istanza, che è più lento del richiamo di un metodo statico.

Per quanto riguarda gli altri cambiamenti, è necessario tenere conto del fatto che il delegato compilato ha un oggetto di chiusura che non viene utilizzato qui, ma indica che si tratta di un delegato designato che potrebbe eseguire un po 'più lentamente. Noterai che il delegato compilato ha un oggetto target e tutti gli argomenti vengono spostati di uno.

Anche i metodi generati da lcg sono considerati statici che tendono ad essere più lenti quando compilati per i delegati rispetto ai metodi di istanza a causa del cambio di registro aziendale. (Duffy ha detto che il puntatore "this" ha un registro riservato in CLR e quando si ha un delegato per una statica deve essere spostato su un registro diverso richiamando un leggero overhead). Infine, il codice generato in fase di runtime sembra essere leggermente più lento del codice generato da VS. Il codice generato al runtime sembra avere sandboxing extra e viene lanciato da un assembly diverso (prova a usare qualcosa come ldftn opcode o calli opcode se non mi credi, quei delegati reflection.emited si compileranno ma non ti permetteranno di eseguirli effettivamente ) che richiama un sovraccarico minimo.

Inoltre stai correndo in modalità rilascio giusto? C'è stato un argomento simile in cui abbiamo esaminato questo problema qui: Perché Func <> creato da Expression <Func <>> più lento di Func <> dichiarato direttamente?

Modifica: vedi anche la mia risposta qui: DynamicMethod è molto più lento della funzione IL compilata

Il takeaway principale è che è necessario aggiungere il seguente codice all'assembly in cui si prevede di creare e richiamare il codice generato in fase di esecuzione.

[assembly: AllowPartiallyTrustedCallers]
[assembly: SecurityTransparent]
[assembly: SecurityRules(SecurityRuleSet.Level2,SkipVerificationInFullTrust=true)]

E per utilizzare sempre un tipo di delegato incorporato o uno da un assieme con tali flag.

Il motivo è che il codice dinamico anonimo è ospitato in un assembly che viene sempre contrassegnato come attendibile parziale. Consentendo i chiamanti parzialmente attendibili, è possibile saltare parte dell'handshake. La trasparenza significa che il tuo codice non aumenterà il livello di sicurezza (cioè il comportamento lento), e infine il vero trucco è quello di invocare un tipo di delegato ospitato in un assembly che è contrassegnato come skip verification. Func<int,int>#Invoke è completamente affidabile, quindi non è necessaria alcuna verifica. Questo ti darà prestazioni del codice generato dal compilatore VS. Non usando questi attributi si sta osservando un sovraccarico in .NET 4. Si potrebbe pensare che SecurityRuleSet.Level1 sarebbe un buon modo per evitare questo overhead, ma cambiare i modelli di sicurezza è anche costoso.

In breve, aggiungere quegli attributi e quindi il test delle prestazioni del micro-loop, funzionerà all'incirca nello stesso modo.


Risposta popolare

Sembra che tu stia investendo nel sovraccarico dell'invocazione. Indipendentemente dall'origine, tuttavia, se il metodo viene eseguito più velocemente quando caricato da un assembly compilato, è sufficiente compilarlo in un assembly e caricarlo! Vedere la mia risposta su Perché è Func <> creato da Expression <Func <>> più lento di Func <> dichiarato direttamente? per maggiori dettagli su come.



Autorizzato sotto: CC-BY-SA with attribution
Non affiliato con Stack Overflow
È legale questo KB? Sì, impara il perché
Autorizzato sotto: CC-BY-SA with attribution
Non affiliato con Stack Overflow
È legale questo KB? Sì, impara il perché