Pertanyaan Kinerja panggilan virtual "langsung" vs. antarmuka panggilan dalam C #


Patokan ini muncul untuk menunjukkan bahwa memanggil metode virtual langsung pada referensi objek lebih cepat daripada menyebutnya pada referensi ke antarmuka objek ini mengimplementasikan.

Dengan kata lain:

interface IFoo {
    void Bar();
}

class Foo : IFoo {
    public virtual void Bar() {}
}

void Benchmark() {
    Foo f = new Foo();
    IFoo f2 = f;
    f.Bar(); // This is faster.
    f2.Bar();    
}

Datang dari dunia C ++, saya akan berharap bahwa kedua panggilan ini akan diimplementasikan secara identik (sebagai pencarian tabel virtual sederhana) dan memiliki kinerja yang sama. Bagaimana C # mengimplementasikan panggilan virtual dan apa ini "ekstra" pekerjaan yang tampaknya dilakukan ketika memanggil melalui antarmuka?

--- EDIT ---

OK, jawaban / komentar saya sampai sejauh ini menyiratkan bahwa ada dereferensi pointer ganda untuk panggilan virtual melalui antarmuka versus hanya satu dereference untuk panggilan virtual melalui objek.

Jadi bisa tolong seseorang menjelaskan Mengapa apakah itu perlu? Apa struktur tabel virtual dalam C #? Apakah itu "datar" (seperti yang khas untuk C ++) atau tidak? Apa pengorbanan desain yang dibuat dalam desain bahasa C # yang mengarah ke ini? Saya tidak mengatakan ini adalah desain yang "buruk", saya hanya ingin tahu mengapa itu perlu.

Singkatnya, saya ingin memahami apa yang dilakukan alat saya di bawah kap mesin sehingga saya dapat menggunakannya dengan lebih efektif. Dan saya akan sangat menghargai jika saya tidak mendapatkan jawaban "Anda tidak seharusnya mengetahui hal itu" atau "menggunakan bahasa lain".

--- EDIT 2 ---

Hanya untuk membuatnya jelas kita tidak berurusan dengan beberapa kompiler dari optimasi JIT di sini yang menghilangkan pengiriman dinamis: Saya memodifikasi patokan yang disebutkan dalam pertanyaan asli untuk instantiate satu kelas atau yang lain secara acak pada saat run-time. Karena instantiasi terjadi setelah kompilasi dan setelah pemuatan perakitan / JITing, tidak ada cara untuk menghindari pengiriman dinamis dalam kedua kasus:

interface IFoo {
    void Bar();
}

class Foo : IFoo {
    public virtual void Bar() {
    }
}

class Foo2 : Foo {
    public override void Bar() {
    }
}

class Program {

    static Foo GetFoo() {
        if ((new Random()).Next(2) % 2 == 0)
            return new Foo();
        return new Foo2();
    }

    static void Main(string[] args) {

        var f = GetFoo();
        IFoo f2 = f;

        Console.WriteLine(f.GetType());

        // JIT warm-up
        f.Bar();
        f2.Bar();

        int N = 10000000;
        Stopwatch sw = new Stopwatch();

        sw.Start();
        for (int i = 0; i < N; i++) {
            f.Bar();
        }
        sw.Stop();
        Console.WriteLine("Direct call: {0:F2}", sw.Elapsed.TotalMilliseconds);

        sw.Reset();
        sw.Start();
        for (int i = 0; i < N; i++) {
            f2.Bar();
        }
        sw.Stop();
        Console.WriteLine("Through interface: {0:F2}", sw.Elapsed.TotalMilliseconds);

        // Results:
        // Direct call: 24.19
        // Through interface: 40.18

    }

}

--- EDIT 3 ---

Jika ada yang tertarik, di sini adalah bagaimana saya Visual C ++ 2010 memaparkan sebuah instance dari kelas yang melipatgandakan mewarisi kelas lain:

Kode:

class IA {
public:
    virtual void a() = 0;
};

class IB {
public:
    virtual void b() = 0;
};

class C : public IA, public IB {
public:
    virtual void a() override {
        std::cout << "a" << std::endl;
    }
    virtual void b() override {
        std::cout << "b" << std::endl;
    }
};

Debugger:

c   {...}   C
    IA  {...}   IA
        __vfptr 0x00157754 const C::`vftable'{for `IA'} *
            [0] 0x00151163 C::a(void)   *
    IB  {...}   IB
        __vfptr 0x00157748 const C::`vftable'{for `IB'} *
            [0] 0x0015121c C::b(void)   *

Beberapa pointer meja virtual terlihat jelas, dan sizeof(C) == 8 (dalam bentuk 32-bit).

Itu...

C c;
std::cout << static_cast<IA*>(&c) << std::endl;
std::cout << static_cast<IB*>(&c) << std::endl;

..pencetakan ...

0027F778
0027F77C

... menunjukkan bahwa pointer ke berbagai antarmuka dalam objek yang sama sebenarnya menunjuk ke bagian yang berbeda dari objek itu (yaitu berisi alamat fisik yang berbeda).


55
2017-08-29 01:14


asal


Jawaban:


Saya pikir artikelnya di http://msdn.microsoft.com/en-us/magazine/cc163791.aspx akan menjawab pertanyaan Anda. Khususnya, lihat bagian Antarmuka Peta Vtable dan Peta Antarmuka, dan bagian berikut pada Virtual Dispatch.

Mungkin ada kemungkinan bagi kompiler JIT untuk mencari tahu dan mengoptimalkan kode untuk kasus sederhana Anda. Tetapi tidak dalam kasus umum.

IFoo f2 = GetAFoo();

Dan GetAFoo didefinisikan sebagai mengembalikan suatu IFoo, maka compiler JIT tidak akan dapat mengoptimalkan panggilan.


23
2017-09-27 17:14



Berikut ini tampilan dis-assembly (Hans benar):

            f.Bar(); // This is faster.
00000062  mov         rax,qword ptr [rsp+20h] 
00000067  mov         rax,qword ptr [rax] 
0000006a  mov         rcx,qword ptr [rsp+20h] 
0000006f  call        qword ptr [rax+60h] 
            f2.Bar();
00000072  mov         r11,7FF000400A0h 
0000007c  mov         qword ptr [rsp+38h],r11 
00000081  mov         rax,qword ptr [rsp+28h] 
00000086  cmp         byte ptr [rax],0 
00000089  mov         rcx,qword ptr [rsp+28h] 
0000008e  mov         r11,qword ptr [rsp+38h] 
00000093  mov         rax,qword ptr [rsp+38h] 
00000098  call        qword ptr [rax] 

19
2017-08-29 01:54



Saya mencoba tes Anda dan pada mesin saya, dalam konteks tertentu, hasilnya sebenarnya adalah sebaliknya.

Saya menjalankan Windows 7 x64 dan saya telah membuat proyek Aplikasi Konsol Visual Studio 2010 yang telah saya salin kode Anda. Jika sebuah kompilasi proyek di Mode debug dan dengan target platform sebagai x86 hasilnya adalah sebagai berikut:

Panggilan langsung: 48,38
  Melalui antarmuka: 42,43

Sebenarnya setiap kali saat menjalankan aplikasi itu akan memberikan hasil yang sedikit berbeda, tetapi antarmuka panggilan akan selalu lebih cepat. Saya berasumsi bahwa karena aplikasi dikompilasi sebagai x86, maka akan dijalankan oleh OS melalui WOW.

Untuk referensi lengkap, di bawah ini adalah hasil untuk sisa konfigurasi kompilasi dan kombinasi target.

Melepaskan mode dan x86 target
Panggilan langsung: 23,02
Melalui antarmuka: 32,73

Debug mode dan x64 target
Panggilan langsung: 49,49
Melalui antarmuka: 56,97

Melepaskan mode dan x64 target
Panggilan langsung: 19,60
Melalui antarmuka: 26,45

Semua tes di atas dibuat dengan. Net 4.0 sebagai platform target untuk compiler. Ketika beralih ke 3.5 dan mengulangi pengujian di atas, panggilan melalui antarmuka selalu lebih lama daripada panggilan langsung.

Jadi, tes di atas agak memperumit keadaan karena kelihatannya perilaku yang Anda lihat tidak selalu terjadi.

Pada akhirnya, dengan risiko membuat Anda sedih, saya ingin menambahkan beberapa pemikiran. Banyak orang menambahkan komentar bahwa perbedaan kinerja sangat kecil dan dalam pemrograman dunia nyata Anda seharusnya tidak peduli dengan mereka dan saya setuju dengan sudut pandang ini. Ada dua alasan utama untuk itu.

Yang pertama dan yang paling diiklankan adalah bahwa .Net dibangun di tingkat yang lebih tinggi untuk memungkinkan pengembang untuk fokus pada tingkat aplikasi yang lebih tinggi. Basis data atau panggilan layanan eksternal ribuan atau kadang jutaan kali lebih lambat daripada pemanggilan metode virtual. Memiliki arsitektur tingkat tinggi yang baik dan berfokus pada konsumen kinerja besar akan selalu membawa hasil yang lebih baik dalam aplikasi modern daripada menghindari double-pointer-dereferences.

Yang kedua dan lebih tidak jelas adalah bahwa tim .Net dengan membangun kerangka kerja pada tingkat yang lebih tinggi sebenarnya telah memperkenalkan serangkaian tingkat abstraksi yang kompilator just in time akan dapat digunakan untuk optimasi pada platform yang berbeda. Semakin banyak akses yang akan mereka berikan kepada lapisan bawah, semakin banyak pengembang akan dapat mengoptimalkan untuk platform tertentu, tetapi semakin sedikit kompiler runtime yang dapat dilakukan untuk yang lain. Itulah teori setidaknya dan itulah mengapa hal-hal tidak didokumentasikan dengan baik seperti dalam C ++ mengenai hal khusus ini.


11
2017-10-03 22:39



Saya pikir kasus fungsi virtual murni dapat menggunakan tabel fungsi virtual sederhana, seperti kelas turunan lainnya Foo implementasi Bar hanya akan mengubah pointer fungsi virtual ke Bar.

Di sisi lain, memanggil fungsi antarmuka IFoo: Bar tidak bisa melakukan pencarian pada sesuatu seperti IFootabel fungsi virtual, karena setiap implementasi IFootidak perlu menerapkan fungsi atau antarmuka lain yang tidak perlu Foo tidak. Jadi posisi entri tabel fungsi virtual untuk Bar dari yang lain class Fubar: IFoo tidak boleh cocok dengan posisi entri tabel fungsi virtual Bar di class Foo:IFoo.

Jadi panggilan fungsi virtual murni dapat bergantung pada indeks yang sama dari pointer fungsi di dalam tabel fungsi virtual di setiap kelas turunan, sementara antarmuka panggilan harus mencari indeks ini terlebih dahulu.


1
2017-10-04 10:40



Aturan umumnya adalah: Kelas cepat. Antarmuka lambat.

Itulah salah satu alasan untuk rekomendasi "Bangun hierarki dengan kelas dan gunakan antarmuka untuk perilaku intra-hierarki".

Untuk metode virtual, perbedaannya mungkin sedikit (seperti 10%). Tetapi untuk metode non-virtual dan bidang perbedaannya sangat besar. Pertimbangkan program ini.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace InterfaceFieldConsoleApplication
{
    class Program
    {
        public abstract class A
        {
            public int Counter;
        }

        public interface IA
        {
            int Counter { get; set; }
        }

        public class B : A, IA
        {
            public new int Counter { get { return base.Counter; } set { base.Counter = value; } }
        }

        static void Main(string[] args)
        {
            var b = new B();
            A a = b;
            IA ia = b;
            const long LoopCount = (int) (100*10e6);
            var stopWatch = new Stopwatch();
            stopWatch.Start();
            for (int i = 0; i < LoopCount; i++)
                a.Counter = i;
            stopWatch.Stop();
            Console.WriteLine("a.Counter: {0}", stopWatch.ElapsedMilliseconds);
            stopWatch.Reset();
            stopWatch.Start();
            for (int i = 0; i < LoopCount; i++)
                ia.Counter = i;
            stopWatch.Stop();
            Console.WriteLine("ia.Counter: {0}", stopWatch.ElapsedMilliseconds);
            Console.ReadKey();
        }
    }
}

Keluaran:

a.Counter: 1560
ia.Counter: 4587

0
2017-09-29 14:18