가상 함수 및 성능-C ++


125

수업 디자인에서는 추상 클래스와 가상 함수를 광범위하게 사용합니다. 가상 기능이 성능에 영향을 미친다는 느낌이 들었습니다. 이것이 사실입니까? 그러나이 성능 차이는 눈에 띄지 않으며 조기 최적화를 수행하는 것처럼 보입니다. 권리?


내 대답에 따라, 이것을 stackoverflow.com/questions/113830의
Suma


2
고성능 컴퓨팅 및 숫자 크 런칭을 수행하는 경우 계산 핵심에 가상을 사용하지 마십시오. 모든 성능을 확실히 제거하고 컴파일 타임에 최적화를 방지합니다. 프로그램의 초기화 또는 마무리에는 중요하지 않습니다. 인터페이스로 작업 할 때 원하는대로 가상을 사용할 수 있습니다.
Vincent

답변:


90

좋은 경험 법칙은 다음과 같습니다.

증명할 수있을 때까지 성능 문제가 아닙니다.

가상 함수를 사용하면 성능에 약간의 영향을 주지만 응용 프로그램의 전체 성능에는 영향을 미치지 않습니다. 성능 향상을 찾는 더 좋은 곳은 알고리즘과 I / O입니다.

가상 함수 (및 기타)에 대한 훌륭한 기사는 멤버 함수 포인터와 가장 빠른 C ++ 대리자 입니다.


순수한 가상 함수는 어떻습니까? 어떤 식 으로든 성능에 영향을 미칩니 까? 그들이 단순히 구현을 시행하기 위해 존재하는 것처럼 보일뿐입니다.
thomthom

2
@thomthom : 맞습니다. 순수한 가상 함수와 일반 가상 함수 사이에는 성능 차이가 없습니다.
Greg Hewgill

168

귀하의 질문에 대한 궁금증이 생겨서 3GHz 순차 PowerPC CPU에서 몇 가지 타이밍을 진행했습니다. 내가 실행 한 테스트는 get / set 함수를 사용하여 간단한 4d 벡터 클래스를 만드는 것이 었습니다.

class TestVec 
{
    float x,y,z,w; 
public:
    float GetX() { return x; }
    float SetX(float to) { return x=to; }  // and so on for the other three 
}

그런 다음 각각이 1024 개의 벡터 (L1에 맞도록 충분히 작음)를 포함하는 3 개의 배열을 설정하고 루프를 실행하여 서로 추가 (Ax = Bx + Cx) 한 횟수를 1000 번 반복했습니다. 나는 정의 기능이 실행 inline, virtual및 일반 함수 호출. 결과는 다음과 같습니다.

  • 인라인 : 8ms (통화 당 0.65ns)
  • 직접 : 68ms (통화 당 5.53ns)
  • 가상 : 160ms (통화 당 13ns)

따라서이 경우 (모든 것이 캐시에 맞는) 가상 함수 호출은 인라인 호출보다 약 20 배 느립니다. 그러나 이것이 실제로 무엇을 의미합니까? 루프를 통과 할 때마다 정확히 3 * 4 * 1024 = 12,288함수 호출 (1024 벡터 x 4 개의 구성 요소 x 추가 당 3 개의 호출)이 발생하므로이 시간은 1000 * 12,288 = 12,288,000함수 호출을 나타냅니다 . 가상 루프는 직접 루프보다 92ms 더 오래 걸렸으므로 호출 당 추가 오버 헤드 는 기능 당 7 나노초 였습니다 .

이로부터 내가 결론 : , 가상 함수가 훨씬 느린 직접 기능 이상 및 없습니다 초당 그들에게 천만 번 호출에 당신을 제외하고있는 거 계획, 그것은 중요하지 않습니다.

생성 된 어셈블리 비교를 참조하십시오 .


그러나 여러 번 전화하면 한 번만 전화하는 것보다 저렴할 수 있습니다. 내 관련없는 블로그 인 phresnel.org/blog를 참조하십시오. "가상 기능으로 간주되지 않는 가상 기능"이라는 제목의 게시물은 물론 코드 경로의 복잡성에 따라 다릅니다.
Sebastian Mach

22
내 테스트는 반복적으로 호출되는 작은 가상 함수 집합을 측정합니다. 귀하의 블로그 게시물은 계산 시간을 계산하여 코드 시간 비용을 측정 할 수 있다고 가정하지만 항상 그런 것은 아닙니다. 최신 프로세서에서 vfunc의 주요 비용은 지점의 잘못된 예측으로 인한 파이프 라인 버블입니다.
Crashworks

10
이것은 gcc LTO (Link Time Optimization)의 훌륭한 벤치 마크입니다. lto를 활성화 한 상태에서 이것을 다시 컴파일 해보십시오 : gcc.gnu.org/wiki/LinkTimeOptimization 그리고 20 배 팩터가 어떻게되는지 확인하십시오
lurscher

1
클래스에 하나의 가상 함수와 하나의 인라인 함수가 있으면 비가 상 메소드의 성능에도 영향을 미칩니 까? 단순히 수업의 성격 상 가상 인 것입니까?
thomthom

4
@thomthom 아니오, 가상 / 비가상은 기능별 속성입니다. 가상으로 표시되거나 가상으로 기본 클래스를 재정의하는 경우 vtable을 통해 함수를 정의하면됩니다. 공용 인터페이스를위한 가상 함수 그룹이있는 클래스와 많은 인라인 접근 자 등이있는 경우가 종종 있습니다. (기술적으로는 구현에 따라 다르며 컴파일러는 '인라인'으로 표시된 기능에 대해서도 가상 폰터를 사용할 수 있지만 그러한 컴파일러를 작성한 사람은 미쳤을 것입니다.)
Crashworks

42

Objective-C (모든 방법이 가상 인 경우)가 iPhone의 기본 언어이고 괴물 Java 가 Android의 기본 언어 인 경우 3GHz 듀얼 코어 타워에서 C ++ 가상 기능을 사용하는 것이 안전하다고 생각합니다.


4
iPhone이 성능 코드의 좋은 예인지 잘 모르겠습니다. youtube.com/watch?v=Pdk2cJpSXLg
Crashworks

13
@Crashworks : iPhone은 코드의 예가 아닙니다. 하드웨어의 예로, 특히 느린 하드웨어를 예로 들었습니다. 저명한 "느린"언어가 저전력 하드웨어에 충분하다면 가상 기능은 큰 문제가되지 않습니다.

52
iPhone은 ARM 프로세서에서 실행됩니다. iOS에 사용되는 ARM 프로세서는 저 MHz 및 저전력 사용을 위해 설계되었습니다. CPU에는 분기 예측을위한 실리콘이 없으므로 가상 함수 호출에서 분기 예측 누락으로 인한 성능 오버 헤드가 없습니다. 또한 iOS 용 MHz 하드웨어는 RAM에서 데이터를 검색하는 동안 캐시 미스가 300 클럭주기 동안 프로세서를 정지시키지 않을 정도로 충분히 낮습니다. 낮은 캐시에서는 캐시 미스가 덜 중요합니다. 요컨대, iOS 장치에서 가상 기능을 사용하는 데 따른 오버 헤드는 없지만 하드웨어 문제이며 데스크탑 CPU에는 적용되지 않습니다.
HaltingState

4
오랫동안 Java 프로그래머가 C ++에 새로 들어 왔기 때문에 Java의 JIT 컴파일러와 런타임 최적화 프로그램이 미리 정의 된 루프 수 후에 런타임에 일부 함수를 컴파일, 예측 및 인라인 할 수있는 기능을 추가하고 싶습니다. 그러나 C ++에 런타임 호출 패턴이 없기 때문에 컴파일 및 링크 타임에 이러한 기능이 있는지 확실하지 않습니다. 따라서 C ++에서는 약간 더 조심해야 할 수도 있습니다.
Alex Suo

@AlexSuo 당신의 요점이 확실하지 않습니까? 물론 C ++은 런타임에 발생할 수있는 일을 기반으로 최적화 할 수 없으므로 CPU 등으로 예측 등을 수행해야하지만 좋은 C ++ 컴파일러 (지시 된 경우)는 오래 전에 함수와 루프를 최적화하기 위해 많은 시간을 보냅니다. 실행 시간.
underscore_d

34

비디오 게임과 같은 성능이 중요한 응용 프로그램에서는 가상 함수 호출이 너무 느릴 수 있습니다. 최신 하드웨어에서 가장 큰 성능 문제는 캐시 미스입니다. 데이터가 캐시에 없으면 데이터를 사용하기 전에 수백 번의주기 일 수 있습니다.

일반 함수 호출은 CPU가 새 함수의 첫 번째 명령어를 가져오고 캐시에 없을 때 명령어 캐시 미스를 생성 할 수 있습니다.

가상 함수 호출은 먼저 객체에서 vtable 포인터를로드해야합니다. 이로 인해 데이터 캐시가 누락 될 수 있습니다. 그런 다음 vtable에서 함수 포인터를로드하여 다른 데이터 캐시 누락을 초래할 수 있습니다. 그런 다음 함수를 호출하여 비가 상 함수처럼 명령어 캐시 미스가 발생할 수 있습니다.

대부분의 경우 두 가지 추가 캐시 누락은 문제가되지 않지만 성능이 중요한 코드가 빡빡하면 성능이 크게 저하 될 수 있습니다.


6
맞습니다. 그러나 타이트 루프에서 반복적으로 호출되는 모든 코드 (또는 vtable)는 캐시 누락이 거의 발생하지 않습니다. 또한 vtable 포인터는 일반적으로 호출 된 메소드가 액세스 할 객체의 다른 데이터와 동일한 캐시 라인에 있으므로 종종 하나의 추가 캐시 미스에 대해서만 이야기합니다.
Qwertie 2016 년

5
@ Qwertie 나는 그것이 사실이라고 생각하지 않습니다. 루프의 본문 (L1 캐시보다 큰 경우)은 vtable 포인터, 함수 포인터 및 후속 반복이 "반복"될 때마다 모든 반복에서 L2 캐시 (또는 그 이상) 액세스를 기다려야합니다.
Ghita

30

Agner Fog의 "C ++에서 소프트웨어 최적화"매뉴얼의 44 페이지부터 :

함수 호출 명령문이 항상 동일한 버전의 가상 함수를 호출하는 경우 가상 멤버 함수를 호출하는 데 걸리는 시간은 비가 상 멤버 함수를 호출하는 것보다 몇 클럭주기 이상입니다. 버전이 변경되면 10-30 클럭 사이클의 오 판정 벌칙을 받게됩니다. 가상 함수 호출의 예측 및 오 판정 규칙은 switch 문과 동일합니다 ...


이 참조에 감사드립니다. Agner Fog의 최적화 매뉴얼은 하드웨어를 최적으로 활용하기위한 최고의 표준입니다.
Arto Bendiken

내 기억과 빠른 검색을 기반으로 - stackoverflow.com/questions/17061967/c-switch-and-jump-tables - 나는이 의심 항상 마찬가지 switch. 완전히 임의의 case값을 사용하십시오. 그러나 모든 case것이 연속적 이라면 컴파일러는 이것을 점프 테이블로 최적화 할 수 있습니다 (아, 좋은 오래된 Z80 일을 생각 나게합니다). 아니 내가 함께 vfuncs를 대체하기 위해 노력하는 것이 좋습니다 switch우스꽝이다. ;)
underscore_d

7

물론. 모든 메소드 호출이 vtable을 호출하기 전에 조회해야했기 때문에 컴퓨터가 100Mhz에서 실행될 때 문제가되었습니다. 그러나 오늘 .. 첫 번째 컴퓨터보다 많은 메모리를 가진 1 단계 캐시를 가진 3Ghz CPU에서? 전혀. 기본 RAM에서 메모리를 할당하면 모든 기능이 가상 인 경우보다 시간이 더 걸립니다.

사람들이 구조화 된 프로그래밍이 느리다고 말했던 옛날과 마찬가지로 모든 코드가 함수로 나뉘어져 있기 때문에 각 함수에는 스택 할당과 함수 호출이 필요했습니다!

가상 함수의 성능 영향을 고려하기 위해 귀찮게 생각할 수있는 유일한 시간은 템플릿 코드에서 매우 많이 사용되고 인스턴스화 된 경우입니다. 그럼에도 불구하고 나는 그것에 너무 많은 노력을 기울이지 않을 것입니다!

추신 : 다른 '사용하기 쉬운'언어를 생각하십시오. 모든 방법은 사실상 가상이며 요즘 크롤링하지 않습니다.


4
심지어 오늘날에도 함수 호출을 피하는 것은 성능이 높은 앱에 중요합니다. 차이점은 오늘날의 컴파일러는 작은 함수를 안정적으로 인라인하므로 작은 함수를 작성하는 데 속도가 저하되지 않는다는 것입니다. 가상 기능에 관해서는 스마트 CPU가 스마트 지점 예측을 수행 할 수 있습니다. 오래된 컴퓨터가 느리다는 사실은 실제로 문제가 아니라고 생각합니다. 예, 훨씬 느리지 만 우리는 알고 있었기 때문에 훨씬 작은 작업 부하를주었습니다. 1992 년에 MP3를 재생했다면 CPU의 절반 이상을 해당 작업에 전념해야한다는 것을 알았습니다.
Qwertie 2016 년

6
mp3는 1995 년부터 시작되었습니다. 92 년에 우리는 거의 386을 가지고 mp3를 재생할 수 없었으며, CPU 시간의 50 %는 우수한 멀티 태스킹 OS, 유휴 프로세스 및 선점 스케줄러를 가정합니다. 당시 소비자 시장에는이 중 어느 것도 존재하지 않았습니다. 전원이 켜진 순간부터 끝까지 100 %였습니다.
v.oddou

7

실행 시간 외에 다른 성능 기준이 있습니다. Vtable은 메모리 공간도 차지하며 경우에 따라 피할 수 있습니다. ATL은 템플릿 과 함께 컴파일 타임 " 시뮬레이트 된 동적 바인딩 "을 사용합니다.설명하기 어려운 "정적 다형성"의 효과를 얻는 것; 기본적으로 파생 클래스를 매개 변수로 기본 클래스 템플릿에 전달하므로 컴파일 타임에 기본 클래스는 각 클래스에 파생 클래스가 무엇인지 "인식"합니다. 여러 가지 파생 클래스를 기본 유형 (런타임 다형성)의 컬렉션에 저장할 수는 없지만 정적 의미에서 정적 클래스에서 기존 클래스 J와 동일한 클래스 Y와 동일한 클래스 Y를 만들려면 이런 종류의 재정의에 대한 후크, 당신은 당신이 관심있는 메소드를 재정의해야하며, vtable을 가질 필요없이 클래스 X의 기본 메소드를 얻습니다.

메모리 공간이 큰 클래스에서는 단일 vtable 포인터의 비용이 많지 않지만 COM의 일부 ATL 클래스는 매우 작으며 런타임 다형성 사례가 발생하지 않으면 vtable 절약 효과가 있습니다.

이 다른 질문 도 참조하십시오 .

그런데 여기 에 CPU 시간 성능 측면에 대해 이야기 하는 게시물이 있습니다.



4

그렇습니다. 맞습니다. 가상 함수 호출 비용에 대해 궁금하다면 이 게시물이 흥미로울 것입니다.


1
링크 된 기사는 가상 호출의 매우 중요한 부분을 고려하지 않으며 이는 분기 오해 가능성이 있습니다.
Suma

4

많은 가상 함수가 꽉 루프 내에서 호출되는 경우 오직 방법 나는 가상 함수가 성능 문제가 될 것이라고 볼 수이며, 및 경우에만 경우 가 발생할 페이지 오류 또는 다른 "무거운"메모리 동작이 발생할 수 있습니다.

다른 사람들이 말했듯이 그것은 실제 생활에서 당신에게 문제가되지 않을 것입니다. 그리고 생각한다면 프로파일 러를 실행하고 테스트를 수행 한 다음 성능상의 이점을 얻기 위해 코드를 "디자인 해제"하기 전에 이것이 실제로 문제가되는지 확인하십시오.


2
꽉 찬 루프에서 무언가를 호출하면 모든 코드와 데이터가 캐시에 뜨겁게 유지 될 수 있습니다 ...
Greg Rogers

2
예, 그러나 해당 루프가 객체 목록을 반복하는 경우 각 객체는 동일한 함수 호출을 통해 다른 주소에서 가상 함수를 호출 할 수 있습니다.
Daemin

3

클래스 메소드가 가상이 아닌 경우 컴파일러는 일반적으로 인라인을 수행합니다. 반대로, 가상 기능을 가진 일부 클래스에 대한 포인터를 사용하면 실제 주소는 런타임에만 알려집니다.

이것은 테스트, 시차 ~ 700 % (!)로 잘 설명되어 있습니다.

#include <time.h>

class Direct
{
public:
    int Perform(int &ia) { return ++ia; }
};

class AbstrBase
{
public:
    virtual int Perform(int &ia)=0;
};

class Derived: public AbstrBase
{
public:
    virtual int Perform(int &ia) { return ++ia; }
};


int main(int argc, char* argv[])
{
    Direct *pdir, dir;
    pdir = &dir;

    int ia=0;
    double start = clock();
    while( pdir->Perform(ia) );
    double end = clock();
    printf( "Direct %.3f, ia=%d\n", (end-start)/CLOCKS_PER_SEC, ia );

    Derived drv;
    AbstrBase *ab = &drv;

    ia=0;
    start = clock();
    while( ab->Perform(ia) );
    end = clock();
    printf( "Virtual: %.3f, ia=%d\n", (end-start)/CLOCKS_PER_SEC, ia );

    return 0;
}

가상 함수 호출의 영향은 상황에 따라 크게 다릅니다. 함수 내부에 호출이 적고 상당한 양의 작업이 있으면 무시할 수 있습니다.

또는 가상 통화가 여러 번 반복적으로 사용되는 경우 간단한 작업을 수행하는 경우 실제로 클 수 있습니다.


4
가상 함수 호출은에 비해 비쌉니다 ++ia. 그래서 무엇?
보 퍼슨

2

나는 내 특정 프로젝트에서 적어도 20 번 이리저리 갔다. 가 있지만 코드 재사용, 선명도, 유지 보수성, 가독성의 측면에서 몇 가지 큰 이익이 될, 다른 한편으로는, 성능 히트는 여전히 가상 기능이 존재합니다.

현대 노트북 / 데스크톱 / 태블릿에서 성능이 눈에 띄게 나타납니다. 그러나 임베디드 시스템이있는 경우, 특히 가상 함수가 반복해서 반복해서 호출되는 경우 성능 저하가 코드의 비 효율성의 원동력이 될 수 있습니다.

다음은 임베디드 시스템 컨텍스트에서 C / C ++에 대한 모범 사례를 분석하는 약간 오래된 문서입니다. http://www.open-std.org/jtc1/sc22/wg21/docs/ESC_Boston_01_304_paper.pdf

결론 : 프로그래머는 다른 구조체보다 특정 구문을 사용하는 장단점을 이해해야합니다. 슈퍼 퍼포먼스 중심이 아니라면 퍼포먼스 히트에 신경 쓰지 않을 것이며 코드를 가능한 한 유용하게 사용하기 위해 C ++의 모든 깔끔한 OO를 사용해야합니다.


2

내 경험상 가장 중요한 것은 함수를 인라인하는 기능입니다. 함수를 인라인해야하는 성능 / 최적화 요구가있는 경우이를 방지 할 수 있으므로 함수를 가상으로 만들 수 없습니다. 그렇지 않으면 아마도 차이를 느끼지 못할 것입니다.


1

주목해야 할 것은 다음과 같습니다.

boolean contains(A element) {
    for (A current: this)
        if (element.equals(current))
            return true;
    return false;
}

이보다 빠를 수 있습니다.

boolean contains(A element) {
    for (A current: this)
        if (current.equals(equals))
            return true;
    return false;
}

첫 번째 메소드는 하나의 함수 만 호출하고 두 번째 메소드는 많은 다른 함수를 호출하기 때문입니다. 이것은 모든 언어의 가상 기능에 적용됩니다.

컴파일러, 캐시 등에 의존하기 때문에 "may"라고 말합니다.


0

가상 기능 사용으로 인한 성능 저하로 인해 설계 수준에서 얻는 이점을 결코 능가 할 수는 없습니다. 가상 함수에 대한 호출은 정적 함수에 대한 직접 호출보다 25 % 덜 효율적일 것입니다. VMT 전체에 간접적 인 수준이 있기 때문입니다. 그러나 전화를 거는 데 걸리는 시간은 일반적으로 실제 기능을 실행하는 데 걸리는 시간과 비교하여 매우 작으므로 총 성능 비용은 무시할 수 있습니다. 특히 현재 하드웨어 성능과 관련하여. 또한 컴파일러는 때때로 가상 호출이 필요하지 않은 것을 최적화하고 확인하여 정적 호출로 컴파일 할 수 있습니다. 따라서 가상 함수와 추상 클래스를 필요한만큼 사용하는 것에 대해 걱정하지 마십시오.


2
대상 컴퓨터가 아무리 작아도 절대로?
zumalifeguard 2016 년

난 당신이 같은 것을 말로 표현했다 동의 한 수 The performance penalty of using virtual functions can sometimes be so insignificant that it is completely outweighed by the advantages you get at the design level.의 주요 차이점 말하고있다 sometimes, 없다 never.
underscore_d

-1

나는 특히 나 자신에게 의문을 제기했다. 특히 몇 년 전부터-나는 표준 멤버 메소드 호출의 타이밍과 가상 호출을 비교 한 테스트를 수행했으며 그 당시 결과에 대해 빈 화를 냈다. 비가 상보다 8 배 느립니다.

오늘 저는 성능이 매우 중요한 앱에서 버퍼 클래스에 더 많은 메모리를 할당하기 위해 가상 기능을 사용할지 여부를 결정해야했기 때문에 Google을 검색하고 발견했으며 결국 테스트를 다시 수행했습니다.

// g++ -std=c++0x -o perf perf.cpp -lrt
#include <typeinfo>    // typeid
#include <cstdio>      // printf
#include <cstdlib>     // atoll
#include <ctime>       // clock_gettime

struct Virtual { virtual int call() { return 42; } }; 
struct Inline { inline int call() { return 42; } }; 
struct Normal { int call(); };
int Normal::call() { return 42; }

template<typename T>
void test(unsigned long long count) {
    std::printf("Timing function calls of '%s' %llu times ...\n", typeid(T).name(), count);

    timespec t0, t1;
    clock_gettime(CLOCK_REALTIME, &t0);

    T test;
    while (count--) test.call();

    clock_gettime(CLOCK_REALTIME, &t1);
    t1.tv_sec -= t0.tv_sec;
    t1.tv_nsec = t1.tv_nsec > t0.tv_nsec
        ? t1.tv_nsec - t0.tv_nsec
        : 1000000000lu - t0.tv_nsec;

    std::printf(" -- result: %d sec %ld nsec\n", t1.tv_sec, t1.tv_nsec);
}

template<typename T, typename Ua, typename... Un>
void test(unsigned long long count) {
    test<T>(count);
    test<Ua, Un...>(count);
}

int main(int argc, const char* argv[]) {
    test<Inline, Normal, Virtual>(argc == 2 ? atoll(argv[1]) : 10000000000llu);
    return 0;
}

실제로 더 이상 중요하지 않다는 사실에 정말 놀랐습니다. 가상이 아닌 것보다 빠른 인라인을 갖는 것이 합리적이며 가상보다 빠를 수는 있지만 캐시에 필요한 데이터가 있는지 여부에 관계없이 전체 컴퓨터의 부하에 종종 영향을 미치며 최적화 할 수 있습니다. 캐시 수준에서는 응용 프로그램 개발자보다 컴파일러 개발자가 수행해야한다고 생각합니다.


12
컴파일러의 코드에서 가상 함수 호출은 Virtual :: call 만 호출 할 수 있다고 말할 수 있습니다. 이 경우 인라인 할 수 있습니다. 요청하지 않아도 컴파일러가 Normal :: call을 인라인하지 못하게하는 것은 없습니다. 그래서 컴파일러가 동일한 코드를 생성하기 때문에 3 개의 작업에 대해 동일한 시간을 얻을 수 있다고 생각합니다.
Bjarke H. Roune
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.