이미 최선의 선택 알고리즘이 있다고 가정하면 C ++ 코드에서 달콤한 달콤한 프레임 속도의 마지막 몇 방울을 압착하기 위해 어떤 저수준 솔루션을 제공 할 수 있습니까?
이 팁은 프로파일 러에서 이미 강조 표시 한 중요한 코드 섹션에만 적용되지만 낮은 수준의 비 구조적 개선이어야합니다. 나는 예를 뿌렸다.
이미 최선의 선택 알고리즘이 있다고 가정하면 C ++ 코드에서 달콤한 달콤한 프레임 속도의 마지막 몇 방울을 압착하기 위해 어떤 저수준 솔루션을 제공 할 수 있습니까?
이 팁은 프로파일 러에서 이미 강조 표시 한 중요한 코드 섹션에만 적용되지만 낮은 수준의 비 구조적 개선이어야합니다. 나는 예를 뿌렸다.
답변:
데이터 레이아웃을 최적화하십시오! (이것은 C ++보다 더 많은 언어에 적용됩니다)
데이터, 프로세서, 멀티 코어 처리 등을 위해 특별히 조정되었습니다. 그러나 기본 개념은 다음과 같습니다.
타이트한 루프로 처리 할 때는 각 반복에 대한 데이터를 가능한 작게, 메모리에서 최대한 가깝게 만들고 싶습니다. 즉, 계산에 필요한 데이터 만 포함하는 객체 배열 (포인터가 아님)이 이상적입니다.
이런 식으로 CPU가 루프의 첫 번째 반복에 대한 데이터를 가져 오면 다음 몇 번의 반복 데이터가 캐시에로드됩니다.
실제로 CPU는 빠르며 컴파일러는 좋습니다. 더 적고 빠른 지침을 사용하여 실제로 할 수있는 일은 많지 않습니다. 캐시 일관성 은 현재 위치에 있습니다 (임의의 기사 인 Googled). 단순히 데이터를 선형으로 실행하지 않는 알고리즘에 대한 캐시 일관성을 얻는 좋은 예가 포함되어 있습니다.
매우 낮은 수준의 팁이지만 유용 할 수있는 팁 :
대부분의 컴파일러는 어떤 형태의 명시 적 조건 힌트를 지원합니다. GCC에는 __builtin_expect라는 함수가 있으며 결과 값이 무엇인지 컴파일러에 알릴 수 있습니다. GCC는이 데이터를 사용하여 예상되는 경우에 가능한 빨리 수행 할 수 있도록 조건부를 최적화하고 예상치 않은 경우에는 실행이 약간 느려질 수 있습니다.
if(__builtin_expect(entity->extremely_unlikely_flag, 0)) {
// code that is rarely run
}
이것을 올바르게 사용하면 속도가 10-20 % 빨라졌습니다.
가장 먼저 이해해야 할 것은 실행중인 하드웨어입니다. 분기를 어떻게 처리합니까? 캐싱은 어떻습니까? SIMD 명령어 세트가 있습니까? 얼마나 많은 프로세서를 사용할 수 있습니까? 프로세서 시간을 다른 것과 공유해야합니까?
동일한 문제를 매우 다른 방식으로 해결할 수도 있습니다. 선택한 알고리즘조차도 하드웨어에 따라 달라집니다. 경우에 따라 O (N)이 O (NlogN)보다 느리게 실행될 수 있습니다 (구현에 따라 다름).
최적화에 대한 조잡한 개요로서, 내가 할 첫 번째 일은 정확히 어떤 문제와 어떤 데이터를 해결하려고 하는지를 살펴 보는 것입니다. 그런 다음 최적화하십시오. 최고의 성능을 원한다면 일반적인 솔루션을 잊어 버리십시오. 가장 많이 사용되는 케이스와 맞지 않는 모든 것을 특별한 경우에 사용할 수 있습니다.
그런 다음 프로파일을 작성하십시오. 프로필, 프로필, 프로필. 메모리 사용량, 분기 페널티, 함수 호출 오버 헤드, 파이프 라인 활용을 살펴보십시오. 코드 속도를 늦추는 것을 해결하십시오. 아마도 데이터 액세스 (데이터 액세스의 오버 헤드에 대한 "The Latency Elephant"라는 기사를 썼습니다-Google. 충분한 "평판"이 없어서 여기에 2 개의 링크를 게시 할 수는 없습니다), 자세히 살펴보고 그런 다음 데이터 레이아웃 ( 멋지고 큰 평평한 균질 배열이 훌륭함 )과 데이터 액세스 (가능한 경우 프리 페치) 를 최적화하십시오 .
메모리 하위 시스템의 오버 헤드를 최소화 한 후에는 명령이 병목 현상인지 확인하고 알고리즘의 SIMD 구현을 살펴보십시오. SoA (Structure-of-Arrays) 구현은 데이터와 효율적인 명령 캐시. SIMD가 문제와 잘 맞지 않으면 내장 함수와 어셈블러 레벨 코딩이 필요할 수 있습니다.
여전히 더 많은 속도가 필요하다면 병렬로 진행하십시오. PS3에서 실행할 수있는 이점이 있다면 SPU가 친구입니다. 그들을 사용하고 사랑하십시오. 이미 SIMD 솔루션을 작성한 경우 SPU로 전환하면 큰 이점을 얻을 수 있습니다.
그런 다음 좀 더 프로파일하십시오. 게임 시나리오에서 테스트-이 코드는 여전히 병목 현상입니까? 사용을 최소화하기 위해이 코드가 더 높은 수준에서 사용되는 방식을 변경할 수 있습니까? 여러 프레임에서 계산을 연기 할 수 있습니까?
어떤 플랫폼을 사용하든 하드웨어 및 사용 가능한 프로파일 러에 대해 최대한 많이 배우십시오. 병목 현상이 무엇인지 알고 있다고 가정하지 마십시오. 프로파일 러에서 찾으십시오. 실제로 게임을 더 빠르게 진행했는지 확인하는 휴리스틱이 있는지 확인하십시오.
그런 다음 다시 프로파일하십시오.
첫 번째 단계 : 알고리즘과 관련하여 데이터에 대해 신중하게 생각하십시오. O (log n)이 항상 O (n)보다 빠른 것은 아닙니다. 간단한 예 : 몇 개의 키만있는 해시 테이블이 선형 검색으로 대체되는 것이 더 좋습니다.
두 번째 단계 : 생성 된 어셈블리를보십시오. C ++은 많은 암시 적 코드 생성을 테이블에 제공합니다. 때로는 모르는 사이에 몰래 빠져 나옵니다.
그러나 그것이 실제로 페달을 밟는 시간이라고 가정하면 : 프로필. 진심으로. "성능 트릭"을 무작위로 적용하면 도움이되는만큼 상처를 입을 수 있습니다.
그런 다음 병목 현상에 따라 모든 것이 달라집니다.
data cache misses => 데이터 레이아웃을 최적화하십시오. 여기 좋은 출발점이 있습니다 : http://gamesfromwithin.com/data-oriented-design
code cache misses => 가상 함수 호출, 과도한 호출 스택 깊이 등을 살펴보십시오. 성능 저하의 일반적인 원인은 기본 클래스 가 가상 이어야 한다는 잘못된 생각입니다 .
다른 일반적인 C ++ 성능 싱크 :
위의 모든 내용은 어셈블리를 볼 때 즉시 알 수 있으므로 위를 참조하십시오.;)
불필요한 가지 제거
일부 플랫폼 및 일부 컴파일러에서는 분기가 전체 파이프 라인을 버릴 수 있으므로 중요하지 않은 if () 블록조차도 비쌀 수 있습니다.
PowerPC 아키텍처 (PS3 / x360)는 부동 소수점 선택 명령어를 제공합니다 fsel
. 블록이 간단한 할당 인 경우 분기 대신 사용할 수 있습니다.
float result = 0;
if (foo > bar) { result = 2.0f; }
else { result = 1.0f; }
된다 :
float result = fsel(foo-bar, 2.0f, 1.0f);
첫 번째 매개 변수가 0보다 크거나 같으면 두 번째 매개 변수가 리턴되고 그렇지 않으면 세 번째 매개 변수가 리턴됩니다.
브랜치를 잃는 가격은 if {} 및 else {} 블록이 모두 실행되므로 값 비싼 작업이거나 NULL 포인터를 역 참조하는 경우이 최적화는 적합하지 않습니다.
때때로 컴파일러가 이미이 작업을 수행 했으므로 먼저 어셈블리를 확인하십시오.
분기 및 fsel에 대한 자세한 내용은 다음과 같습니다.
컴파일러 내장 함수를 사용하십시오.
컴파일러가 내장 함수를 사용하여 특정 작업에 대해 가장 효율적인 어셈블리를 생성하는지 확인하십시오. 컴파일러가 최적화 된 어셈블리로 바뀌는 함수 호출과 같은 구조입니다.
불필요한 가상 함수 호출 제거
가상 함수의 디스패치는 매우 느릴 수 있습니다. 이 기사에서는 그 이유를 잘 설명합니다. 가능하면 프레임 당 여러 번 호출되는 기능은 피하십시오.
몇 가지 방법으로이 작업을 수행 할 수 있습니다. 때로는 상속을 필요로하지 않기 위해 클래스를 다시 작성할 수 있습니다. MachineGun은 무기의 유일한 하위 클래스이며, 합병 할 수 있습니다.
템플릿을 사용하여 런타임 다형성을 컴파일 타임 다형성으로 바꿀 수 있습니다. 런타임에 객체의 하위 유형을 알고있는 경우에만 작동하며 주요 재 작성이 가능합니다.
내 기본 원칙은 : 필요없는 것은하지 마십시오 .
특정 함수가 병목 현상을 발견 한 경우 함수를 최적화하거나 처음부터 호출되지 않도록 할 수 있습니다.
이것이 반드시 잘못된 알고리즘을 사용하고 있다는 의미는 아닙니다. 예를 들어 잠깐 동안 (또는 완전히 사전 계산 된) 캐시 될 수있는 모든 프레임에서 계산을 실행하고 있음을 의미 할 수 있습니다.
나는 실제로 저수준 최적화를 시도하기 전에 항상이 접근법을 시도합니다.
아직 수행하지 않은 경우 SIMD (SSE 별)를 사용하십시오. Gamasutra는 이것에 관한 좋은 기사를 가지고 있습니다. 기사 마지막 부분에서 제시된 라이브러리에서 소스 코드를 다운로드 할 수 있습니다.
CPU 파이프 라인을 더 잘 사용하기 위해 종속성 체인을 최소화하십시오.
간단한 경우 루프 언 롤링을 활성화하면 컴파일러에서이를 수행 할 수 있습니다. 그러나 표현식을 재정렬하면 결과가 변경 될 때 특히 부동 소수점이있는 경우에는 그렇지 않습니다.
예:
float *data = ...;
int length = ...;
// Slow version
float total = 0.0f;
int i;
for (i=0; i < length; i++)
{
total += data[i]
}
// Fast version
float total1, total2, total3, total4;
for (i=0; i < length-3; i += 4)
{
total1 += data[i];
total2 += data[i+1];
total3 += data[i+2];
total4 += data[i+3];
}
for (; i < length; i++)
{
total += data[i]
}
total += (total1 + total2) + (total3 + total4);
컴파일러를 간과하지 마십시오. 인텔에서 gcc를 사용하는 경우 예를 들어 인텔 C / C ++ 컴파일러로 전환하여 성능을 쉽게 얻을 수 있습니다. ARM 플랫폼을 대상으로하는 경우 ARM 상용 컴파일러를 확인하십시오. iPhone을 사용하는 경우 Apple은 iOS 4.0 SDK부터 Clang을 사용하도록 허용했습니다.
최적화, 특히 x86에서 발생할 수있는 한 가지 문제는 최신 CPU 구현에서 많은 직관적 인 것들이 당신에게 불리하게 작용한다는 것입니다. 불행히도 대부분의 사람들에게 컴파일러를 최적화하는 기능은 오랫동안 사라졌습니다. 컴파일러는 자체 CPU 지식을 기반으로 스트림의 명령어를 예약 할 수 있습니다. 또한 CPU는 자체 요구에 따라 명령어를 다시 예약 할 수도 있습니다. 메소드를 배열하는 최적의 방법을 생각하더라도 컴파일러 또는 CPU가 이미 자체적으로 작성하여 해당 최적화를 수행했을 가능성이 있습니다.
저의 최선의 조언은 저수준 최적화를 무시하고 고수준 최적화에 집중하는 것입니다. 컴파일러와 CPU는 알고리즘이 아무리 좋더라도 O (n ^ 2)에서 O (1) 알고리즘으로 알고리즘을 변경할 수 없습니다. 그것은 당신이하려는 일을 정확하게보고 그것을하는 더 좋은 방법을 찾아야 할 것입니다. 컴파일러와 CPU가 낮은 수준에 대해 걱정하게하고 중간 수준에서 높은 수준에 중점을 둡니다.
제한 키워드는 특히 포인터와 객체를 조작해야하는 경우, 잠재적으로 편리합니다. 이를 통해 컴파일러는 뾰족한 객체가 다른 방식으로 수정되지 않는다고 가정하여 객체의 일부를 레지스터에 유지하거나 읽기 및 쓰기 순서를 다시 지정하는 등보다 적극적인 최적화를 수행 할 수 있습니다.
키워드에 대한 한 가지 좋은 점은 알고리즘을 재정렬하지 않고 한 번만 적용하면 이점을 볼 수있는 힌트라는 것입니다. 나쁜 점은 잘못된 장소에서 사용하면 데이터가 손상 될 수 있다는 것입니다. 그러나 일반적으로 그것을 사용하는 것이 합법적 인 곳을 찾는 것은 매우 쉽습니다. 이것은 프로그래머가 컴파일러가 안전하게 가정 할 수있는 것보다 더 많은 것을 알 것으로 기대할 수있는 몇 가지 예 중 하나입니다. 그래서 키워드가 소개되었습니다.
기술적으로 '제한'은 표준 C ++에 존재하지 않지만 대부분의 C ++ 컴파일러에서 플랫폼 별 기능을 사용할 수 있으므로 고려해 볼 가치가 있습니다.
참조 : http://cellperformance.beyond3d.com/articles/2006/05/demystifying-the-restrict-keyword.html
모든 것을 정하십시오!
컴파일러에 데이터에 대한 더 많은 정보를 제공할수록 (적어도 내 경험상) 최적화가 더 좋습니다.
void foo(Bar * x) {...;}
된다;
void foo(const Bar * const x) {...;}
컴파일러는 이제 포인터 x가 바뀌지 않을 것이고 그것을 가리키는 데이터도 바뀌지 않을 것임을 알고 있습니다.
또 다른 이점은 우발적 인 버그의 수를 줄여서 자신이나 다른 사람이하지 말아야 할 사항을 수정하지 못하게하는 것입니다.
const
컴파일러 최적화를 향상시키지 않습니다. 변수가 변경되지 않는다는 것을 알고 있지만 const
충분한 보증을 제공하지 않으면 컴파일러가 더 나은 코드를 생성 할 수 있습니다 .
성능을 얻는 가장 좋은 방법은 알고리즘을 변경하는 것입니다. 구현이 덜 일반적 일수록 금속에 더 가까이 갈 수 있습니다.
완료되었다고 가정합니다 ....
실제로 중요한 코드 인 경우 메모리 읽기를 피하고 미리 계산할 수있는 항목을 계산하지 마십시오 (규칙 번호 1을 위반하는 조회 테이블은 없지만). 알고리즘이하는 일을 알고 컴파일러가 알고있는 방식으로 작성하십시오. 어셈블리를 확인하십시오.
캐시 누락을 피하십시오. 가능한 한 일괄 처리하십시오. 가상 기능 및 기타 간접적 인 지시를 피하십시오.
궁극적으로 모든 것을 측정하십시오. 규칙은 항상 바뀝니다. 3 년 전에 코드 속도를 높이 던 것으로 인해 속도가 느려졌습니다. 좋은 예는 'float 버전 대신 이중 수학 함수 사용'입니다. 내가 읽지 않았다면 그 사실을 깨닫지 못했을 것입니다.
잊어 버렸습니다-기본 생성자가 변수를 초기화하지 않았거나 주장하면 적어도 그렇지 않은 생성자를 만듭니다. 프로필에 표시되지 않는 사항에 유의하십시오. 코드 줄당 하나의 불필요한 사이클을 잃으면 프로파일 러에 아무것도 표시되지 않지만 전체적으로 많은 사이클이 손실됩니다. 다시 한 번, 코드가 무엇을하고 있는지 알고 있습니다. 핵심 기능을 완전 함이 아니라 마른 상태로 만드십시오. 필요한 경우 Foolproof 버전을 호출 할 수 있지만 항상 필요한 것은 아닙니다. 다양성은 가격이 책정됩니다. 성능은 하나입니다.
기본 초기화가없는 이유를 설명하기 위해 편집되었습니다. 많은 코드가 다음과 같이 말합니다. Vector3 bla; bla = DoSomething ();
생성자의 초기화는 시간 낭비입니다. 또한이 경우 낭비되는 시간은 짧지 만 (아마도 벡터를 지우는 것) 프로그래머가 습관적 으로이 작업을 수행하면 시간이 더 걸립니다. 또한 많은 함수가 임시 (생성 된 연산자를 생각하십시오)를 생성합니다.이 연산자는 0으로 초기화되고 즉시 할당됩니다. 프로파일 러에서 급상승을보기에는 너무 작은 숨겨진 손실주기, 그러나 코드베이스 전체에서 블리드주기. 또한 어떤 사람들은 생성자에서 훨씬 더 많은 일을합니다 (분명히 아니요). 생성자가 무거운 쪽에서 발생하는 사용되지 않는 변수에서 수 밀리 초의 이득을 보았습니다. 생성자가 부작용을 일으키는 즉시 컴파일러는 그것을 최적화 할 수 없으므로 위의 코드를 사용하지 않으면 초기화되지 않는 생성자를 선호합니다.
벡터 3 bla (noInit); bla = doSomething ();
const Vector3 = doSomething()
? 그런 다음 반환 값 최적화를 수행하여 할당을 높일 수 있습니다.
부울 식 평가 감소
이것은 매우 미묘하지만 위험한 코드 변경이므로 필사적입니다. 그러나 과도한 횟수로 평가되는 조건이있는 경우 대신 비트 연산자를 사용하여 부울 평가의 오버 헤드를 줄일 수 있습니다. 그래서:
if ((foo && bar) || blah) { ... }
된다 :
if ((foo & bar) | blah) { ... }
대신 정수 산술을 사용하십시오. foo와 bar가 상수이거나 if () 이전에 평가되는 경우 이는 일반적인 부울 버전보다 빠를 수 있습니다.
보너스로 산술 버전은 일반 부울 버전보다 분기가 적습니다. 또 다른 최적화 방법 입니다.
큰 단점은 지연 평가를 잃는다는 것입니다-전체 블록이 평가되므로 할 수 없습니다 foo != NULL & foo->dereference()
. 이 때문에 이것이 유지하기 어렵다는 것은 논쟁의 여지가 있으며, 따라서 절충이 너무 클 수 있습니다.