누구나 가상 테이블이 정확히 작동하는 방식과 가상 함수가 호출 될 때 어떤 포인터가 연관되는지 자세히 설명 할 수 있습니다.
실제로 느리다면 가상 함수를 실행하는 데 걸리는 시간이 일반 클래스 메서드보다 큼을 보여줄 수 있습니까? 일부 코드를 보지 않고 어떻게 / 무슨 일이 일어나고 있는지 쉽게 잃을 수 있습니다.
누구나 가상 테이블이 정확히 작동하는 방식과 가상 함수가 호출 될 때 어떤 포인터가 연관되는지 자세히 설명 할 수 있습니다.
실제로 느리다면 가상 함수를 실행하는 데 걸리는 시간이 일반 클래스 메서드보다 큼을 보여줄 수 있습니까? 일부 코드를 보지 않고 어떻게 / 무슨 일이 일어나고 있는지 쉽게 잃을 수 있습니다.
답변:
가상 메소드는 일반적으로 함수 포인터가 저장되는 소위 가상 메소드 테이블 (단순 vtable)을 통해 구현됩니다. 이것은 실제 호출에 대한 간접 성을 추가합니다 (vtable에서 호출 할 함수의 주소를 가져 와서 바로 호출하는 대신 호출하십시오). 물론 시간과 코드가 더 필요합니다.
그러나 반드시 속도 저하의 주요 원인은 아닙니다. 실제 문제는 컴파일러가 (일반적으로 / 보통) 어떤 함수가 호출 되는지 알 수 없다는 것입니다. 따라서 인라인하거나 다른 최적화를 수행 할 수 없습니다. 이것만으로도 12 개의 무의미한 명령어 (레지스터 준비, 호출, 이후 상태 복원)가 추가 될 수 있으며, 관련이없는 것처럼 보이는 다른 최적화를 방해 할 수 있습니다. 또한 많은 다른 구현을 호출하여 미친 것처럼 분기하는 경우 다른 방법을 통해 미친 것처럼 분기하는 것과 같은 타격을 입습니다. 캐시 및 분기 예측기가 도움이되지 않으면 분기가 완전히 예측 가능한 것보다 오래 걸립니다 분기.
크지 만 : 이러한 성능 적중은 보통 너무 작아서 중요하지 않습니다. 고성능 코드를 생성하고 놀라운 빈도로 호출되는 가상 기능을 추가하는 것을 고려할 가치가 있습니다. 그러나 또한 분기의 다른 수단을 가상 함수 호출을 대체하는 (것을 명심 if .. else
, switch
, 함수 포인터 등) 근본적인 문제가 해결되지 않습니다 - 그것은 아주 잘 느려질 수 있습니다. 문제는 (있는 경우) 가상 기능이 아니라 (필요하지 않은) 간접적 인 것입니다.
편집 : 통화 지침의 차이점은 다른 답변에 설명되어 있습니다. 기본적으로 정적 ( "정상") 호출 코드는 다음과 같습니다.
가상 호출은 함수 주소를 컴파일 타임에 알 수 없다는 점을 제외하고는 정확히 동일합니다. 대신 몇 가지 지침 ...
브랜치 (branch) : 브랜치는 다음 명령을 실행시키는 대신 다른 명령으로 점프하는 것입니다. 여기에는 if
, switch
때때로 다양한 루프, 함수 호출 등과 그렇지 않은 컴파일러 구현 물건의 일부가 실제로 후드 아래 지점을 필요로하는 방식으로 지점에 보인다. 정렬되지 않은 배열보다 정렬 된 배열을 처리하는 이유는 무엇입니까?를 참조하십시오 . 왜 이것이 느려질 수 있는지, CPU가이 둔화를 막기 위해 무엇을하는지, 그리고 이것이 전부가 아닌 방법을 위해.
다음은 각각 가상 함수 호출과 비가 상 호출에서 실제로 디스 어셈블 된 코드입니다.
mov -0x8(%rbp),%rax
mov (%rax),%rax
mov (%rax),%rax
callq *%rax
callq 0x4007aa
가상 통화에는 정확한 주소를 찾기 위해 세 개의 추가 명령어가 필요하지만 가상 통화가 아닌 주소는 컴파일 할 수 있습니다.
그러나 추가 조회 시간은 대부분 무시할 수있는 것으로 간주됩니다. 루프에서와 같이 조회 시간이 중요한 상황에서는 일반적으로 루프 전에 처음 세 명령을 수행하여 값을 캐시 할 수 있습니다.
조회 시간이 중요한 다른 상황은 객체 모음이 있고 각 객체에서 가상 함수를 호출하여 반복하는 경우입니다. 그러나이 경우 어쨌든 호출 할 함수를 선택하는 몇 가지 방법이 필요 하며 가상 테이블 조회는 다른 방법과 마찬가지로 좋은 방법입니다. 실제로 vtable 조회 코드가 널리 사용되기 때문에 최적화가 많이되어 있으므로 수동으로 해결하려고하면 성능이 저하 될 수 있습니다.
-0x8(%rbp)
. 오 마이 ... 그 AT & T 문법.
무엇보다 느리게 ?
가상 함수는 직접 함수 호출로 해결할 수없는 문제를 해결합니다. 일반적으로 동일한 것을 계산하는 두 프로그램 만 비교할 수 있습니다. "이 광선 추적기는 해당 컴파일러보다 빠릅니다"는 의미가 없으며이 원칙은 개별 함수 나 프로그래밍 언어 구문과 같은 작은 것까지 일반화합니다.
가상 객체를 사용하여 객체 유형과 같은 데이텀을 기반으로 코드 조각으로 동적으로 전환하지 않으면 switch
동일한 작업을 수행하기 위해 명령문 과 같은 다른 것을 사용해야합니다 . 다른 것에는 자체 오버 헤드가 있으며 유지 관리 및 글로벌 성능에 영향을 미치는 프로그램 구성에 영향을 미칩니다.
C ++에서 가상 함수 호출이 항상 동적 인 것은 아닙니다. 객체가 포인터 나 참조가 아니거나 그 유형이 정적으로 유추 될 수 있기 때문에 정확한 유형을 알고있는 객체에서 호출을 수행하면 일반 멤버 함수 호출입니다. 이는 오버 헤드가 발생하지 않을뿐만 아니라 이러한 호출이 일반 호출과 동일한 방식으로 인라인 될 수 있음을 의미합니다.
즉, 가상 함수에 가상 디스패치가 필요하지 않은 경우 C ++ 컴파일러가 작동 할 수 있으므로 일반적으로 비가 상 함수에 비해 성능에 대해 걱정할 필요가 없습니다.
새로운 : 또한, 우리는 공유 라이브러리를 잊어서는 안된다. 공유 라이브러리에있는 클래스를 사용하는 경우 일반 멤버 함수에 대한 호출은 단순히 좋은 명령 시퀀스가 아닙니다 callq 0x4007aa
. "프로그램 링크 테이블"또는 이와 같은 구조를 통한 간접 지정과 같은 몇 가지 과정을 거쳐야합니다. 따라서 공유 라이브러리 간접 지정은 (완전히 간접적 인) 가상 호출과 직접 호출 간의 비용 차이를 어느 정도 (완전하지는 않지만) 평준화 할 수 있습니다. 따라서 가상 함수 트레이드 오프에 대한 추론은 프로그램이 어떻게 빌드되는지를 고려해야합니다. 대상 객체의 클래스가 호출하는 프로그램에 모 놀리 식으로 연결되어 있는지 여부입니다.