42 votes

Pourquoi Calli est-il plus rapide qu'un appel de délégués ?

Je jouais avec Reflection.Emit et j'ai découvert l'outil peu utilisé qu'est le EmitCalli . Intrigué, je me suis demandé si c'était différent d'un appel de méthode normal, et j'ai donc créé le code ci-dessous :

using System;
using System.Diagnostics;
using System.Reflection.Emit;
using System.Runtime.InteropServices;
using System.Security;

[SuppressUnmanagedCodeSecurity]
static class Program
{
    const long COUNT = 1 << 22;
    static readonly byte[] multiply = IntPtr.Size == sizeof(int) ?
      new byte[] { 0x8B, 0x44, 0x24, 0x04, 0x0F, 0xAF, 0x44, 0x24, 0x08, 0xC3 }
    : new byte[] { 0x0f, 0xaf, 0xca, 0x8b, 0xc1, 0xc3 };

    static void Main()
    {
        var handle = GCHandle.Alloc(multiply, GCHandleType.Pinned);
        try
        {
            //Make the native method executable
            uint old;
            VirtualProtect(handle.AddrOfPinnedObject(),
                (IntPtr)multiply.Length, 0x40, out old);
            var mulDelegate = (BinaryOp)Marshal.GetDelegateForFunctionPointer(
                handle.AddrOfPinnedObject(), typeof(BinaryOp));

            var T = typeof(uint); //To avoid redundant typing

            //Generate the method
            var method = new DynamicMethod("Mul", T,
                new Type[] { T, T }, T.Module);
            var gen = method.GetILGenerator();
            gen.Emit(OpCodes.Ldarg_0);
            gen.Emit(OpCodes.Ldarg_1);
            gen.Emit(OpCodes.Ldc_I8, (long)handle.AddrOfPinnedObject());
            gen.Emit(OpCodes.Conv_I);
            gen.EmitCalli(OpCodes.Calli, CallingConvention.StdCall,
                T, new Type[] { T, T });
            gen.Emit(OpCodes.Ret);

            var mulCalli = (BinaryOp)method.CreateDelegate(typeof(BinaryOp));

            var sw = Stopwatch.StartNew();
            for (int i = 0; i < COUNT; i++) { mulDelegate(2, 3); }
            Console.WriteLine("Delegate: {0:N0}", sw.ElapsedMilliseconds);
            sw.Reset();

            sw.Start();
            for (int i = 0; i < COUNT; i++) { mulCalli(2, 3); }
            Console.WriteLine("Calli:    {0:N0}", sw.ElapsedMilliseconds);
        }
        finally { handle.Free(); }
    }

    delegate uint BinaryOp(uint a, uint b);

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool VirtualProtect(
        IntPtr address, IntPtr size, uint protect, out uint oldProtect);
}

J'ai exécuté le code en mode x86 et en mode x64. Les résultats ?

32 bits :

  • Version délégué : 994
  • Version Calli : 46

64 bits :

  • Version délégué : 326
  • Version Calli : 83

Je suppose que la question est évidente maintenant... pourquoi y a-t-il une telle différence de vitesse ?


Mise à jour :

J'ai également créé une version 64 bits de P/Invoke :

  • Version délégué : 284
  • Version Calli : 77
  • Version P/Invoke : 31

Apparemment, P/Invoke est plus rapide... est-ce un problème avec mon benchmarking, ou y a-t-il quelque chose que je ne comprends pas ? (Je suis en mode release, d'ailleurs).

10voto

Kevin Points 138

Compte tenu de vos performances, je suppose que vous utilisez le cadre 2.0 ou quelque chose de similaire. Les chiffres sont bien meilleurs en 4.0, mais la version "Marshal.GetDelegate" est toujours plus lente.

Le fait est que tous les délégués ne sont pas créés égaux.

Les délégués pour les fonctions de code géré sont essentiellement un appel de fonction direct (sur x86, c'est un __fastcall), avec l'ajout d'un petit "switcheroo" si vous appelez une fonction statique (mais cela ne représente que 3 ou 4 instructions sur x86).

Les délégués créés par "Marshal.GetDelegateForFunctionPointer", d'autre part, sont un appel direct à une fonction "stub", qui fait un peu d'overhead (marshalling et autres) avant d'appeler la fonction non gérée. Dans ce cas, il y a très peu de marshalling, et le marshalling pour cet appel semble être pratiquement optimisé en 4.0 (mais passe probablement encore par l'interpréteur ML en 2.0) - mais même en 4.0, il y a un stackWalk qui demande des permissions de code non géré qui ne fait pas partie de votre délégué calli.

J'ai généralement constaté que, à moins de connaître quelqu'un de l'équipe de développement .NET, votre meilleure chance de comprendre ce qui se passe avec l'interopérabilité gérée/non gérée est de creuser un peu avec WinDbg et SOS.

6voto

daitangio Points 441

Difficile de répondre :) Quoi qu'il en soit, je vais essayer.

L'EmitCalli est plus rapide parce qu'il s'agit d'un appel à un code d'octets brut. Je pense que le SuppressUnmanagedCodeSecurity désactivera également certaines vérifications, par exemple les vérifications d'index de dépassement de pile/de tableau hors limites. Le code n'est donc pas sûr et s'exécute à pleine vitesse.

La version déléguée comportera du code compilé pour vérifier le typage et effectuera également un appel de déréférence (parce que le délégué est comme un pointeur de fonction typée).

Mes deux cents !

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X