나는 이미 이미 훌륭한 답변 중 하나로 뛰어 들어가서 다형성 코드를 변경 switches
하거나 if/else
반향을 측정 하는 반 패턴으로 이득을 측정 하는 반 패턴으로 실제로 역행하는 추한 접근 방식을 취하고 있음을 인정했습니다 . 그러나 나는 가장 중요한 길을 위해서만이 도매를하지 않았습니다. 흑백 일 필요는 없습니다.
면책 조항으로서, 정확성이 달성하기가 어렵지 않고 (종종 퍼지 및 근사화되는) 레이트 레이싱과 같은 영역에서 작업하지만 속도는 종종 가장 경쟁력있는 품질 중 하나입니다. 렌더링 시간 단축은 종종 가장 일반적인 사용자 요청 중 하나이며, 우리는 끊임없이 머리를 긁고 가장 중요한 측정 경로를 달성하는 방법을 알아냅니다.
조건부 다형성 리팩토링
첫째, 조건부 분기 ( switch
또는 여러 가지 if/else
문) 보다 유지 관리 측면에서 다형성이 더 좋은 이유를 이해할 가치가 있습니다 . 여기서 주요 이점은 확장 성 입니다.
다형성 코드를 사용하면 코드베이스에 새로운 하위 유형을 도입하고 일부 다형성 데이터 구조에 해당 인스턴스를 추가 할 수 있으며 기존의 모든 다형성 코드가 더 이상 수정없이 자동으로 작동하도록 할 수 있습니다. "이 유형이 'foo'이면 그 형식을 수행하십시오 " 와 유사한 대형 코드베이스에 여러 개의 코드가 흩어져 있다면 50 개의 서로 다른 코드 섹션을 업데이트해야하는 끔찍한 부담이 생길 수 있습니다. 새로운 유형의 일이지만 여전히 몇 가지가 빠져 있습니다.
이러한 유형 검사를 수행해야하는 코드베이스의 한두 섹션 만 있으면 다형성의 유지 관리 이점이 자연스럽게 줄어 듭니다.
최적화 장벽
나는 이것을 분기와 파이프 라이닝의 관점에서 많이 보지 말고 최적화 장벽의 컴파일러 디자인 사고 방식에서 더 많이 살펴볼 것을 제안합니다. 하위 유형을 기반으로 데이터를 정렬하는 것과 같이 두 시퀀스 모두에 적용되는 분기 예측을 향상시키는 방법이 있습니다 (시퀀스에 맞는 경우).
이 두 전략의 차이점은 옵티마이 저가 미리 가지고있는 정보의 양입니다. 알려진 함수 호출은 훨씬 더 많은 정보를 제공하며 컴파일 타임에 알 수없는 함수를 호출하는 간접 함수 호출은 최적화 장벽을 초래합니다.
호출되는 함수가 알려지면 컴파일러는 구조를 없애고 스패 터리스로 스쿼시하여 호출을 인라인하고 잠재적 인 앨리어싱 오버 헤드를 제거하며 명령 / 레지스터 할당에서 더 나은 작업을 수행 할 수 있습니다. 적절한 경우 switch
코딩 된 소형 LUT (GCC 5.3은 최근 점프 테이블이 아니라 결과에 하드 코딩 된 LUT 데이터를 사용하여 설명에 놀랐다 ).
간접 함수 호출의 경우와 같이 컴파일 타임을 알 수없는 믹스를 믹스에 도입하기 시작하면 조건부 분기가 우위를 점할 가능성이있는 일부 이점이 사라집니다.
메모리 최적화
타이트한 루프로 일련의 생물을 반복적으로 처리하는 비디오 게임의 예를 들어 보자. 이러한 경우 다음과 같은 다형성 컨테이너가있을 수 있습니다.
vector<Creature*> creatures;
참고 : 단순화를 위해 unique_ptr
여기서 피 했습니다.
... 여기서 Creature
다형성 기본 유형이 있습니다. 이 경우, 다형성 컨테이너의 어려움 중 하나는 종종 각 하위 유형에 대해 개별 / 개별적으로 메모리를 할당하려고한다는 것입니다 (예 : operator new
각 개별 생물에 대한 기본 던지기 사용 ).
그것은 종종 브랜칭이 아닌 메모리 기반의 최적화 (필요한 경우) 우선 순위를 결정합니다. 여기서 한 가지 전략은 각 하위 유형에 고정 할당자를 사용하여 할당 된 각 하위 유형에 대해 메모리를 풀링하고 큰 청크를 할당하여 연속적인 표현을 장려하는 것입니다. 이러한 전략을 사용하면 creatures
분기 예측을 향상시킬뿐만 아니라 참조 하위 위치를 개선하여 동일한 하위 유형의 여러 생물체에 액세스 할 수 있기 때문에이 컨테이너를 하위 유형 (및 주소)별로 정렬하는 것이 확실히 도움이 될 수 있습니다 제거하기 전에 단일 캐시 라인에서).
데이터 구조와 루프의 부분 가상화
이 모든 동작을 겪었지만 여전히 더 빠른 속도를 원한다고 가정 해 봅시다. 여기서 우리가 벤처하는 각 단계가 유지 관리 성을 저하시키고 있으며, 이미 성능 수익이 감소하는 다소 금속 연마 단계에있을 것입니다. 따라서이 영역으로 넘어 가면 성능에 대한 요구가 상당히 높아야합니다.이 영역에서 더 작고 작은 성능 향상을 위해 유지 관리 성을 더욱 희생하고자합니다.
그러나 다음 시도는 (그리고 전혀 도움이되지 않으면 변경 사항을 기꺼이 기꺼이 포기하려는) 수동 육성 일 수 있습니다.
버전 관리 팁 : 나보다 훨씬 최적화에 정통하지 않다면, 최적화 노력이 빠질 경우이 지점에서 새 브랜치를 만들면 가치가 있습니다. 나에게 그것은 프로파일 러가 있더라도 이러한 종류의 포인트 이후의 모든 시행 착오입니다.
그럼에도 불구하고 우리는이 사고 방식을 적용 할 필요가 없습니다. 우리의 예를 계속하면서,이 비디오 게임은 대부분 인간으로 이루어져 있다고 가정 해 봅시다. 이 경우 인간 생물을 끌어 올리고 그것들을위한 별도의 데이터 구조를 만들어 인간 생물을 가상화 할 수 있습니다.
vector<Human> humans; // common case
vector<Creature*> other_creatures; // additional rare-case creatures
이것은 생물체를 처리해야하는 코드베이스의 모든 영역이 인간 생물체에 대해 별도의 특수 사례 루프가 필요하다는 것을 의미합니다. 그러나 이는 가장 일반적인 생물 유형 인 인간에 대한 동적 디스패치 오버 헤드 (또는 아마도 더 적절하게는 최적화 장벽)를 제거합니다. 이 영역의 수가 많고 여유가 있다면 다음과 같이 할 수 있습니다.
vector<Human> humans; // common case
vector<Creature*> other_creatures; // additional rare-case creatures
vector<Creature*> creatures; // contains humans and other creatures
... 우리가 이것을 감당할 수 있다면 덜 중요한 경로는 그대로 유지되고 모든 생물체 유형을 추상적으로 처리 할 수 있습니다. 중요한 경로는 humans
하나의 루프와 other_creatures
두 번째 루프에서 처리 할 수 있습니다 .
우리는 필요에 따라이 전략을 확장하고 잠재적으로 이런 방식으로 이익을 줄 수 있지만 프로세스에서 유지 관리 성을 얼마나 저하시키고 있는지 주목할 가치가 있습니다. 여기서 함수 템플릿을 사용하면 로직을 수동으로 복제하지 않고도 인간과 생물 모두에 대한 코드를 생성 할 수 있습니다.
클래스의 부분 탈 가상화
몇 년 전에 내가 실제로 해왔 던 일이 더 이상 유익하지 않다고 확신하지는 않지만 (C ++ 03 시대에) 클래스의 부분적인 가상화였습니다. 이 경우, 우리는 이미 다른 목적을 위해 (가상이 아닌 기본 클래스의 접근자를 통해 액세스 한) 각 인스턴스와 함께 클래스 ID를 저장하고있었습니다. 거기에서 우리는 이것과 비슷한 것을했습니다 (내 기억은 약간 흐릿합니다).
switch (obj->type())
{
case id_common_type:
static_cast<CommonType*>(obj)->non_virtual_do_something();
break;
...
default:
obj->virtual_do_something();
break;
}
... virtual_do_something
서브 클래스에서 비가 상 버전을 호출하도록 구현되었습니다. 나는 함수 호출을 구체화하기 위해 명시 적 정적 다운 캐스트를 수행하는 것이 중요합니다. 나는 몇 년 동안 이런 종류의 것을 시도하지 않았으므로 이것이 지금 얼마나 유익한 지 전혀 모른다. 데이터 지향 디자인에 노출되면서 데이터 구조와 루프를 핫 / 콜드 방식으로 분할하는 위의 전략이 훨씬 더 유용하여 최적화 전략을위한 더 많은 문을 열었습니다.
도매 가상화
나는 지금까지 최적화 사고 방식을 적용하지 않았다는 것을 인정해야하므로 이점에 대해 전혀 모른다. 하나의 중앙 조건 조건 집합 (예 : 하나의 중앙 장소 처리 이벤트로 이벤트 처리) 만 있음을 알았지 만 다형 적 사고 방식으로 시작하여 모든 방법으로 최적화되지 않은 경우 예측 기능에서 간접 기능을 피했습니다. 여기까지
이론적으로, 여기서 즉각적인 이점은 이러한 최적화 장벽을 완전히 없애는 것 외에도 가상 포인터보다 유형을 식별하는 잠재적으로 더 작은 방법 일 수 있습니다 (예 : 256 개의 고유 한 유형 이하라는 아이디어에 전념 할 수있는 경우 단일 바이트). .
switch
하위 유형을 기반으로 데이터 구조와 루프를 분할하지 않고 하나의 중앙 명령문을 사용 하거나 순서가있는 경우 유지 관리하기 쉬운 코드를 작성하는 데 도움이 될 수 있습니다 (위의 최적화 된 수동 가상화 예제와 비교). 사물을 정확한 순서로 처리해야하는 경우 (종속으로 분기하는 경우에도)-종속성. 이 작업을 수행해야하는 장소가 너무 많지 않은 경우에 해당합니다 switch
.
일반적으로 유지 관리가 쉽지 않으면 성능이 매우 중요한 사고 방식으로도 권장하지 않습니다. "쉬운 유지"는 두 가지 주요 요소에 달려 있습니다.
- 실제 확장 성이 필요하지 않은 경우 (예 : 처리 할 항목이 정확히 8 가지이며 더 이상 은 없는지 확인)
- 코드에 이러한 유형을 확인해야하는 장소가 많지 않습니다 (예 : 하나의 중앙 장소).
...하지만 대부분의 경우 위의 시나리오를 권장하고 필요에 따라 부분 가상화로 더 효율적인 솔루션을 반복합니다. 확장 성과 유지 관리 필요성을 성능과 균형을 맞출 수있는 더 많은 호흡 공간을 제공합니다.
가상 함수와 함수 포인터
이것을 끝내기 위해 가상 함수와 함수 포인터에 대한 토론이 있음을 알았습니다. 가상 함수는 호출하는 데 약간의 추가 작업이 필요하지만 이것이 느리다는 것을 의미하지는 않습니다. 직관적으로, 심지어 더 빨라질 수도 있습니다.
여기서는 훨씬 직관적 인 메모리 계층 구조의 역학에주의를 기울이지 않고 명령어 측면에서 비용을 측정하는 데 익숙하기 때문에 반 직관적입니다.
우리는을 비교하는 경우 class
대 20 개 가상 기능 struct
하는 매장 20 함수 포인터, 모두가 각각의 메모리 오버 헤드를 여러 번 인스턴스화 class
메모리 동안,이 경우에는 64 비트 시스템에 대한 가상 포인터 8 바이트 인스턴스 의 오버 헤드 struct
는 160 바이트입니다.
실제 비용은 가상 함수를 사용하는 클래스와 함수 포인터 테이블을 사용하여 훨씬 더 강제적이고 강제적이지 않은 캐시 미스가있을 수 있습니다 (그리고 충분히 큰 입력 스케일에서 페이지 오류가 발생할 수 있음). 이 비용은 가상 테이블 인덱싱의 약간의 추가 작업을 방해하는 경향이 있습니다.
또한 structs
함수 포인터로 채워지고 여러 번 인스턴스화 된 레거시 C 코드베이스 (나보다 오래된)를 처리했으며 실제로 가상 함수가있는 클래스로 변환하여 실제로 성능을 크게 향상 (100 % 이상 향상)했습니다. 메모리 사용량의 대폭 감소, 캐시 친 화성 증가 등으로 인해
반대로, 사과와 사과를 비교할 때 C ++ 가상 함수 마인드에서 C 스타일 함수 포인터 마인드로 변환하는 반대 마인드가 다음 유형의 시나리오에서 유용하다는 것을 알았습니다.
class Functionoid
{
public:
virtual ~Functionoid() {}
virtual void operator()() = 0;
};
... 클래스가 무시할 수있는 단일 함수를 저장하는 곳 (또는 가상 소멸자를 세면 두 개). 이 경우, 중요한 경로에서이를 다음과 같이 전환하는 데 도움이 될 수 있습니다.
void (*func_ptr)(void* instance_data);
... 유형 안전 인터페이스 뒤에 배치하여 위험한 캐스트를 숨길 수 있습니다 void*
.
단일 가상 함수가있는 클래스를 사용하려는 경우 함수 포인터를 대신 사용하는 것이 신속하게 도움이 될 수 있습니다. 큰 이유는 함수 포인터를 호출 할 때 비용이 반드시 절감되는 것은 아닙니다. 더 이상 힙의 흩어진 영역에 각각의 개별 기능을 할당하려는 유혹에 직면하지 않기 때문입니다. 이러한 종류의 접근 방식은 인스턴스 데이터가 예를 들어 동종이고 동작 만 변하는 경우 힙 관련 및 메모리 조각화 오버 헤드를 피하는 것이 더 쉬울 수 있습니다.
따라서 함수 포인터를 사용하면 도움이 될 수있는 경우가 있지만 클래스 포인터 당 하나의 포인터 만 저장 해야하는 단일 vtable과 함수 포인터 테이블을 비교하는 경우 종종 다른 방법을 찾았습니다. . 이 vtable은 종종 하나 이상의 L1 캐시 라인과 타이트한 루프에 있습니다.
결론
어쨌든, 그것은이 주제에 대한 나의 작은 회전입니다. 이 부분을주의해서 환기시키는 것이 좋습니다. 본능이 아닌 신뢰 측정 및 이러한 최적화로 인해 유지 관리 성이 저하되는 방식을 고려할 때, 가능한 한 멀리 만 이동하십시오 (유지 한 경로는 유지 관리 측면에서 오류가 발생 함).