Nice programing

"직접"가상 호출과 C #의 인터페이스 호출 성능

nicepro 2020. 12. 5. 10:39
반응형

"직접"가상 호출과 C #의 인터페이스 호출 성능


이 벤치 마크 는 객체 참조에서 직접 가상 메서드를 호출하는 것이이 객체가 구현하는 인터페이스에 대한 참조에서 호출하는 것보다 빠르다는 것을 보여줍니다.

다시 말해:

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();    
}

C ++ 세계에서 왔기 때문에이 두 호출이 모두 동일하게 (간단한 가상 테이블 조회로) 구현되고 동일한 성능을 가질 것이라고 예상했을 것입니다. C #은 가상 호출을 어떻게 구현하며 인터페이스를 통해 호출 할 때 분명히 수행되는 "추가"작업은 무엇입니까?

--- 편집하다 ---

좋아, 지금까지받은 답변 / 댓글은 인터페이스를 통한 가상 호출에 대한 이중 포인터 역 참조와 개체를 통한 가상 호출에 대한 단 하나의 역 참조가 있음을 암시합니다.

그렇다면 누군가 필요한지 설명해 주 시겠습니까? C #에서 가상 테이블의 구조는 무엇입니까? "플랫"(일반적인 C ++)입니까? 이로 이어지는 C # 언어 디자인에서 만들어진 디자인 절충점은 무엇입니까? 나는 이것이 "나쁜"디자인이라고 말하는 것이 아니라 왜 필요한지 궁금합니다.

요컨대, 내 도구가 더 효과적으로 사용할 수 있도록 내 도구의 기능 이해 하고 싶습니다 . 그리고 더 이상 "당신은 그것을 알면 안된다"또는 "다른 언어 사용"유형의 대답을 얻지 못했다면 감사하겠습니다.

--- 편집 2 ---

명확히하기 위해 여기서는 동적 디스패치를 ​​제거하는 JIT 최적화 컴파일러를 다루지 않습니다. 원래 질문에서 언급 한 벤치 마크를 수정하여 런타임에 한 클래스 또는 다른 클래스를 무작위로 인스턴스화했습니다. 인스턴스화는 컴파일 후 및 어셈블리로드 / JITing 후에 발생하므로 두 경우 모두 동적 디스패치를 ​​피할 수있는 방법이 없습니다.

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

    }

}

--- 편집 3 ---

관심이 있다면 Visual C ++ 2010에서 다른 클래스를 곱하는 클래스의 인스턴스를 레이아웃하는 방법은 다음과 같습니다.

암호:

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

디버거 :

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)   *

다중 가상 테이블 포인터가 명확하게 표시됩니다 sizeof(C) == 8(32 비트 빌드에서).

그만큼...

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

..인쇄물...

0027F778
0027F77C

... 동일한 객체 내의 다른 인터페이스에 대한 포인터가 실제로 해당 객체의 다른 부분을 가리킴을 나타냅니다 (즉, 다른 물리적 주소를 포함 함).


http://msdn.microsoft.com/en-us/magazine/cc163791.aspx 의 기사 가 귀하의 질문에 답할 것이라고 생각합니다 . 특히 Interface Vtable Map 및 Interface Map 섹션과 Virtual Dispatch에 대한 다음 섹션을 참조하십시오.

JIT 컴파일러가 간단한 경우에 맞게 코드를 파악하고 최적화 할 수 있습니다. 그러나 일반적인 경우는 아닙니다.

IFoo f2 = GetAFoo();

그리고 GetAFoo가 반환로 정의 IFoo하고 JIT 컴파일러가 호출을 최적화 할 수 없습니다.


분해는 다음과 같습니다 (Hans가 맞습니다).

            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] 

나는 당신의 테스트를 시도했고 내 컴퓨터에서 특정 상황에서 결과는 실제로 반대입니다.

I am running Windows 7 x64 and I have created a Visual Studio 2010 Console Application project into which I have copied your code. If a compile the project in Debug mode and with the platform target as x86 the output will be the following:

Direct call: 48.38
Through interface: 42.43

Actually every time when running the application it will provide slightly different results, but the interface calls will always be faster. I assume that since the application is compiled as x86, it will be run by the OS through WOW.

For a complete reference, below are the results for the rest of compilation configuration and target combinations.

Release mode and x86 target
Direct call: 23.02
Through interface: 32.73

Debug mode and x64 target
Direct call: 49.49
Through interface: 56.97

Release mode and x64 target
Direct call: 19.60
Through interface: 26.45

All of the above tests were made with .Net 4.0 as the target platform for the compiler. When switching to 3.5 and repeating the above tests, the calls through the interface were always longer than the direct calls.

So, the above tests rather complicate things since it seems that the behavior you spotted is not always happening.

In the end, with the risk of upsetting you, I would like to add a few thoughts. Many people added comments that the performance differences are quite small and in real world programming you should not care about them and I agree with this point of view. There are two main reasons for it.

The first and the most advertised one is that .Net was build on a higher level in order to enable developers to focus on the higher levels of applications. A database or an external service call is thousands or sometimes millions of times slower than virtual method call. Having a good high level architecture and focusing on the big performance consumers will always bring better results in modern applications rather than avoiding double-pointer-dereferences.

The second and more obscure one is that the .Net team by building the framework on a higher level has actually introduced a series of abstraction levels which the just in time compiler would be able to use for optimizations on different platforms. The more access they would give to the under layers the more developers would be able to optimize for a specific platform but the less the runtime compiler would be able to do for the others. That is the theory at least and that is why things are not as well documented as in C++ regarding this particular matter.


The general rule is: Classes are fast. Interfaces are slow.

That's one of the reasons for the recommendation "Build hierarchies with classes and use interfaces for intra-hierarchy behavior".

For virtual methods, the difference might be slight (like 10%). But for non-virtual methods and fields the difference is huge. Consider this program.

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();
        }
    }
}

Output:

a.Counter: 1560
ia.Counter: 4587

I think the pure virtual function case can use a simple virtual function table, as any derived class of Foo implementing Bar would just change the virtual function pointer to Bar.

On the other hand, calling an interface function IFoo:Bar couldn't do a lookup at something like IFoo's virtual function table, because every implementation of IFoo doesn't need to necceserely implement other functions nor interfaces that Foo does. So the virtual function table entry position for Bar from another class Fubar: IFoo must not match the virtual function table entry position of Bar in class Foo:IFoo.

Thus a pure virtual function call can rely on the same index of the function pointer inside the virtual function table in every derived class, while the interface call has to look up the this index first.

참고URL : https://stackoverflow.com/questions/7225205/performance-of-direct-virtual-call-vs-interface-call-in-c-sharp

반응형