C ++ 클래스에서 가상 메서드를 사용할 때의 성능 비용은 얼마입니까?


107

C ++ 클래스 (또는 상위 클래스 중 하나)에 가상 메서드가 하나 이상 있다는 것은 클래스에 가상 테이블이 있고 모든 인스턴스에 가상 포인터가 있음을 의미합니다.

따라서 메모리 비용은 매우 명확합니다. 가장 중요한 것은 인스턴스의 메모리 비용입니다 (특히 인스턴스가 작은 경우, 예를 들어 정수를 포함하려는 경우 :이 경우 모든 인스턴스에 가상 포인터가 있으면 인스턴스 크기가 두 배가 될 수 있습니다.). 가상 테이블이 사용하는 메모리 공간은 실제 메소드 코드가 사용하는 공간에 비해 일반적으로 무시할 수 있다고 생각합니다.

이것은 저에게 질문을 던집니다. 방법을 가상으로 만드는 데 측정 가능한 성능 비용 (즉, 속도 영향)이 있습니까? 런타임시 모든 메서드 호출시 가상 테이블에서 조회가 발생하므로이 메서드에 대한 호출이 매우 자주 발생하고이 메서드가 매우 짧으면 측정 가능한 성능 저하가있을 수 있습니까? 플랫폼에 따라 다르지만 벤치 마크를 실행하는 사람이 있습니까?

내가 묻는 이유는 프로그래머가 가상 메소드를 정의하는 것을 잊었 기 때문에 발생한 버그를 발견했기 때문입니다. 이런 종류의 실수를 본 것은 이번이 처음이 아닙니다. 그리고 저는 생각했습니다. 왜 가상 키워드가 필요 하지 않다고 확신 할 때 가상 키워드 를 제거 하는 대신 필요할 때 가상 키워드를 추가 해야합니까? 성능 비용이 낮 으면 팀에서 다음을 권장합니다. 모든 클래스에서 소멸자를 포함한 모든 메서드를 기본적으로 가상으로 만들고 필요할 때만 제거합니다. 그게 당신에게 미친 것처럼 들립니까?



7
가상 통화와 가상 통화가 아닌 통화를 비교하는 것은 좋지 않습니다. 그들은 다른 기능을 제공합니다. 가상 함수 호출을 C 등가물과 비교하려면 가상 함수의 동등한 기능을 구현하는 코드 비용을 추가해야합니다.
Martin York

switch 문 또는 big if 문입니다. 영리하다면 함수 포인터 테이블을 사용하여 다시 구현할 수 있지만 잘못 될 가능성은 훨씬 높습니다.
Martin York


7
문제는 가상 일 필요가없는 함수 호출에 관한 것이므로 비교가 의미가 있습니다.
Mark Ransom

답변:


104

나는 약간의 타이밍을 실행 3GHz의에서 주문 PowerPC 프로세서에. 해당 아키텍처에서 가상 함수 호출 비용은 직접 (비가 상) 함수 호출보다 7 나노초 더 깁니다.

따라서 함수가 인라인 이외의 것이 일종의 낭비 인 사소한 Get () / Set () 접근 자와 같은 것이 아니라면 비용에 대해 걱정할 가치가 없습니다. 0.5ns로 인라인되는 함수에 대한 7ns 오버 헤드는 심각합니다. 실행하는 데 500ms가 걸리는 함수의 7ns 오버 헤드는 의미가 없습니다.

가상 함수의 큰 비용은 실제로 vtable에서 함수 포인터를 조회하는 것이 아니라 (일반적으로 단일 사이클), 간접 점프는 일반적으로 분기 예측이 불가능합니다. 간접 점프 (함수 포인터를 통한 호출)가 종료되고 새 명령 포인터가 계산 될 때까지 프로세서가 명령을 가져올 수 없기 때문에 이로 인해 큰 파이프 라인 버블이 발생할 수 있습니다. 따라서 가상 함수 호출의 비용은 어셈블리를 살펴 보는 것보다 훨씬 큽니다. 그러나 여전히 7 나노초에 불과합니다.

편집 : Andrew, Not Sure 등은 가상 함수 호출이 명령어 캐시 미스를 유발할 수 있다는 매우 좋은 점을 제기합니다. 캐시에없는 코드 주소로 점프하면 전체 프로그램이 중단되는 동안 명령은 주 메모리에서 가져옵니다. 이것은 항상 중요한 지연입니다. Xenon에서는 약 650 사이클 (내 테스트에 의해)입니다.

그러나 이것은 가상 함수에 특정한 문제가 아닙니다. 캐시에없는 명령어로 건너 뛰면 직접 함수 호출로도 누락이 발생하기 때문입니다. 중요한 것은 함수가 최근 이전에 실행되었는지 여부 (캐시에있을 가능성이 더 높음)와 아키텍처가 정적 (가상이 아닌) 분기를 예측하고 해당 명령을 미리 캐시로 가져올 수 있는지 여부입니다. 내 PPC는 그렇지 않지만 인텔의 최신 하드웨어는 그렇지 않을 수 있습니다.

내 타이밍은 실행에 대한 icache 미스의 영향을 제어하므로 (고의적으로 CPU 파이프 라인을 개별적으로 검사하려고했기 때문에) 비용을 할인합니다.


3
주기 비용은 가져 오기와 분기 종료 사이의 파이프 라인 단계 수와 거의 같습니다. 상당한 비용이 들지 않고 합산 될 수 있지만, 엄격한 고성능 루프를 작성하지 않는 한 더 큰 성능의 물고기를 튀길 수 있습니다.
Crashworks

무엇보다 7 나노초 더 길다. 정상적인 통화가 70 나노초이면 위엄이있는 1 나노초라면 그렇지 않습니다.
Martin York

타이밍을 살펴보면 0.66ns 인라인 비용이 드는 함수의 경우 직접 함수 호출의 차등 오버 헤드가 4.8ns이고 가상 함수 12.3ns (인라인과 비교하여)라는 것을 알았습니다. 함수 자체의 비용이 밀리 초이면 7ns는 아무 의미가 없다는 점을 지적합니다.
Crashworks

2
600 사이클과 비슷하지만 좋은 점입니다. 파이프 라인 버블과 프롤로그 / 에피 로그로 인한 오버 헤드에만 관심이 있었기 때문에 타이밍에서 제외했습니다. icache 누락은 직접 함수 호출과 마찬가지로 쉽게 발생합니다 (Xenon에는 icache 분기 예측기가 없음).
Crashworks

2
사소한 세부 사항이지만 "하지만 이것은 특정 문제가 아닙니다 ..."와 관련하여 캐시에 있어야 하는 추가 페이지 (또는 페이지 경계를 넘어가는 경우 두 개)가 있기 때문에 가상 디스패치의 경우 약간 더 나쁩니다. -클래스의 가상 디스패치 테이블 용.
Tony Delroy 2014 년

19

가상 함수를 호출 할 때 확실히 측정 가능한 오버 헤드가 있습니다. 호출시 vtable을 사용하여 해당 유형의 객체에 대한 함수 주소를 확인해야합니다. 추가 지침은 걱정할 필요가 없습니다. vtables는 많은 잠재적 컴파일러 최적화를 방지 할뿐만 아니라 (유형이 컴파일러의 다형성이므로) I-Cache를 스 래시 할 수도 있습니다.

물론 이러한 불이익이 중요한지 여부는 애플리케이션, 해당 코드 경로가 실행되는 빈도 및 상속 패턴에 따라 다릅니다.

제 생각에는 기본적으로 모든 것을 가상으로 사용하는 것이 다른 방법으로 해결할 수있는 문제에 대한 포괄적 인 해결책입니다.

아마도 클래스가 어떻게 설계 / 문서화 / 작성되는지 살펴볼 수있을 것입니다. 일반적으로 클래스의 헤더는 파생 클래스에서 재정의 할 수있는 함수와 호출 방법을 명확히해야합니다. 프로그래머가이 문서를 작성하게하면 가상 문서로 올바르게 표시되도록하는 데 도움이됩니다.

또한 모든 기능을 가상으로 선언하면 가상으로 표시하는 것을 잊는 것보다 더 많은 버그가 발생할 수 있다고 말하고 싶습니다. 모든 기능이 가상이면 모든 것이 기본 클래스 (공개, 보호, 개인)로 대체 될 수 있습니다. 모든 것이 공정한 게임이됩니다. 우연히 또는 의도적으로 하위 클래스는 기본 구현에서 사용할 때 문제를 일으키는 함수의 동작을 변경할 수 있습니다.


가장 큰 손실 된 최적화는 인라인, 특히 가상 기능이 종종 작거나 비어있는 경우입니다.
Zan Lynx

@Andrew : 흥미로운 관점. 그래도 마지막 단락에 다소 동의하지 않습니다. 기본 클래스에 기본 클래스 save의 특정 함수 구현에 의존하는 함수가 write있는 경우 save코드가 잘못 작성되었거나 write비공개 여야 하는 것 같습니다 .
MiniQuark

2
쓰기가 개인용이라고해서 재정의되는 것을 막지는 않습니다. 이것은 기본적으로 가상으로 만들지 않는 또 다른 주장입니다. 어쨌든 나는 그 반대를 생각하고 있었다. 일반적이고 잘 작성된 구현은 구체적이고 호환되지 않는 동작을 가진 것으로 대체된다.
Andrew Grant

캐싱에 투표-모든 대규모 객체 지향 코드 기반에서 코드 지역성 성능 관행을 따르지 않는 경우 가상 호출이 캐시 누락을 유발하고 중단을 유발하기가 매우 쉽습니다.
확실하지 않음

그리고 icache 중단은 정말 심각 할 수 있습니다. 테스트에서 600 회주기입니다.
Crashworks

9

때에 따라 다르지. :) (다른 것을 기대 했습니까?)

일단 클래스가 가상 함수를 받으면 더 이상 POD 데이터 유형이 될 수 없으며 (이전에도 하나가 아니었을 수 있으며,이 경우 차이가 없습니다) 전체 범위의 최적화를 불가능하게 만듭니다.

일반 POD 유형의 std :: copy ()는 간단한 memcpy 루틴에 의존 할 수 있지만 비 POD 유형은 더 신중하게 처리해야합니다.

vtable을 초기화해야하므로 구성이 훨씬 느려집니다. 최악의 경우 POD 데이터 유형과 비 POD 데이터 유형 간의 성능 차이가 클 수 있습니다.

최악의 경우 실행 속도가 5 배 더 느릴 수 있습니다 (최근에 몇 가지 표준 라이브러리 클래스를 다시 구현하기 위해 수행 한 대학 프로젝트에서 가져온 것입니다. 컨테이너가 저장 한 데이터 유형이 vtable)

물론, 대부분의 경우 측정 가능한 성능 차이를 볼 가능성은 거의 없습니다 . 이는 일부 국경의 경우 비용이 많이들 수 있음 을 지적하기위한 것입니다 .

그러나 여기서 성능이 주요 고려 사항은 아닙니다. 모든 것을 가상으로 만드는 것은 다른 이유로 완벽한 솔루션이 아닙니다.

파생 클래스에서 모든 것을 재정의하도록 허용하면 클래스 불변성을 유지하기가 훨씬 더 어려워집니다. 클래스는 메서드 중 하나를 언제든지 재정의 할 수있을 때 일관된 상태를 유지하는 것을 어떻게 보장합니까?

모든 것을 가상으로 만들면 몇 가지 잠재적 버그를 제거 할 수 있지만 새로운 버그도 도입됩니다.


7

가상 디스패치 기능이 필요한 경우 가격을 지불해야합니다. C ++의 장점은 직접 구현하는 비효율적 인 버전이 아니라 컴파일러에서 제공하는 가상 디스패치의 매우 효율적인 구현을 사용할 수 있다는 것입니다.

그러나 필요하지 않은 경우 오버 헤드로 자신을 벌목하는 것은 너무 멀리 갈 수 있습니다. 그리고 대부분의 클래스는 상속되도록 설계되지 않았습니다. 좋은 기본 클래스를 만들려면 함수를 가상으로 만드는 것 이상이 필요합니다.


좋은 대답이지만, IMO, 후반부에 충분히 강조하지는 않습니다. 필요하지 않은 경우 오버 헤드를 처리하는 것은 매우 솔직히 말도 안되는 일입니다. 사용하지 마십시오. " 누군가가 왜 그것이 비가 상일 수 있고 /되어야 하는지를 정당화 할 때까지 기본적으로 모든 것을 가상으로 만드는 것은 끔찍한 정책입니다.
underscore_d

5

가상 디스패치는 인라인 방지만큼 간접적 인 것이 아니라 일부 대안보다 훨씬 느립니다. 아래에서는 가상 디스패치를 ​​개체에 "유형 (-식별) 번호"를 포함하고 스위치 문을 사용하여 유형별 코드를 선택하는 구현과 대조하여 설명합니다. 이렇게하면 함수 호출 오버 헤드가 완전히 방지됩니다. 로컬 점프 만 수행하면됩니다. 유형별 기능의 강제 지역화 (스위치에서)를 통해 유지 관리 가능성, 재 컴파일 종속성 등에 대한 잠재적 인 비용이 있습니다.


이행

#include <iostream>
#include <vector>

// virtual dispatch model...

struct Base
{
    virtual int f() const { return 1; }
};

struct Derived : Base
{
    virtual int f() const { return 2; }
};

// alternative: member variable encodes runtime type...

struct Type
{
    Type(int type) : type_(type) { }
    int type_;
};

struct A : Type
{
    A() : Type(1) { }
    int f() const { return 1; }
};

struct B : Type
{
    B() : Type(2) { }
    int f() const { return 2; }
};

struct Timer
{
    Timer() { clock_gettime(CLOCK_MONOTONIC, &from); }
    struct timespec from;
    double elapsed() const
    {
        struct timespec to;
        clock_gettime(CLOCK_MONOTONIC, &to);
        return to.tv_sec - from.tv_sec + 1E-9 * (to.tv_nsec - from.tv_nsec);
    }
};

int main(int argc)
{
  for (int j = 0; j < 3; ++j)
  {
    typedef std::vector<Base*> V;
    V v;

    for (int i = 0; i < 1000; ++i)
        v.push_back(i % 2 ? new Base : (Base*)new Derived);

    int total = 0;

    Timer tv;

    for (int i = 0; i < 100000; ++i)
        for (V::const_iterator i = v.begin(); i != v.end(); ++i)
            total += (*i)->f();

    double tve = tv.elapsed();

    std::cout << "virtual dispatch: " << total << ' ' << tve << '\n';

    // ----------------------------

    typedef std::vector<Type*> W;
    W w;

    for (int i = 0; i < 1000; ++i)
        w.push_back(i % 2 ? (Type*)new A : (Type*)new B);

    total = 0;

    Timer tw;

    for (int i = 0; i < 100000; ++i)
        for (W::const_iterator i = w.begin(); i != w.end(); ++i)
        {
            if ((*i)->type_ == 1)
                total += ((A*)(*i))->f();
            else
                total += ((B*)(*i))->f();
        }

    double twe = tw.elapsed();

    std::cout << "switched: " << total << ' ' << twe << '\n';

    // ----------------------------

    total = 0;

    Timer tw2;

    for (int i = 0; i < 100000; ++i)
        for (W::const_iterator i = w.begin(); i != w.end(); ++i)
            total += (*i)->type_;

    double tw2e = tw2.elapsed();

    std::cout << "overheads: " << total << ' ' << tw2e << '\n';
  }
}

성능 결과

내 Linux 시스템에서 :

~/dev  g++ -O2 -o vdt vdt.cc -lrt
~/dev  ./vdt                     
virtual dispatch: 150000000 1.28025
switched: 150000000 0.344314
overhead: 150000000 0.229018
virtual dispatch: 150000000 1.285
switched: 150000000 0.345367
overhead: 150000000 0.231051
virtual dispatch: 150000000 1.28969
switched: 150000000 0.345876
overhead: 150000000 0.230726

이는 인라인 유형 번호 전환 방식이 약 (1.28-0.23) / (0.344-0.23) = 9.2 배 빠르다는 것을 의미합니다. 물론 이것은 테스트 된 정확한 시스템 / 컴파일러 플래그 및 버전 등에 따라 다르지만 일반적으로 표시됩니다.


가상 디스패치에 대한 의견

가상 함수 호출 오버 헤드는 거의 중요하지 않은 것이지만 자주 호출되는 사소한 함수 (게터 및 세터와 같은)에만 해당됩니다. 그럼에도 불구하고 한 번에 많은 것을 가져오고 설정하는 단일 기능을 제공하여 비용을 최소화 할 수 있습니다. 사람들은 가상 디스패치를 ​​너무 많이 걱정하므로 어색한 대안을 찾기 전에 프로파일 링을 수행하십시오. 이들의 주요 문제는 라인 외부 함수 호출을 수행하지만 캐시 활용 패턴을 변경하는 실행 된 코드를 지역화 해제 (더 좋거나 (더 자주) 더 나쁘게)하는 것입니다.


/ 및을 사용하여 "이상한"결과 가 있으므로 코드에 대해 질문했습니다 . 나는 미래의 독자들을 위해 여기서 언급 할 가치가 있다고 생각했습니다. g++clang-lrt
Holt

@Holt : 신비로운 결과를 고려한 좋은 질문입니다! 기회가 반만된다면 며칠 안에 자세히 살펴 볼게요. 건배.
Tony Delroy

3

추가 비용은 대부분의 시나리오에서 사실상 아무것도 아닙니다. (말장난을 용서하십시오). ejac은 이미 합리적인 상대 측정을 게시했습니다.

당신이 포기하는 가장 큰 것은 인라인으로 인한 가능한 최적화입니다. 상수 매개 변수로 함수를 호출하면 특히 유용 할 수 있습니다. 이것은 거의 실제적인 차이를 만들지 않지만, 몇몇 경우에 이것은 엄청날 수 있습니다.


최적화와 관련하여 :
언어 구성의 상대적 비용을 알고 고려하는 것이 중요합니다. Big O 표기법은 이야기의 절반에 불과 합니다. 응용 프로그램은 어떻게 확장됩니까 ? 나머지 절반은 그 앞에있는 상수 요소입니다.

경험상 가상 기능이 병목이라는 명확하고 구체적인 징후가없는 한, 가상 기능을 피하려고하지 않습니다. 깨끗한 디자인이 항상 우선입니다.하지만 다른 사람에게 과도하게 해를 끼치 지 않아야하는 것은 오직 한 명의 이해 관계자입니다 .


Contrived Example : 100 만 개의 작은 요소 배열에있는 빈 가상 소멸자는 최소 4MB의 데이터를 훑어 보며 캐시를 스 래시 할 수 있습니다. 해당 소멸자가 인라인 될 수있는 경우 데이터는 건드리지 않습니다.

라이브러리 코드를 작성할 때 이러한 고려 사항은 너무 이른 것이 아닙니다. 함수 주위에 몇 개의 루프가 배치되는지 결코 알 수 없습니다.


2

다른 모든 사람들은 가상 메소드의 성능 등에 대해 옳지 만, 진짜 문제는 팀이 C ++에서 가상 키워드의 정의에 대해 알고 있는지 여부입니다.

이 코드를 고려하면 출력은 무엇입니까?

#include <stdio.h>

class A
{
public:
    void Foo()
    {
        printf("A::Foo()\n");
    }
};

class B : public A
{
public:
    void Foo()
    {
        printf("B::Foo()\n");
    }
};

int main(int argc, char** argv)
{    
    A* a = new A();
    a->Foo();

    B* b = new B();
    b->Foo();

    A* a2 = new B();
    a2->Foo();

    return 0;
}

여기에 놀라운 것은 없습니다.

A::Foo()
B::Foo()
A::Foo()

가상은 없습니다. 가상 키워드가 A와 B 클래스 모두에서 Foo의 앞에 추가되면 출력에 대해 다음과 같이 표시됩니다.

A::Foo()
B::Foo()
B::Foo()

모두가 기대하는 것입니다.

이제 누군가 가상 키워드를 추가하는 것을 잊었 기 때문에 버그가 있다고 언급하셨습니다. 따라서이 코드를 고려하십시오 (가상 키워드가 A에 추가되지만 B 클래스에는 추가되지 않음). 그러면 출력은 무엇입니까?

#include <stdio.h>

class A
{
public:
    virtual void Foo()
    {
        printf("A::Foo()\n");
    }
};

class B : public A
{
public:
    void Foo()
    {
        printf("B::Foo()\n");
    }
};

int main(int argc, char** argv)
{    
    A* a = new A();
    a->Foo();

    B* b = new B();
    b->Foo();

    A* a2 = new B();
    a2->Foo();

    return 0;
}

답 : 가상 키워드가 B에 추가되는 것과 동일합니까? 그 이유는 B :: Foo의 서명이 A :: Foo ()와 정확히 일치하고 A의 Foo가 가상이므로 B도 마찬가지이기 때문입니다.

이제 B의 Foo는 가상이고 A는 가상이 아닌 경우를 고려하십시오. 그러면 출력은 무엇입니까? 이 경우 출력은 다음과 같습니다.

A::Foo()
B::Foo()
A::Foo()

가상 키워드는 계층 구조에서 위쪽이 아니라 아래쪽으로 작동합니다. 기본 클래스 메서드를 가상으로 만들지 않습니다. 계층 구조에서 처음으로 가상 메서드를 만나는 것은 다형성이 시작될 때입니다. 이후 클래스가 이전 클래스에 가상 메서드를 갖도록하는 방법은 없습니다.

가상 메서드는이 클래스가 향후 클래스에 일부 동작을 재정의 / 변경할 수있는 기능을 제공한다는 것을 의미합니다.

따라서 가상 키워드를 제거하는 규칙이있는 경우 의도 한 효과가 없을 수 있습니다.

C ++의 가상 키워드는 강력한 개념입니다. 팀의 각 구성원이이 개념을 실제로 알고 있는지 확인하여 설계된대로 사용할 수 있도록해야합니다.


안녕하세요 Tommy, 튜토리얼 주셔서 감사합니다. 우리가 가진 버그는 기본 클래스의 메서드에서 "가상"키워드가 누락 되었기 때문입니다. BTW, 모든 기능을 가상 (반대 아님)으로 만들고 , 필요하지 않은 경우 "가상"키워드를 제거합니다.
MiniQuark

@MiniQuark : Tommy Hui는 모든 기능을 가상으로 만들면 프로그래머가 파생 된 클래스에서 키워드를 제거하고 효과가 없다는 사실을 깨닫지 못할 수 있다고 말합니다. 가상 키워드 제거가 항상 기본 클래스에서 발생하도록하는 방법이 필요합니다.
M. Dudley

1

플랫폼에 따라 가상 호출의 오버 헤드가 매우 바람직하지 않을 수 있습니다. 모든 함수를 가상으로 선언하면 본질적으로 함수 포인터를 통해 모두 호출합니다. 적어도 이것은 추가 역 참조이지만 일부 PPC 플랫폼에서는이를 수행하기 위해 마이크로 코딩되거나 느린 명령을 사용합니다.

이런 이유로 귀하의 제안에 반대하는 것이 좋지만 버그를 방지하는 데 도움이된다면 그만한 가치가 있습니다. 나는 도울 수 없지만 찾을 가치가있는 중간 지점이 있어야한다고 생각한다.


-1

가상 메서드를 호출하려면 몇 가지 추가 asm 명령이 필요합니다.

그러나 fun (int a, int b)에는 fun ()에 비해 몇 가지 추가 '푸시'명령이 있다고 걱정하지 않습니다. 따라서 특별한 상황에 처해 실제로 문제가 발생하는 것을 확인할 때까지 가상 장치에 대해서도 걱정하지 마십시오.

추신 : 가상 방법이있는 경우 가상 소멸자가 있는지 확인하십시오. 이렇게하면 가능한 문제를 피할 수 있습니다.


'xtofl'및 'Tom'댓글에 대한 응답. 세 가지 기능으로 작은 테스트를 수행했습니다.

  1. 가상
  2. 표준
  3. 3 개의 정수 매개 변수가있는 일반

내 테스트는 간단한 반복이었습니다.

for(int it = 0; it < 100000000; it ++) {
    test.Method();
}

결과는 다음과 같습니다.

  1. 3,913 초
  2. 3,873 초
  3. 3,970 초

디버그 모드에서 VC ++에 의해 컴파일되었습니다. 메서드 당 5 번의 테스트 만 수행하고 평균값을 계산했습니다 (결과가 매우 정확하지 않을 수 있음) ... 어쨌든 값은 1 억 호출을 가정 할 때 거의 동일합니다. 그리고 세 번의 추가 푸시 / 팝 방식은 더 느 렸습니다.

요점은 푸시 / 팝에 대한 비유가 마음에 들지 않으면 코드에서 추가 if / else를 생각해보십시오. if / else를 추가 할 때 CPU 파이프 라인에 대해 생각하십니까 ;-) 또한 코드가 실행될 CPU를 알 수 없습니다 ... 보통 컴파일러는 한 CPU에 대해 더 최적화 된 코드를 생성하고 다른 CPU에 대해서는 덜 최적화 된 코드를 생성 할 수 있습니다 ( Intel C ++ 컴파일러 )


2
추가 asm은 페이지 오류를 유발할 수 있습니다 (비가 상 기능에는 없을 것입니다)-문제를 지나치게 단순화했다고 생각합니다.
xtofl

2
xtofl의 댓글에 +1. 가상 함수는 파이프 라인 "버블"을 도입하고 캐싱 동작에 영향을 미치는 간접적 기능을 도입합니다.

1
디버그 모드에서 타이밍을 지정하는 것은 의미가 없습니다. MSVC는 디버그 모드에서 매우 느린 코드를 만들고 루프 오버 헤드는 대부분의 차이를 숨길 수 있습니다. 고성능을 목표로 한다면 빠른 경로에서 if / else 브랜치를 최소화하는 것을 고려해야 합니다. 저수준 x86 성능 최적화에 대한 자세한 내용은 agner.org/optimize 를 참조하십시오 . (또한 x86 태그 위키
Peter Cordes

1
@Tom : 여기서 핵심은 비가 상 함수는 인라인 할 수 있지만 가상은 할 수 없다는 것입니다 (예를 들어 final재정의에서 사용 하고 기본 유형이 아닌 파생 유형에 대한 포인터가있는 경우 컴파일러가 가상화를 해제 할 수없는 경우) ). 이 테스트는 매번 동일한 가상 기능을 호출했기 때문에 완벽하게 예측했습니다. 제한된 call처리량을 제외하고는 파이프 라인 거품이 없습니다 . 그리고 그 간접적 인 call것은 몇 가지 더 많은 uop 일 수 있습니다. 분기 예측은 특히 항상 동일한 대상에있는 경우 간접 분기에서도 잘 작동합니다.
Peter Cordes

이것은 마이크로 벤치 마크의 일반적인 함정에 속합니다. 분기 예측자가 뜨겁고 다른 일이 일어나지 않을 때 빠르게 보입니다. 잘못된 예측 오버 헤드는 call직접보다 간접적으로 더 높습니다 call. (예, 일반 call명령어도 예측이 필요합니다. 페치 단계는이 블록이 디코딩되기 전에 페치 할 다음 주소를 알아야하므로 명령 주소가 아닌 현재 블록 주소를 기반으로 다음 페치 블록을 예측해야합니다. 이 블록에서 분기 명령이있는 위치를 예측합니다 ...)
Peter Cordes
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.