최적화를 위한 유용한 정보
이 문서에서는 Visual C++의 최적화와 관련된 몇 가지 모범 사례에 대해 설명합니다. 다음 항목에 대해 설명합니다.
컴파일러 및 링커 옵션
프로필 기반 최적화
적합한 최적화 수준
부동 소수점 스위치
최적화 declspec
최적화 pragma
__restrict 및 __assume
내장 함수 지원
예외
컴파일러 및 링커 옵션
프로필 기반 최적화
Visual C++에서는 PGO(프로필 기반 최적화)를 지원합니다. 이 최적화에서는 이전에 응용 프로그램의 계측된 버전을 실행하여 얻은 프로필 데이터를 사용하여 이후에 응용 프로그램의 최적화를 수행합니다. PGO는 사용하는 데 많은 시간이 필요하므로 모든 개발자가 사용할 수는 없지만, 제품의 최종 릴리스 빌드에는 PGO를 사용하는 것이 좋습니다. 자세한 내용은 프로필 기반 최적화을 참조하십시오.
또한 링크 시간 코드 생성이라고도 하는 전체 프로그램 최적화 및 /O1과 /O2 최적화가 향상되었습니다. 일반적으로 이들 옵션 중 하나를 사용하여 컴파일한 응용 프로그램은 이전 버전의 컴파일러에서 컴파일한 동일한 응용 프로그램보다 빠릅니다.
자세한 내용은 /GL(전체 프로그램 최적화) 및 /O1, /O2(크기 최소화, 속도 최대화)를 참조하십시오.
적합한 최적화 수준
가능하면 최종 릴리스 빌드는 프로필 기반 최적화를 사용하여 컴파일해야 합니다. 인프라가 부족하여 계측된 빌드를 실행할 수 없거나 응용 프로그램 사용 시나리오를 알 수 없어 PGO를 사용하여 빌드할 수 없는 경우에는 전체 프로그램 최적화를 사용하여 빌드하는 것이 좋습니다.
/Gy 스위치도 매우 유용합니다. 이 스위치를 사용하면 각 함수에 대해 별도의 COMDAT가 생성되므로 링커에서 보다 유연하게 참조되지 않는 COMDAT 제거 및 COMDAT 정리를 수행할 수 있습니다. /Gy를 사용하는 경우 유일한 단점은 빌드 시간이 약간 길어진다는 것입니다. 따라서 일반적으로 이 스위치를 사용하는 것이 좋습니다. 자세한 내용은 /Gy(함수 수준 링크 사용)을 참조하십시오.
64비트 환경에서 링크하는 경우 /OPT:REF,ICF 링커 옵션을, 32비트 환경에서는 /OPT:REF를 사용하는 것이 좋습니다. 자세한 내용은 /OPT(최적화)을 참조하십시오.
또한 최적화된 릴리스 빌드에서도 디버그 기호를 생성하는 것이 좋습니다. 이렇게 해도 생성되는 코드에는 영향을 주지 않으며, 필요한 경우 응용 프로그램을 디버깅하기가 매우 쉬워집니다.
부동 소수점 스위치
/Op 컴파일러 옵션이 제거되었고 부동 소수점 최적화를 다루는 다음과 같은 네 개의 컴파일러 옵션이 추가되었습니다.
/fp:precise |
대부분의 경우에 권장되는 기본값입니다. |
/fp:fast |
게임 등 성능이 가장 중요한 경우에 권장됩니다. 이 옵션을 사용하면 성능이 가장 빨라집니다. |
/fp:strict |
정확한 부동 소수점 예외 및 IEEE 동작이 필요한 경우에 권장됩니다. 이 옵션을 사용하면 성능이 가장 느려집니다. |
/fp:except[-] |
/fp:strict 또는 /fp:precise와 함께 사용할 수 있지만 /fp:fast와 함께 사용할 수는 없습니다. |
자세한 내용은 /fp(부동 소수점 동작 지정)을 참조하십시오.
최적화 declspec
이 단원에서는 프로그램에서 성능을 향상시키는 데 사용할 수 있는 두 개의 declspec인 __declspec(restrict) 및 __declspec(noalias)을 살펴봅니다.
restrict declspec은 __declspec(restrict) void *malloc(size_t size);과 같이 포인터를 반환하는 함수 선언에만 적용할 수 있습니다.
restrict declspec은 별칭이 지정되지 않은 포인터를 반환하는 함수에 사용됩니다. 이 키워드는 malloc의 C 런타임 라이브러리 구현에 사용됩니다. 그 이유는 이미 해제된 메모리를 사용하는 등의 잘못된 작업을 수행하지 않는 한 이 함수는 현재 프로그램에서 이미 사용 중인 포인터 값을 반환하지 않기 때문입니다.
restrict declspec을 사용하면 컴파일러 최적화를 수행하기 위한 보다 자세한 정보가 컴파일러에 제공됩니다. 컴파일러가 판단하기 가장 어려운 내용 중 하나는 다른 포인터의 별칭으로 사용되는 포인터를 확인하는 것이며, 이 정보를 제공하면 컴파일러에 매우 큰 도움이 됩니다.
이 정보는 컴파일러에 대한 약속에 불과하며 컴파일러에서 확인하는 것은 아닙니다. 프로그램에서 이 restrict declspec을 잘못 사용하면 프로그램이 잘못 작동할 수 있습니다.
자세한 내용은 restrict을 참조하십시오.
noalias declspec도 함수에만 적용할 수 있으며, 함수가 부분 순수 함수임을 나타냅니다. 부분 순수 함수는 지역 변수, 인수 및 인수의 1차 간접 참조만 참조하거나 수정하는 함수입니다. 이 declspec은 컴파일러에 대한 약속이며, 함수에서 전역 변수 또는 포인터 인수의 2차 간접 참조를 참조하면 컴파일러에서 생성하는 코드로 인해 응용 프로그램이 제대로 작동하지 않을 수 있습니다.
자세한 내용은 noalias을 참조하십시오.
최적화 pragma
코드 최적화와 관련된 몇 가지 유용한 pragma도 있습니다. 여기서 살펴볼 첫 번째 pragma는 #pragma optimize입니다.
#pragma optimize("{opt-list}", on | off)
이 pragma를 사용하면 함수별로 최적화 수준을 설정할 수 있습니다. 이 기능은 특정 함수를 최적화하여 컴파일할 때 응용 프로그램이 충돌하는 것과 같이 드물게 발생하는 경우에 유용합니다. 이 pragma를 사용하여 단일 함수의 최적화를 해제할 수 있습니다.
#pragma optimize("", off)
int myFunc() {...}
#pragma optimize("", on)
자세한 내용은 최적화을 참조하십시오.
인라인은 컴파일러가 수행하는 가장 중요한 최적화 중 하나이며, 여기서는 인라인 동작을 수정하는 몇 가지 pragma를 살펴 봅니다.
#pragma inline_recursion은 응용 프로그램에서 재귀적 호출을 인라인할 수 있는지 여부를 지정하는 데 유용합니다. 기본적으로 이 pragma는 해제됩니다. 작은 함수를 얕은 수준으로 재귀 호출하는 경우 이 pragma를 설정할 수 있습니다. 자세한 내용은 inline_recursion을 참조하십시오.
다른 유용한 pragma로는 인라인 수준을 제한하는 #pragma inline_depth가 있습니다. 이 pragma는 일반적으로 프로그램 또는 함수의 크기를 제한하려는 경우에 유용합니다. 자세한 내용은 inline_depth을 참조하십시오.
__restrict 및 __assume
Visual C++에는 성능에 도움이 되는 __restrict 및 __assume과 같은 여러 키워드가 있습니다.
우선 __restrict와 __declspec(restrict)은 서로 다르다는 점에 주의해야 합니다. 이 두 키워드에는 일부 연관성이 있지만 의미가 서로 다릅니다. __restrict는 const 또는 volatile과 같은 형식 한정자이지만 포인터 형식에만 사용할 수 있습니다.
__restrict로 한정된 포인터는 __restrict 포인터라고 합니다. __restrict 포인터에는 __restrict 포인터를 통해서만 액세스할 수 있습니다. 즉, __restrict 포인터가 가리키는 데이터에는 다른 포인터를 사용하여 액세스할 수 없습니다.
__restrict는 Visual C++ 최적화 프로그램의 강력한 도구이지만 세심한 주의를 기울여 사용해야 합니다. 이 키워드를 잘못 사용하면 최적화를 수행한 결과로 응용 프로그램이 제대로 동작하지 않을 수 있습니다.
__restrict 키워드는 이전 버전의 /Oa 스위치를 대체합니다.
개발자는 __assume을 사용하여 컴파일러가 일부 변수의 값을 예상하도록 할 수 있습니다.
예를 들어 __assume(a < 5);을 사용하면 최적화 프로그램은 해당 코드 줄에서 변수 a가 5보다 작은 것으로 가정합니다. 마찬가지로 이는 컴파일러에 대한 약속입니다. 프로그램의 이 시점에서 실제로는 a가 6인 경우 컴파일러에서 프로그램을 최적화한 후 프로그램이 예상과 다르게 동작할 수 있습니다. __assume은 switch 문 및 조건식 앞에서 가장 유용하게 사용됩니다.
__assume에는 몇 가지 제한이 있습니다. 첫째로, 이는 __restrict와 마찬가지로 제안일 뿐이므로 컴파일러에 의해 무시될 수 있습니다. 또한 현재 __assume에는 변수와 상수로 구성된 부등식만 사용할 수 있습니다. 예를 들어 assume(a < b)과 같이 기호로만 구성된 부등식은 사용할 수 없습니다.
내장 함수 지원
내장 함수는 컴파일러가 호출 관련 정보를 내부적으로 알고 있는 경우에 사용되는 함수 호출이며, 내장 함수에서는 라이브러리의 함수를 호출하는 대신 해당 함수의 코드를 생성합니다. <Installation_Directory>\VC\include\에 있는 intrin.h 헤더 파일에는 지원되는 세 가지 플랫폼(x86, x64 및 ARM)에서 각각 사용할 수 있는 내장 함수가 모두 들어 있습니다.
프로그래머는 어셈블리를 사용하지 않고도 내장 함수를 통해 코드를 자세히 분석할 수 있습니다. 내장 함수를 사용하면 다음과 같은 여러 이점이 있습니다.
코드의 이식성이 향상됩니다. 일부 내장 함수는 여러 CPU 아키텍처에서 사용할 수 있습니다.
코드가 여전히 C/C++로 작성되므로 코드를 읽기가 쉬워집니다.
코드에서 컴파일러 최적화의 이점을 활용할 수 있습니다. 컴파일러가 발전함에 따라 내장 함수 코드 생성도 개선됩니다.
자세한 내용은 컴파일러 내장 함수 및 내장 함수 사용의 장점를 참조하십시오.
예외
예외를 사용하면 성능이 저하됩니다. try 블록을 사용하면 컴파일러에서 특정 최적화를 수행하지 못하게 하는 몇 가지 제한이 발생합니다. x86 플랫폼에서는 try 블록을 사용하면 코드 실행 도중 상태 정보를 추가로 생성해야 하므로 성능이 더욱 저하됩니다. 64비트 플랫폼에서는 try 블록으로 인한 성능 저하가 그만큼 크지 않지만, 예외가 throw될 때 처리기를 찾고 스택을 해제하는 과정에서 성능에 많은 부담이 생길 수 있습니다.
따라서 코드에 반드시 필요하지 않은 try/catch 블록은 사용하지 않는 것이 좋습니다. 예외를 사용해야 하는 경우에는 가능하면 동기 예외를 사용합니다. 자세한 내용은 구조적 예외 처리 (C/C++)을 참조하십시오.
마지막으로, 예외적인 경우에만 예외를 throw하십시오. 일반적인 제어 흐름에서 예외를 사용하면 성능이 저하되기 쉽습니다.