수업 디자인에서는 추상 클래스와 가상 함수를 광범위하게 사용합니다. 가상 기능이 성능에 영향을 미친다는 느낌이 들었습니다. 이것이 사실입니까? 그러나이 성능 차이는 눈에 띄지 않으며 조기 최적화를 수행하는 것처럼 보입니다. 권리?
수업 디자인에서는 추상 클래스와 가상 함수를 광범위하게 사용합니다. 가상 기능이 성능에 영향을 미친다는 느낌이 들었습니다. 이것이 사실입니까? 그러나이 성능 차이는 눈에 띄지 않으며 조기 최적화를 수행하는 것처럼 보입니다. 권리?
답변:
좋은 경험 법칙은 다음과 같습니다.
증명할 수있을 때까지 성능 문제가 아닙니다.
가상 함수를 사용하면 성능에 약간의 영향을 주지만 응용 프로그램의 전체 성능에는 영향을 미치지 않습니다. 성능 향상을 찾는 더 좋은 곳은 알고리즘과 I / O입니다.
가상 함수 (및 기타)에 대한 훌륭한 기사는 멤버 함수 포인터와 가장 빠른 C ++ 대리자 입니다.
귀하의 질문에 대한 궁금증이 생겨서 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
및 일반 함수 호출. 결과는 다음과 같습니다.
따라서이 경우 (모든 것이 캐시에 맞는) 가상 함수 호출은 인라인 호출보다 약 20 배 느립니다. 그러나 이것이 실제로 무엇을 의미합니까? 루프를 통과 할 때마다 정확히 3 * 4 * 1024 = 12,288
함수 호출 (1024 벡터 x 4 개의 구성 요소 x 추가 당 3 개의 호출)이 발생하므로이 시간은 1000 * 12,288 = 12,288,000
함수 호출을 나타냅니다 . 가상 루프는 직접 루프보다 92ms 더 오래 걸렸으므로 호출 당 추가 오버 헤드 는 기능 당 7 나노초 였습니다 .
이로부터 내가 결론 : 예 , 가상 함수가 훨씬 느린 직접 기능 이상 및 없습니다 더 초당 그들에게 천만 번 호출에 당신을 제외하고있는 거 계획, 그것은 중요하지 않습니다.
생성 된 어셈블리 비교를 참조하십시오 .
Objective-C (모든 방법이 가상 인 경우)가 iPhone의 기본 언어이고 괴물 Java 가 Android의 기본 언어 인 경우 3GHz 듀얼 코어 타워에서 C ++ 가상 기능을 사용하는 것이 안전하다고 생각합니다.
비디오 게임과 같은 성능이 중요한 응용 프로그램에서는 가상 함수 호출이 너무 느릴 수 있습니다. 최신 하드웨어에서 가장 큰 성능 문제는 캐시 미스입니다. 데이터가 캐시에 없으면 데이터를 사용하기 전에 수백 번의주기 일 수 있습니다.
일반 함수 호출은 CPU가 새 함수의 첫 번째 명령어를 가져오고 캐시에 없을 때 명령어 캐시 미스를 생성 할 수 있습니다.
가상 함수 호출은 먼저 객체에서 vtable 포인터를로드해야합니다. 이로 인해 데이터 캐시가 누락 될 수 있습니다. 그런 다음 vtable에서 함수 포인터를로드하여 다른 데이터 캐시 누락을 초래할 수 있습니다. 그런 다음 함수를 호출하여 비가 상 함수처럼 명령어 캐시 미스가 발생할 수 있습니다.
대부분의 경우 두 가지 추가 캐시 누락은 문제가되지 않지만 성능이 중요한 코드가 빡빡하면 성능이 크게 저하 될 수 있습니다.
Agner Fog의 "C ++에서 소프트웨어 최적화"매뉴얼의 44 페이지부터 :
함수 호출 명령문이 항상 동일한 버전의 가상 함수를 호출하는 경우 가상 멤버 함수를 호출하는 데 걸리는 시간은 비가 상 멤버 함수를 호출하는 것보다 몇 클럭주기 이상입니다. 버전이 변경되면 10-30 클럭 사이클의 오 판정 벌칙을 받게됩니다. 가상 함수 호출의 예측 및 오 판정 규칙은 switch 문과 동일합니다 ...
switch
. 완전히 임의의 case
값을 사용하십시오. 그러나 모든 case
것이 연속적 이라면 컴파일러는 이것을 점프 테이블로 최적화 할 수 있습니다 (아, 좋은 오래된 Z80 일을 생각 나게합니다). 아니 내가 함께 vfuncs를 대체하기 위해 노력하는 것이 좋습니다 switch
우스꽝이다. ;)
물론. 모든 메소드 호출이 vtable을 호출하기 전에 조회해야했기 때문에 컴퓨터가 100Mhz에서 실행될 때 문제가되었습니다. 그러나 오늘 .. 첫 번째 컴퓨터보다 많은 메모리를 가진 1 단계 캐시를 가진 3Ghz CPU에서? 전혀. 기본 RAM에서 메모리를 할당하면 모든 기능이 가상 인 경우보다 시간이 더 걸립니다.
사람들이 구조화 된 프로그래밍이 느리다고 말했던 옛날과 마찬가지로 모든 코드가 함수로 나뉘어져 있기 때문에 각 함수에는 스택 할당과 함수 호출이 필요했습니다!
가상 함수의 성능 영향을 고려하기 위해 귀찮게 생각할 수있는 유일한 시간은 템플릿 코드에서 매우 많이 사용되고 인스턴스화 된 경우입니다. 그럼에도 불구하고 나는 그것에 너무 많은 노력을 기울이지 않을 것입니다!
추신 : 다른 '사용하기 쉬운'언어를 생각하십시오. 모든 방법은 사실상 가상이며 요즘 크롤링하지 않습니다.
실행 시간 외에 다른 성능 기준이 있습니다. Vtable은 메모리 공간도 차지하며 경우에 따라 피할 수 있습니다. ATL은 템플릿 과 함께 컴파일 타임 " 시뮬레이트 된 동적 바인딩 "을 사용합니다.설명하기 어려운 "정적 다형성"의 효과를 얻는 것; 기본적으로 파생 클래스를 매개 변수로 기본 클래스 템플릿에 전달하므로 컴파일 타임에 기본 클래스는 각 클래스에 파생 클래스가 무엇인지 "인식"합니다. 여러 가지 파생 클래스를 기본 유형 (런타임 다형성)의 컬렉션에 저장할 수는 없지만 정적 의미에서 정적 클래스에서 기존 클래스 J와 동일한 클래스 Y와 동일한 클래스 Y를 만들려면 이런 종류의 재정의에 대한 후크, 당신은 당신이 관심있는 메소드를 재정의해야하며, vtable을 가질 필요없이 클래스 X의 기본 메소드를 얻습니다.
메모리 공간이 큰 클래스에서는 단일 vtable 포인터의 비용이 많지 않지만 COM의 일부 ATL 클래스는 매우 작으며 런타임 다형성 사례가 발생하지 않으면 vtable 절약 효과가 있습니다.
이 다른 질문 도 참조하십시오 .
그런데 여기 에 CPU 시간 성능 측면에 대해 이야기 하는 게시물이 있습니다.
많은 가상 함수가 꽉 루프 내에서 호출되는 경우 오직 방법 나는 가상 함수가 성능 문제가 될 것이라고 볼 수이며, 및 경우에만 경우 가 발생할 페이지 오류 또는 다른 "무거운"메모리 동작이 발생할 수 있습니다.
다른 사람들이 말했듯이 그것은 실제 생활에서 당신에게 문제가되지 않을 것입니다. 그리고 생각한다면 프로파일 러를 실행하고 테스트를 수행 한 다음 성능상의 이점을 얻기 위해 코드를 "디자인 해제"하기 전에 이것이 실제로 문제가되는지 확인하십시오.
클래스 메소드가 가상이 아닌 경우 컴파일러는 일반적으로 인라인을 수행합니다. 반대로, 가상 기능을 가진 일부 클래스에 대한 포인터를 사용하면 실제 주소는 런타임에만 알려집니다.
이것은 테스트, 시차 ~ 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;
}
가상 함수 호출의 영향은 상황에 따라 크게 다릅니다. 함수 내부에 호출이 적고 상당한 양의 작업이 있으면 무시할 수 있습니다.
또는 가상 통화가 여러 번 반복적으로 사용되는 경우 간단한 작업을 수행하는 경우 실제로 클 수 있습니다.
++ia
. 그래서 무엇?
나는 내 특정 프로젝트에서 적어도 20 번 이리저리 갔다. 가 있지만 수 코드 재사용, 선명도, 유지 보수성, 가독성의 측면에서 몇 가지 큰 이익이 될, 다른 한편으로는, 성능 히트는 여전히 할 가상 기능이 존재합니다.
현대 노트북 / 데스크톱 / 태블릿에서 성능이 눈에 띄게 나타납니다. 그러나 임베디드 시스템이있는 경우, 특히 가상 함수가 반복해서 반복해서 호출되는 경우 성능 저하가 코드의 비 효율성의 원동력이 될 수 있습니다.
다음은 임베디드 시스템 컨텍스트에서 C / C ++에 대한 모범 사례를 분석하는 약간 오래된 문서입니다. http://www.open-std.org/jtc1/sc22/wg21/docs/ESC_Boston_01_304_paper.pdf
결론 : 프로그래머는 다른 구조체보다 특정 구문을 사용하는 장단점을 이해해야합니다. 슈퍼 퍼포먼스 중심이 아니라면 퍼포먼스 히트에 신경 쓰지 않을 것이며 코드를 가능한 한 유용하게 사용하기 위해 C ++의 모든 깔끔한 OO를 사용해야합니다.
내 경험상 가장 중요한 것은 함수를 인라인하는 기능입니다. 함수를 인라인해야하는 성능 / 최적화 요구가있는 경우이를 방지 할 수 있으므로 함수를 가상으로 만들 수 없습니다. 그렇지 않으면 아마도 차이를 느끼지 못할 것입니다.
주목해야 할 것은 다음과 같습니다.
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"라고 말합니다.
가상 기능 사용으로 인한 성능 저하로 인해 설계 수준에서 얻는 이점을 결코 능가 할 수는 없습니다. 가상 함수에 대한 호출은 정적 함수에 대한 직접 호출보다 25 % 덜 효율적일 것입니다. VMT 전체에 간접적 인 수준이 있기 때문입니다. 그러나 전화를 거는 데 걸리는 시간은 일반적으로 실제 기능을 실행하는 데 걸리는 시간과 비교하여 매우 작으므로 총 성능 비용은 무시할 수 있습니다. 특히 현재 하드웨어 성능과 관련하여. 또한 컴파일러는 때때로 가상 호출이 필요하지 않은 것을 최적화하고 확인하여 정적 호출로 컴파일 할 수 있습니다. 따라서 가상 함수와 추상 클래스를 필요한만큼 사용하는 것에 대해 걱정하지 마십시오.
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
.
나는 특히 나 자신에게 의문을 제기했다. 특히 몇 년 전부터-나는 표준 멤버 메소드 호출의 타이밍과 가상 호출을 비교 한 테스트를 수행했으며 그 당시 결과에 대해 빈 화를 냈다. 비가 상보다 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;
}
실제로 더 이상 중요하지 않다는 사실에 정말 놀랐습니다. 가상이 아닌 것보다 빠른 인라인을 갖는 것이 합리적이며 가상보다 빠를 수는 있지만 캐시에 필요한 데이터가 있는지 여부에 관계없이 전체 컴퓨터의 부하에 종종 영향을 미치며 최적화 할 수 있습니다. 캐시 수준에서는 응용 프로그램 개발자보다 컴파일러 개발자가 수행해야한다고 생각합니다.