programing

가상 기능 vs. 기능 포인터 - 성능?

bestprogram 2023. 10. 19. 22:37

가상 기능 vs. 기능 포인터 - 성능?

C++ 가상 함수는 다형 베이스 클래스에서 C 스타일 함수 포인터를 호출하는 것만큼 빠르게 호출됩니까?정말로 다른 점이 있습니까?

함수 포인터를 사용하는 성능 지향 코드를 리팩토링하여 다형성에서 가상 함수로 변경하는 것을 고려하고 있습니다.

대부분의 C++ 구현은 이와 유사하게 작동합니다. (그리고 아마도 C로 컴파일된 첫 번째 구현은 다음과 같은 코드를 생성했습니다.)

struct ClassVTABLE {
    void (* virtuamethod1)(Class *this);
    void (* virtuamethod2)(Class *this, int arg);
};

struct Class {
    ClassVTABLE *vtable;
};

그렇다면, 예를 들어,Class x, 메소드 호출virtualmethod1e기 입니다.x.vtable->virtualmethod1(&x), 따라서 하나의 추가 디레퍼런스, 하나의 인덱스 룩업.vtable, 그리고 하나의 추가 논쟁 (=this스택에 밀어 넣거나 레지스터에 전달합니다.

그러나 컴파일러는 함수 내의 인스턴스에 대해 반복되는 메서드 호출을 최적화할 수 있습니다.Class x구성된 후 클래스를 변경할 수 없습니다. 컴파일러는 전체를 고려할 수 있습니다.x.vtable->virtualmethod1일반적인 서브 express온으로 루프 밖으로 이동합니다.따라서 이 경우 단일 함수 내에서 반복되는 가상 메서드 호출은 단순 함수 포인터를 통해 함수를 호출하는 것과 속도가 같습니다.

C++ 가상 함수는 다형 베이스 클래스에서 C 스타일 함수 포인터를 호출하는 것만큼 빠르게 호출됩니까?정말로 다른 점이 있습니까?

사과와 오렌지."1 대 1" 수준의 아주 작은 수준에서 가상 기능 호출에는 간접/인덱스 오버헤드가 발생하기 때문에 작업량이 조금 더 필요합니다.vptr.vtable입적

하지만 가상 기능을 통한 상담 속도는 더욱 빨라짐

어떻게 이럴 수가 있어요?가상 함수 호출은 약간 더 많은 작업이 필요하다고 했는데, 사실입니다.

사람들이 잊는 경향이 있는 것은 여기서 더 면밀한 비교를 시도하는 것입니다. (비록 사과와 오렌지이지만, 사과와 오렌지를 조금 덜 만들기 위해 노력하는 것입니다.)우리는 일반적으로 하나의 가상 기능만으로 클래스를 만들지 않습니다.만약 그렇다면 성능(코드 크기와 같은 것도 마찬가지)은 분명히 함수 포인터를 선호할 것입니다.우리는 종종 다음과 같은 것을 가지고 있습니다.

class Foo
{
public:
    virtual ~Foo() {}
    virtual f1() = 0;
    virtual f2() = 0;
    virtual f3() = 0;
    virtual f4() = 0;
};

... 이 경우 더 직접적인 함수 포인터 비유는 다음과 같습니다.

struct Bar
{
     void (*f1)();
     void (*f2)();
     void (*f3)();
     void (*f4)();
};

이런 종류의 경우, 각 인스턴스에서 가상 기능을 호출합니다.Foo보다 훨씬 더 효율적일 수 있습니다.Bar. 그것은 바로 그 사실 때문입니다.Foo단일 vptr만 반복적으로 액세스되는 중앙 vtable에 저장하면 됩니다.이를 통해 기준 지역성이 향상됩니다(소규모).Foos그리고 잠재적으로 캐시 라인에 더 적합하고 수적으로 더 자주 액세스할 수 있는Foo'scentral vtable).

Bar, 반면에, 더 많은 메모리를 요구하고 효과적으로 내용을 복제하고 있습니다.Foo'svtable: 각 인스턴스에서Bar(100만 건의 사례가 있다고 가정해 보겠습니다.Foo그리고.Bar 그 경우 중복되는 데이터의 크기가 부풀려집니다.Bar함수 포인터 호출당 약간 적은 작업을 수행하는 비용을 훨씬 초과하는 경우가 많습니다.

만약 개체당 하나의 함수 포인터만 저장하면 되는데, 이것이 매우 인기 있는 장소였다면, 함수 포인터를 저장하는 것이 좋을 수도 있습니다(예: 원격으로 유사한 것을 구현하는 사람에게 유용할 수도 있습니다).std::function함수 포인터를 저장합니다).

따라서 이는 일종의 사과와 오렌지이지만, 이와 유사한 사용 사례를 모델링하면 중앙의 공유 함수 주소 테이블(C 또는 C++)을 저장하는 vtable 방식이 훨씬 더 효율적일 수 있습니다.

개체에 저장된 단일 함수 포인터가 하나인 경우와 가상 함수가 하나인 vtable인 경우를 모델링하는 경우에는 함수 포인터가 조금 더 효율적일 수 있습니다.

많은 차이를 보일 것 같지는 않지만, 이 모든 것들과 마찬가지로, 종종 작은 세부 사항(예를 들어 컴파일러가 다음을 통과해야 함)이 있습니다.this가상 함수를 가리키는 포인터)는 성능의 차이를 유발할 수 있습니다.virtual함수 자체는 "후드 아래" 함수 포인터이므로 컴파일러가 작업을 완료하면 두 경우 모두 상당히 유사한 코드를 얻을 수 있습니다.

가상 기능을 잘 활용하는 것처럼 들리는데, 누군가 반대하며 "성능 차이가 날 것"이라고 한다면 "증명해보라"고 할 것입니다.그러나 이러한 논의를 피하고 싶다면 기존 코드의 성능을 측정하는 벤치마크(이미 없는 경우)를 만들고 재팩터링(또는 일부)하여 결과를 비교합니다.이상적으로, 두 개의 다른 기계에서 테스트를 수행하면 기계에서는 더 잘 작동하지만 일부 다른 유형의 기계(세대 프로세서, 제조업체 또는 프로세서 등)에서는 잘 작동하지 않는 결과를 얻을 수 있습니다.

가상 함수 호출은 두 개의 역참조를 포함하며, 그 중 하나는 색인화된, 즉 다음과 같은 것을 포함합니다.*(object->_vtable[3])().

함수 포인터를 통한 호출에는 하나의 역참조가 포함됩니다.

메서드 호출은 숨겨진 인수를 다음과 같이 수신해야 합니다.this.

메서드 본문이 실질적으로 비어 있고 인수나 반환 값이 없는 경우가 아니라면 차이점을 알아차릴 가능성이 거의 없습니다.

함수 포인터 호출과 가상 함수 호출의 차이는 위에서 이미 병목 현상이라고 측정하지 않는 한 무시할 수 있습니다.

유일한 차이점은 다음과 같습니다.

  • 가상 함수에는 vtable에 대한 메모리 읽기와 함수의 주소에 대한 간접 호출이 있습니다.
  • 함수 포인터는 함수에 대한 간접 호출을 한 번만 갖습니다.

이는 가상 함수가 함수 포인터가 이미 알고 있는 동안 호출할 함수의 주소를 조회해야 하기 때문입니다(함수 포인터가 자체에 저장되어 있기 때문입니다).

C++로 작업하고 있으니 가상화 방법이 최선의 방법일 것입니다.

속도 테스트 결과

#include <iostream>
#include <vector>

#define CALCULATING return 1;
//#define CALCULATING int res = 0;\
                    for (int i = 0; i < 10000; i++)\
                        res += i;\
                    return res;

static int Deriver1_foo_pf_impl()
{
    CALCULATING
}

int f_foo()
{
    CALCULATING
}

inline int if_foo()
{
    CALCULATING
}

class MyClass
{
public:
    int foo()
    {
        CALCULATING
    }
};

class IBase
{
public:
    virtual int foo()
    {
        CALCULATING
    }
};

class Deriver1 : public IBase
{
public:
    int foo() override
    {
        CALCULATING
    }
};

class Deriver2 : public Deriver1
{
public:
    int foo() override
    {
        CALCULATING
    }
};

class IBase_pf
{
public:
    int (*foo)();
};

class Deriver1_pf : public IBase_pf
{
protected:

public:
    Deriver1_pf()
    {
        this->foo = &f_foo;
    }
};

int main()
{
#ifdef _DEBUG
    std::cout << "DEBUG VERSION" << std::endl << std::endl;
#else
    std::cout << "RELEASE VERSION" << std::endl << std::endl;
#endif // _DEBUG

    std::cout.precision(10);

    unsigned long long countOfCalls = 1000000000;
    unsigned long long trash = 0;

    int (*pf_variable)() = &f_foo;

    Deriver2 vpt;
    Deriver1_pf fpo;
    MyClass mcf;

    clock_t start;
    clock_t end;

    std::cout << "Count of call funtions and methods : " << countOfCalls << std::endl << std::endl;

    start = clock();
    for (int i = 0; i < countOfCalls; i++)
    {
        trash += vpt.foo();
    }
    end = clock();
    
    std::cout << "Virtual method : " << std::endl << "\t";
    std::cout << "time : " << end - start << ", calling per second : " << static_cast<double>(countOfCalls) / (static_cast<double>(end - start)) / 1000 << "(millions per sec), trash : " << trash << std::endl;
    trash = 0;

    start = clock();

    for (int i = 0; i < countOfCalls; i++)
    {
        trash += fpo.foo();
    }
    end = clock();

    std::cout << "Function poiter from base overridden in deriver : " << std::endl << "\t";
    std::cout << "time : " << end - start << ", calling per second : " << static_cast<double>(countOfCalls) / (static_cast<double>(end - start)) / 1000 << "(millions per sec), trash : " << trash << std::endl;
    trash = 0;

    start = clock();

    for (int i = 0; i < countOfCalls; i++)
    {
        trash += mcf.foo();
    }
    end = clock();

    std::cout << "Classic method : " << std::endl << "\t";
    std::cout << "time : " << end - start << ", calling per second : " << static_cast<double>(countOfCalls) / (static_cast<double>(end - start)) / 1000 << "(millions per sec), trash : " << trash << std::endl;
    trash = 0;

    start = clock();
    for (int i = 0; i < countOfCalls; i++)
    {
        trash += pf_variable();
    }
    end = clock();

    std::cout << "Call classic c-style function by function pointer (Callback) : " << std::endl << "\t";
    std::cout << "time : " << end - start << ", calling per second : " << static_cast<double>(countOfCalls) / (static_cast<double>(end - start)) / 1000 << "(millions per sec), trash : " << trash << std::endl;
    trash = 0;

    start = clock();
    for (int i = 0; i < countOfCalls; i++)
    {
        trash += f_foo();
    }
    end = clock();

    std::cout << "Classic C-style function : " << std::endl << "\t";
    std::cout << "time : " << end - start << ", calling per second : " << static_cast<double>(countOfCalls) / (static_cast<double>(end - start)) / 1000 << "(millions per sec), trash : " << trash << std::endl;
    trash = 0;

    start = clock();
    for (int i = 0; i < countOfCalls; i++)
    {
        trash += if_foo();
    }
    end = clock();

    std::cout << "Inline classic C-style function : " << std::endl << "\t";
    std::cout << "time : " << end - start << ", calling per second : " << static_cast<double>(countOfCalls) / (static_cast<double>(end - start)) / 1000 << "(millions per sec), trash : " << trash << std::endl;
    trash = 0;

    return 0;
}

가상 메소드는 콜백보다 더 빨리 작동합니다.이는 가상 메소드에 대한 최적화의 결과일 수도 있고 콜백 캐시가 누락된 결과일 수도 있고 둘 다일 수도 있습니다.

언급URL : https://stackoverflow.com/questions/17959246/virtual-function-vs-function-pointer-performance