come sono i metodi virtuali più lenti in C #

5

Stavo leggendo che le chiamate virtuali rendono il codice più lento rispetto a chiamare non-virtual in C #. Tuttavia l'istruzione IL per entrambi è la stessa callvirt tranne nei casi in cui viene chiamato base.somemethod() .

Quindi, in che modo il metodo virtuale danneggia le prestazioni?

    
posta jtkSource 02.04.2014 - 01:59
fonte

4 risposte

10

Come sottolineato qui , la chiamata di callvirt è più lenta , ma con un margine molto piccolo. Non sono sicuro di come stai ricevendo il codice CIL, ma come Eric Gunnerson sottolinea , .net non usa mai call per le classi di istanze. sempre utilizza callvirt , e afferma anche in un post di follow-up che la differenza di impatto sulle prestazioni è minima.

Solo un test molto veloce, in cui tutte le classi hanno un metodo void PrintTest() (che stampa solo un messaggio sulla console) ...

  1. BaseVirtual è una classe base con il metodo definito come virtual .
  2. DerivedVirtual e DerivedVirtual2 utilizzano override per ridefinire il metodo virtuale, ereditando da BaseVirtual .
  3. Base è una classe regolare con una classe di istanza regolare (senza virtual o sealed ).
  4. Seal è una classe sealed , solo per i calci.
  5. Stat è una classe con il metodo definito come static .

Ecco il codice principale:

using System;

namespace CSharp
{
    class Program
    {
        static void Main(string[] args)
        {
            BaseVirtual baseVirtual = new BaseVirtual();
            DerivedVirtual derivedVirtual = new DerivedVirtual();
            DerivedVirtual2 derivedVirtual2 = new DerivedVirtual2();
            Base regularBase = new Base();
            Seal seal = new Seal();

            baseVirtual.PrintTest();
            derivedVirtual.PrintTest();
            derivedVirtual2.PrintTest();
            regularBase.PrintTest();
            seal.PrintTest();
            Stat.PrintTest();
        }
    }
}

E, ecco il codice CIL:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       68 (0x44)
  .maxstack  1
  .locals init ([0] class CSharp.BaseVirtual baseVirtual,
           [1] class CSharp.DerivedVirtual derivedVirtual,
           [2] class CSharp.DerivedVirtual2 derivedVirtual2,
           [3] class CSharp.Base regularBase,
           [4] class CSharp.Seal seal)
  IL_0000:  newobj     instance void CSharp.BaseVirtual::.ctor()
  IL_0005:  stloc.0
  IL_0006:  newobj     instance void CSharp.DerivedVirtual::.ctor()
  IL_000b:  stloc.1
  IL_000c:  newobj     instance void CSharp.DerivedVirtual2::.ctor()
  IL_0011:  stloc.2
  IL_0012:  newobj     instance void CSharp.Base::.ctor()
  IL_0017:  stloc.3
  IL_0018:  newobj     instance void CSharp.Seal::.ctor()
  IL_001d:  stloc.s    seal
  IL_001f:  ldloc.0
  IL_0020:  callvirt   instance void CSharp.BaseVirtual::PrintTest()
  IL_0025:  ldloc.1
  IL_0026:  callvirt   instance void CSharp.BaseVirtual::PrintTest()
  IL_002b:  ldloc.2
  IL_002c:  callvirt   instance void CSharp.BaseVirtual::PrintTest()
  IL_0031:  ldloc.3
  IL_0032:  callvirt   instance void CSharp.Base::PrintTest()
  IL_0037:  ldloc.s    seal
  IL_0039:  callvirt   instance void CSharp.Seal::PrintTest()
  IL_003e:  call       void CSharp.Stat::PrintTest()
  IL_0043:  ret
} // end of method Program::Main

Niente di eccessivo, ma questo mostra che solo il metodo static ha utilizzato call . Anche così, non penso che questo farà davvero la differenza per la maggior parte delle applicazioni, quindi non dovremmo preoccuparci di questo.

Quindi, in conclusione, come afferma il post SO, è possibile ottenere un risultato in termini di prestazioni a causa della ricerca che il runtime deve chiamare i metodi virtuali. Rispetto ai metodi statici, ottieni il sovraccarico di callvirt .

... Beh, è stato divertente ...

    
risposta data 02.04.2014 - 02:58
fonte
10

Stai facendo un solo errore: pensando che il codice IL abbia effetto sulle prestazioni. Il fatto è che il compilatore di codice macchina può ancora fare tonnellate di ottimizzazioni basate sul codice IL stesso. Ed è sicuramente in grado di differenziare tra chiamata di metodo virtuale e non virtuale, anche se usano lo stesso opcode. Ciò comporta quindi un codice macchina diverso e prestazioni diverse.

Ecco un semplice benchmark delle prestazioni che ho fatto:

class ClassA
{
    public int Func1(int a)
    {
        return a + 1;
    }

    public virtual int Func2(int a)
    {
        return a + 2;
    }
}

class ClassB : ClassA
{
    public override int Func2(int a)
    {
        return a + 3;
    }
}

class Program
{
    public static void Main()
    {
        int x = 0;
        ClassA a = new ClassA();
        ClassB b = new ClassB();
        ClassA c = new ClassB();

        Stopwatch watch = new Stopwatch();
        watch.Start();
        for (int i = 0; i < 10000000; i++)
        {
            x = x + 1; // no function
        }
        watch.Stop();
        System.Console.WriteLine(watch.ElapsedTicks);

        watch.Restart();
        for (int i = 0; i < 10000000; i++)
        {
            x = a.Func1(x); // non virtual
        }
        watch.Stop();
        System.Console.WriteLine(watch.ElapsedTicks);

        watch.Restart();
        for (int i = 0; i < 10000000; i++)
        {
            x = a.Func2(x); // virtual on a
        }
        watch.Stop();
        System.Console.WriteLine(watch.ElapsedTicks);

        watch.Restart();
        for (int i = 0; i < 10000000; i++)
        {
            x = b.Func2(x); // virtual on b
        }
        watch.Stop();
        System.Console.WriteLine(watch.ElapsedTicks);

        watch.Restart();
        for (int i = 0; i < 10000000; i++)
        {
            x = c.Func2(x); // virtual on B typed as A
        }
        watch.Stop();
        System.Console.WriteLine(watch.ElapsedTicks);

        System.Console.WriteLine(x); // so the compiler doesn't optimize it away
    }
}

Quando lo eseguo (Release, senza debugging, Any CPU, .NET 4.5, x64 Windows 8.1) Ottengo quei risultati:

9904
8921
56921
60076
58289

La prima cosa interessante è che chiamare il metodo non virtuale non è diverso dal non utilizzare affatto il metodo. Questo dimostra chiaramente che il compilatore sta incoraggiando il codice del metodo in posizione in modo che non ci sia overhead di chiamata alla funzione. Il secondo mostra chiaramente che i metodi virtuali non hanno questa inlining e aggiungono la necessità di ulteriori ricerche. Rendendoli molto più lenti rispetto alle chiamate ai metodi non virtuali.

Ma non avere un'idea sbagliata. Nel grande schema delle cose, l'overhead causato dai metodi virtuali è trascurabile rispetto al sovraccarico dell'algoritmo e ai problemi di cache dovuti all'accesso alla memoria.

    
risposta data 02.04.2014 - 08:25
fonte
5

Poiché io e altri abbiamo trovato il punto di riferimento @Euphoric sopra illuminante, di seguito è una versione estesa che aggiunge interfacce, lambda, delegati e statici. Come esempio, con .Net 4.5.2 e x86 restituiscono il risultato (in milioni di iterazioni al secondo) costantemente raggruppato in tre segmenti:

No function                   1,388.9 MOps/s
Non-virtual                   1,479.3 MOps/s
Static                        1,201.9 MOps/s

Virtual via class               456.2 MOps/s
Overridden via class            425.9 MOps/s
Base class                      394.3 MOps/s
Non-virtual via interface       357.4 MOps/s
Virtual via interface           466.0 MOps/s

Lambda                          286.9 MOps/s
Delegate                        277.6 MOps/s

Benchmark espanso:

using System;
using System.Diagnostics;

    interface IClassA
    {
        int Func1(int a);
        int Func2(int a);
    }

    class ClassA : IClassA
    {
        public int Func1(int a)
        {
            return a + 2;
        }

        public virtual int Func2(int a)
        {
            return a + 2;
        }

        public static int StaticFunc(int a)
        {
            return a + 2;
        }
    }

    class ClassB : ClassA
    {
        public override int Func2(int a)
        {
            return a + 2;
        }
    }

    delegate int MyDelegate(int a);

    class Program
    {
        static int forDelegate(int a)
        {
            return a + 2;
        }

        public static void Main()
        {
            MethodCall();
        }

        public static void MethodCall()
        {
            const int loops = 500000000;
            int x = 0;
            ClassA a = new ClassA();
            ClassB b = new ClassB();
            ClassA c = new ClassB();

            Console.WriteLine("Method Call Overhead:");

            Stopwatch watch = new Stopwatch();
            watch.Start();
            for (int i = 0; i < loops; i++)
            {
                x = x + 2;
            }
            watch.Stop();
            Report("No function", loops, watch.ElapsedMilliseconds);
            x -= 2 * loops;

            watch.Restart();
            for (int i = 0; i < loops; i++)
            {
                x = a.Func1(x);
            }
            watch.Stop();
            Report("Non-virtual", loops, watch.ElapsedMilliseconds);
            x -= 2 * loops;

            watch.Restart();
            for (int i = 0; i < loops; i++)
            {
                x = ClassA.StaticFunc(x);
            }
            watch.Stop();
            Report("Static", loops, watch.ElapsedMilliseconds);
            x -= 2 * loops;

            watch.Restart();
            for (int i = 0; i < loops; i++)
            {
                x = a.Func2(x); 
            }
            watch.Stop();
            Report("Virtual via class", loops, watch.ElapsedMilliseconds);
            x -= 2 * loops;

            watch.Restart();
            for (int i = 0; i < loops; i++)
            {
                x = b.Func2(x); 
            }
            watch.Stop();
            Report("Overridden via class", loops, watch.ElapsedMilliseconds);
            x -= 2 * loops;

            watch.Restart();
            for (int i = 0; i < loops; i++)
            {
                x = c.Func2(x); 
            }
            watch.Stop();
            Report("Base class", loops, watch.ElapsedMilliseconds);
            x -= 2 * loops;

            IClassA iClassA = a;
            watch.Restart();
            for (int i = 0; i < loops; i++)
            {
                x = iClassA.Func1(x);
            }
            watch.Stop();
            Report("Non-virtual via interface", loops, watch.ElapsedMilliseconds);
            x -= 2 * loops;

            watch.Restart();
            for (int i = 0; i < loops; i++)
            {
                x = iClassA.Func2(x);
            }
            watch.Stop();
            Report("Virtual via interface", loops, watch.ElapsedMilliseconds);
            x -= 2 * loops;

            Func<int, int> func = l => l + 2;
            watch.Restart();
            for (int i = 0; i < loops; i++)
            {
                x = func(x);
            }
            watch.Stop();
            Report("Lambda", loops, watch.ElapsedMilliseconds);
            x -= 2 * loops;

            MyDelegate myDelegate = forDelegate;
            watch.Restart();
            for (int i = 0; i < loops; i++)
            {
                x = myDelegate(x);
            }
            watch.Stop();
            Report("Delegate", loops, watch.ElapsedMilliseconds);
            x -= 2 * loops;

            System.Console.ReadKey();
            System.Console.WriteLine(x); // so the compiler doesn't optimize it away
        }

        static void Report(string message, int iterations, long milliseconds)
        {
            System.Console.WriteLine(string.Format("{0,-26:} {1,10:N1} MOps/s, {2,7:N3} s", message, (double)iterations / 1000.0 / milliseconds, milliseconds / 1000.0));
        }
    }

Spero che ti aiuti!

    
risposta data 12.04.2015 - 19:44
fonte
2

Ero curioso di sapere perché Lambdas e i delegati si sono esibiti così male nel test di Kristian Wedberg
Ho pensato che forse dovrebbero essere chiamati una volta prima che il test inizi a verificare che siano JIT, in quanto ciò potrebbe distorcere il tempo.

Nel mio test su .NET 4.6.1 e x86 il lambda esegue costantemente sia le chiamate di interfaccia che le chiamate, solo il delegato è stato più lento.
Ho quindi diviso il caso di test delegato in 3 per testare diversi modi di creare il delegato:

  1. allocazione da un gruppo di metodi (metodo istanza )
  2. allocazione da un gruppo di metodi (metodo statico )
  3. allocazione da un lambda

Risultati:

No function                   1.404,5 MOps/s,   0,356 s
Non-virtual                     429,2 MOps/s,   1,165 s
Static                          432,9 MOps/s,   1,155 s
Virtual via class               427,7 MOps/s,   1,169 s
Overridden via class            399,4 MOps/s,   1,252 s
Base class                      363,6 MOps/s,   1,375 s
Non-virtual via interface       331,3 MOps/s,   1,509 s
Virtual via interface           337,2 MOps/s,   1,483 s
Lambda                          339,4 MOps/s,   1,473 s
Delegate (inst. mthd grp)       338,5 MOps/s,   1,477 s
Delegate (stat. mthd grp)       204,0 MOps/s,   2,451 s
Delegate (lambda)               341,5 MOps/s,   1,464 s

È interessante notare che 1) e 3) funzionano in modo coerente così come i test lambda o dell'interfaccia, solo 2) è più lento (questa era la versione usata nel test di Kristian). Sono sconcertato sul motivo per cui un metodo statico di tutte le cose dovrebbe essere più lento.

Ecco il test modificato:

using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;

interface IClassA {
    int Func1(int a);
    int Func2(int a);
}

class ClassA : IClassA {
    [MethodImpl(MethodImplOptions.NoInlining)]
    public int Func1(int a) {
        return a + 2;
    }

    public virtual int Func2(int a) {
        return a + 2;
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    public static int StaticFunc(int a) {
        return a + 2;
    }
}

class ClassB : ClassA {
    public override int Func2(int a) {
        return a + 2;
    }
}

delegate int MyDelegate(int a);

class Program {
    static int staticDelegate(int a) {
        return a + 2;
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    static Func<int, int> GetFunc() {
        return l => l + 2;
    }

    public static void Main() {
        MethodCall();
    }

    public static void MethodCall() {
        const int loops = 500000000;
        int x = 0;
        ClassA a = new ClassA();
        ClassB b = new ClassB();
        ClassA c = new ClassB();

        Console.WriteLine("Method Call Overhead:");

        Stopwatch watch = new Stopwatch();
        watch.Start();
        for (int i = 0; i < loops; i++) {
            x = x + 2;
        }
        watch.Stop();
        Report("No function", loops, watch.ElapsedMilliseconds);
        x -= 2 * loops;

        watch.Restart();
        for (int i = 0; i < loops; i++) {
            x = a.Func1(x);
        }
        watch.Stop();
        Report("Non-virtual", loops, watch.ElapsedMilliseconds);
        x -= 2 * loops;

        watch.Restart();
        for (int i = 0; i < loops; i++) {
            x = ClassA.StaticFunc(x);
        }
        watch.Stop();
        Report("Static", loops, watch.ElapsedMilliseconds);
        x -= 2 * loops;

        watch.Restart();
        for (int i = 0; i < loops; i++) {
            x = a.Func2(x);
        }
        watch.Stop();
        Report("Virtual via class", loops, watch.ElapsedMilliseconds);
        x -= 2 * loops;

        watch.Restart();
        for (int i = 0; i < loops; i++) {
            x = b.Func2(x);
        }
        watch.Stop();
        Report("Overridden via class", loops, watch.ElapsedMilliseconds);
        x -= 2 * loops;

        watch.Restart();
        for (int i = 0; i < loops; i++) {
            x = c.Func2(x);
        }
        watch.Stop();
        Report("Base class", loops, watch.ElapsedMilliseconds);
        x -= 2 * loops;

        IClassA iClassA = a;
        watch.Restart();
        for (int i = 0; i < loops; i++) {
            x = iClassA.Func1(x);
        }
        watch.Stop();
        Report("Non-virtual via interface", loops, watch.ElapsedMilliseconds);
        x -= 2 * loops;

        watch.Restart();
        for (int i = 0; i < loops; i++) {
            x = iClassA.Func2(x);
        }
        watch.Stop();
        Report("Virtual via interface", loops, watch.ElapsedMilliseconds);
        x -= 2 * loops;

        Func<int, int> func = GetFunc();
        x += func(0) - 2; // call once to JIT
        watch.Restart();
        for (int i = 0; i < loops; i++) {
            x = func(x);
        }
        watch.Stop();
        Report("Lambda", loops, watch.ElapsedMilliseconds);
        x -= 2 * loops;

        MyDelegate myDelegate;

        myDelegate = a.Func1;
        x += myDelegate(0) - 2; // call once to JIT
        watch.Restart();
        for (int i = 0; i < loops; i++) {
            x = myDelegate(x);
        }
        watch.Stop();
        Report("Delegate (inst. mthd grp)", loops, watch.ElapsedMilliseconds);
        x -= 2 * loops;

        myDelegate = Program.staticDelegate;
        x += myDelegate(0) - 2; // call once to JIT
        watch.Restart();
        for (int i = 0; i < loops; i++) {
            x = myDelegate(x);
        }
        watch.Stop();
        Report("Delegate (stat. mthd grp)", loops, watch.ElapsedMilliseconds);
        x -= 2 * loops;

        myDelegate = new MyDelegate(i => i + 2);
        x += myDelegate(0) - 2; // call once to JIT
        watch.Restart();
        for (int i = 0; i < loops; i++) {
            x = myDelegate(x);
        }
        watch.Stop();
        Report("Delegate (lambda)", loops, watch.ElapsedMilliseconds);
        x -= 2 * loops;

        Console.ReadKey();
        Console.WriteLine(x); // so the compiler doesn't optimize it away
    }

    static void Report(string message, int iterations, long milliseconds) {
        Console.WriteLine(string.Format("{0,-26:} {1,10:N1} MOps/s, {2,7:N3} s", message, (double)iterations / 1000.0 / milliseconds, milliseconds / 1000.0));
    }
}
    
risposta data 25.02.2016 - 18:44
fonte

Leggi altre domande sui tag