Performances des expressions compilées à déléguer

c# dynamically-generated expression-trees performance

Question

Je génère un arbre d'expression qui mappe les propriétés d'un objet source vers un objet de destination, qui est ensuite compilé en Func<TSource, TDestination, TDestination> et exécuté.

Voici la vue de débogage de l' LambdaExpression résultante:

.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
    }
}

Nettoyé ce serait:

.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
    }
}

C'est le code qui mappe les propriétés sur ces types:

.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
    }
}

Le code manuel pour faire ceci est:

.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
    }
}

Le problème est que lorsque je compile LambdaExpression et compare le delegate résultant delegate il est environ 10 fois plus lent que la version manuelle. Je n'ai pas d'idées pourquoi c'est comme ça. Et l’idée principale à ce sujet est la performance maximale sans l’ennui de la cartographie manuelle.

Lorsque je prends le code de Bart de Smet dans son article de blog sur ce sujet et que je compare la version manuelle du calcul des nombres premiers à l’arbre d’expression compilé, leurs performances sont parfaitement identiques.

Qu'est-ce qui peut causer cette énorme différence lorsque la vue de débogage de LambdaExpression ressemble à ce que vous attendez?

MODIFIER

Comme demandé, j'ai ajouté le repère que j'ai utilisé:

.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
    }
}

Le second est naturellement plus lent que de le faire manuellement car elle implique une recherche de dictionnaire et quelques objets instanciation, mais le troisième devrait être tout aussi vite qu'il est le délégué brut il qui est en cours invoquaient et la distribution du Delegate à Func se passe en dehors du boucle.

J'ai également essayé d'intégrer le code manuel dans une fonction, mais je me souviens que cela ne faisait pas une différence notable. Dans les deux cas, un appel de fonction ne doit pas ajouter un ordre de grandeur de surcharge.

Je fais également la référence deux fois pour m'assurer que l'EJI n'interfère pas.

MODIFIER

Vous pouvez obtenir le code pour ce projet ici:

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

J'ai utilisé l'extension de débogueur Sons-of-Strike décrite dans ce billet de blog de Bart de Smet pour vider l'IL généré de la méthode dynamique:

.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
    }
}

Je ne suis pas un expert en IL, mais cela semble assez simple et exactement ce à quoi on s'attendrait, non? Alors pourquoi est-ce si lent? Pas d'opérations de boxe étranges, pas d'instanciations cachées, rien. Ce n'est pas exactement la même chose que l'arbre d'expression ci-dessus, car il y a aussi une vérification null à right.Complex . right.Complex maintenant.

Voici le code de la version manuelle (obtenue via Reflector):

.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
    }
}

Semble identique à moi ..

MODIFIER

J'ai suivi le lien dans la réponse de Michael B à ce sujet. J'ai essayé d'implémenter l'astuce dans la réponse acceptée et cela a fonctionné! Si vous voulez un résumé de l'astuce: il crée un assemblage dynamique et compile l'arborescence des expressions en une méthode statique dans cet assemblage, ce qui est 10 fois plus rapide. L'inconvénient est que mes classes de référence étaient internes (en fait, les classes publiques imbriquées dans une classe interne) et une exception s'est produite lorsque j'ai essayé d'y accéder car elles n'étaient pas accessibles. Cela ne semble pas être une solution de contournement, mais je peux simplement détecter si les types référencés sont internes ou non et décider de la méthode de compilation à utiliser.

Ce qui me tracasse encore cependant est pourquoi cette méthode de choix des nombres est identique dans la performance à l'arbre d'expression compilé.

Et encore une fois, je vous invite à utiliser le code dans ce dépôt GitHub pour confirmer mes mesures et vous assurer que je ne suis pas fou :)

Réponse acceptée

C'est assez étrange pour un tel énorme surpris. Il y a quelques choses à prendre en compte. D'abord, le code compilé du VS a différentes propriétés qui pourraient influencer l'optimisation de la gigue.

Incluez-vous la première exécution du délégué compilé dans ces résultats? Vous ne devriez pas, vous devriez ignorer la première exécution de l'un des chemins de code. Vous devez également transformer le code normal en délégué, car l'invocation de délégué est légèrement plus lente que l'invocation d'une méthode d'instance, ce qui est plus lent que l'invocation d'une méthode statique.

En ce qui concerne les autres modifications, il convient de tenir compte du fait que le délégué compilé a un objet de fermeture qui n’est pas utilisé ici mais qui signifie qu’il s’agit d’un délégué ciblé dont l’exécution risque d’être un peu plus lente. Vous remarquerez que le délégué compilé a un objet cible et que tous les arguments sont décalés d’un cran.

De plus, les méthodes générées par lcg sont considérées comme statiques, ce qui a tendance à être plus lent lorsqu’elles sont compilées en délégués par rapport aux méthodes d’instance en raison de la commutation des registres. (Duffy a déclaré que le pointeur "this" a un registre réservé dans CLR et que lorsque vous avez un délégué pour un statique, il doit être déplacé vers un autre registre, ce qui entraîne une légère surcharge). Enfin, le code généré à l'exécution semble fonctionner légèrement plus lentement que le code généré par VS. Le code généré à l'exécution semble avoir un extra sandbox et est lancé depuis un assemblage différent (essayez quelque chose comme l'opcode ldftn ou l'opcode calli si vous ne me croyez pas, les délégués de reflect.Mited compileront mais ne vous laisseront pas les exécuter ) qui invoque une surcharge minimale.

Aussi, vous utilisez le mode release, non? Nous avons examiné ce problème dans un sujet similaire: Pourquoi Func <> est-il créé à partir d'Expression <Func <>> plus lent que Func <> déclaré directement?

Edit: Voir aussi ma réponse ici: DynamicMethod est beaucoup plus lent que la fonction IL compilée

La solution principale consiste à ajouter le code suivant à l'assembly où vous prévoyez de créer et d'appeler du code généré au moment de l'exécution.

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

Et de toujours utiliser un type de délégué intégré ou un type provenant d'un assembly avec ces indicateurs.

La raison en est que le code dynamique anonyme est hébergé dans un assembly toujours marqué comme une confiance partielle. En autorisant des appelants partiellement dignes de confiance, vous pouvez ignorer une partie de la poignée de main. La transparence signifie que votre code ne va pas augmenter le niveau de sécurité (c'est-à-dire un comportement lent). Enfin, le vrai truc consiste à appeler un type de délégué hébergé dans un assembly marqué comme vérification par saut. Func<int,int>#Invoke est totalement fiable. Aucune vérification n'est donc nécessaire. Cela vous donnera des performances du code généré à partir du compilateur VS. En n'utilisant pas ces attributs, vous créez une surcharge dans .NET 4. Vous pourriez penser que SecurityRuleSet.Level1 serait un bon moyen d'éviter cette surcharge, mais le changement de modèle de sécurité est également coûteux.

En bref, ajoutez ces attributs, puis votre test de performance de micro-boucle se déroulera à peu près de la même façon.


Réponse populaire

On dirait que vous vous retrouvez face à une surcharge d'invocation. Cependant, quelle que soit la source, si votre méthode s'exécute plus rapidement lorsqu'elle est chargée à partir d'un assembly compilé, il vous suffit de la compiler dans un assembly et de le charger! Voir ma réponse à la rubrique Pourquoi Func <> est-il créé à partir d'Expression <Func <>> plus lente que Func <> déclarée directement? pour plus de détails sur comment.




Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi
Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi