Microsoft Visual C++ 2005를 사용한 프로필 기준 최적화
Kang Su Gatlin
Microsoft Corporation
2004년 3월
적용 대상:
Microsoft Visual C++ 2005
요약 : 응용 프로그램을 실제 고객 시나리오에 맞게 조정할 수 있는 강력한 새 기능인 Microsoft Visual C++ 2005(이전의 Visual C++ "Whidbey")의 프로필 기준 최적화(profile-guided optimization)에 대해 설명합니다. 실제로 20% 이상 성능을 향상시킬 수 있습니다(13페이지/인쇄 페이지 기준).
목차
소개
일반 C++ 컴파일러의 작동 방식
링크 시간 코드 생성으로 전체 프로그램 최적화
일반적인 효율성
기타 PGO 도구
PGO 및 Visual Studio IDE
PGO 사용을 위한 몇 가지 팁
결론
소개
C++에서 프로그래밍하는 이유에는 여러 가지가 있습니다. 그 중 가장 중요한 이유는 성능을 크게 향상시킬 수 있기 때문입니다. Microsoft Visual C++ 2005 릴리스를 사용하여 모든 일반 최적화 방법으로 성능을 크게 향상시킬 수 있을 뿐 아니라 새롭게 추가된 기능으로 응용 프로그램을 더욱 유용하게 활용할 수 있습니다. 이 기사에서는 PGO(프로필 기준 최적화)를 사용하여 성능을 크게 향상시키는 방법을 설명합니다.
일반 C++ 컴파일러의 작동 방식
프로필 기준 최적화(profile-guided optimization)를 전체적으로 이해하기 위해 일반 컴파일러에서 수행할 최적화를 결정하는 방식을 알아 봅니다.
일반 컴파일러는 정적 소스 파일을 기반으로 최적화를 수행합니다. 즉, 이 컴파일러는 소스 파일의 텍스트를 분석하지만 소스 코드에서 직접 얻을 수 없는 잠재적 사용자 입력에 대한 지식을 활용하지 않습니다. 예를 들어 .cpp 파일에 다음 함수가 있다고 가정합니다.
int setArray(int a, int *array) { for(int x = 0; x < a; ++x) array[x] = 0; return x; }
이 파일에서 컴파일러는 "a"가 int 형식이어야 한다는 사실 외에는 그 잠재적 값에 대해 알지 못하며, 일반 배열 맞춤에 대해서도 알지 못합니다.
이 컴파일러/링커가 특별히 잘못된 것은 아니지만, 여기서는 두 가지 주요 최적화 기회가 상실됩니다. 첫째로 모든 소스 파일을 함께 분석하여 얻을 수 있는 정보를 활용하지 않으며, 둘째로 응용 프로그램의 예상되거나 프로파일링된 동작을 기반으로 최적화를 수행하지 않습니다. WPO 및 PGO를 사용하면 이 두 가지를 모두 수행할 수 있습니다.
링크 시간 코드 생성으로 전체 프로그램 최적화
Itanium 컴파일러의 모든 최신 버전을 비롯하여 Visual C++ 7.0 이상을 사용할 경우 Visual C++에서는 LTCG(링크 시간 코드 생성)라는 메커니즘을 지원합니다. Matt Pietrek이 MSDN에서 무료로 볼 수 있는 Under the Hood 칼럼(2002년 5월)에서 LTCG에 대해 좋은 기사를 썼기 때문에 이 절에서는 많은 시간을 투자하지는 않을 것이며 기본적인 내용만 설명하겠습니다.
LTCG는 컴파일러에서 모든 소스 파일을 단일 변환 단위로서 효율적으로 컴파일할 수 있게 하는 기술입니다. 이 작업은 다음 두 단계로 수행됩니다.
컴파일러는 소스 파일을 컴파일하고 결과를 표준 개체 파일이 아닌 생성된 .obj 파일에 IL(Intermediate Language)로 내보냅니다. 이 IL이 Microsoft .NET Framework에서 사용하는 MSIL과 다르다는 것에는 신경쓰지 않아도 됩니다.
/LTCG를 사용하여 링커를 호출할 경우 링커는 실제로 백엔드를 호출하여 WPO로 컴파일된 모든 코드를 컴파일합니다. WPO .obj 파일의 모든 IL이 집계되고 전체 프로그램의 호출 그래프를 생성할 수 있습니다. 여기서 컴파일러 백엔드 및 링커는 전체 프로그램을 컴파일하여 실행 파일 이미지에 연결합니다.
WPO를 사용할 경우 이제 컴파일러는 전체 프로그램의 구조에 대한 자세한 정보를 보유하게 됩니다. 그러므로 특정 유형의 최적화를 더욱 효율적으로 수행할 수 있습니다. 예를 들어 일반 컴파일/링크를 수행할 때 컴파일러는 소스 파일 foo.cpp의 함수를 소스 파일 bar.cpp로 인라인할 수 없습니다. bar.cpp를 컴파일할 경우 컴파일러에는 foo.cpp에 대한 어떤 정보도 없습니다. WPO를 사용할 경우 컴파일러는 이제 bar.cpp 및 foo.cpp(IL 형식)를 모두 사용할 수 있으며 상호 변환 단위 인라인과 같이 일반적으로는 불가능한 최적화를 수행할 수 있습니다.
응용 프로그램을 컴파일하여 LTCG를 활용하는 방법에는 다음과 같은 두 단계가 있습니다.
먼저, 전체 프로그램 최적화 컴파일러 스위치(/GL)를 사용하여 소스 코드를 컴파일합니다.
cl.exe /GL /O2 /c test.cpp foo.cpp
그런 다음 /LTCG 스위치를 사용하여 프로그램의 모든 개체 파일을 연결합니다.
link /LTCG test.obj foo.obj /out:test.exe
이로써 작업이 완료됩니다. 이제, 생성된 실행 파일을 실행할 수 있으며, 실행 속도는 일반적으로 더 빠릅니다. 지금까지 LTCG의 많은 이점에 대해 알아 보았지만 여기에는 대가가 따릅니다. 즉, 컴파일/링크 시 필요한 메모리가 증가됩니다. 그 이유는 수십 또는 수백 개의 컴파일 단위일 수 있는 IL을 모두 처리할 수 있어야 하기 때문입니다. 그러므로 프로젝트를 만드는 데 필요한 메모리 요구 사항이 증가될 수 있으며 더 나아가 프로젝트를 만드는 전체 시간이 증가될 수 있습니다.
프로필 기준 최적화
LTCG는 확실히 성능상 이점이 있지만 LTCG를 통한 응용 프로그램의 성능 향상은 시작에 불과합니다. LTCG와 함께 사용되는 또 다른 새 기술은 추가로 성능을 향상시킬 수 있으며, 대부분의 경우 성능을 크게 향상시킬 수 있습니다. 이 기술을 PGO(프로필 기준 최적화)라고 합니다.
PGO의 개념은 간단합니다. 실제 입력에서 실행 파일/dll을 실행하여 프로필을 생성합니다. 생성된 프로필은 컴파일러가 특정 실행 파일에 대해 최적화된 코드를 생성하도록 하는 데 사용됩니다. PGO를 관리되지 않는 실행 파일 또는 DLL 최적화에 적용할 수는 있지만 .NET/관리되는 이미지에는 적용할 수 없습니다. 기사의 나머지 부분에서 정보는 DLL에도 동일하게 적용되지만, 최적화된 이미지를 실행 파일 또는 응용 프로그램이라고 지칭하겠습니다. 이제 더 상세한 내용을 살펴보겠습니다.
다음과 같은 세 가지 일반적인 단계로 PGO 응용 프로그램을 만들 수 있습니다.
측정 코드(instrumented code)로 컴파일합니다.
측정 코드(instrumented code)를 연습합니다.
최적화된 코드로 다시 컴파일합니다.
다음에서 이러한 세 단계에 대해 보다 심도 있게 설명하겠습니다. 그림 1에서 이 프로세스를 그림으로 볼 수 있습니다.
그림 1. PGO 빌드 프로세스
측정 코드 컴파일
첫 번째 단계는 실행 파일을 측정(instrumentation)하는 것입니다. 그러기 위해서는 먼저 WPO(/GL)를 사용하여 소스 파일을 컴파일합니다. 그런 다음 응용 프로그램에서 모든 소스 파일을 가져와서 /LTCG:PGINSTRUMENT 스위치(약어: /LTCG:PGI)에 연결합니다. PGO가 응용 프로그램에서 전체적으로 작동되기 위해 모든 파일을 /GL로 컴파일해야 하는 것은 아닙니다. PGO는 /GL로 컴파일된 이 파일을 측정하며 그렇지 않은 파일은 측정하지 않습니다.
이 측정(instrumentation)은 코드에 다른 유형의 시험을 전략적으로 배치하여 수행됩니다. 이러한 시험은 대략 두 가지 유형으로 나눌 수 있습니다. 즉, 흐름 정보를 수집하는 시험과 값 정보를 수집하는 시험이 있습니다. 사용할 시험을 결정하는 방법, 사용 위치 등에 대해 상세히 설명하지 않지만 시험의 효과적인 사용을 위한 연습은 수행할 것입니다. 또한 측정 코드(instrumented code)는 측정되지 않은 동일한 /O2 코드만큼 /O2로 최적화되지 않는다는 것에도 신경쓸 필요가 없습니다. 그러나 측정 코드(instrumented code)에 배치하는 시험을 간섭하지 않으면서 가능한 한 많은 최적화를 수행하게 됩니다. 그러므로 측정(instrumentation)과 최적화되지 않은 코드를 함께 사용할 경우 응용 프로그램 속도가 느려집니다. 물론, 최적화된 코드는 코드의 시험 없이 최적화됩니다.
/LTCG:PGI를 연결한 결과는 실행 파일 또는 DLL 및 PGO 데이터베이스 파일(.PGD)입니다. 기본적으로 PGD 파일은 생성된 실행 파일의 이름을 사용하지만, 사용자는 /PGD:filename 링커 옵션으로 연결할 때 PGD 파일의 이름을 지정할 수 있습니다.
아래 표 1에는 왼쪽 열에 표시된 각 단계 후 생성되는 파일 목록이 있습니다. 파일은 제거되지 않습니다.
표 1. 각 단계 후 생성되는 파일
단계 |
생성되는 파일 |
---|---|
컴파일 시작 후 |
MyApp.cpp foo.cpp |
/c /GL로 컴파일한 후 |
MyApp.obj foo.obj |
/LTCG:PGI /PGD:MyApp.pgd /out:MyApp.inst.exe로 연결한 후 |
MyApp.inst.exe MyApp.pgd |
세 시나리오로 측정 응용 프로그램(instrumented application)을 연습한 후 |
MyApp1.pgc MyApp2.pgc MyApp3.pgc |
/LTCG:PGO./PGD:MyApp.pgd로 다시 연결한 후 |
MyApp.opt.exe |
측정 코드 연습
측정 실행 파일(instrumented executable)을 만든 후 다음 단계는 실행 파일을 연습하는 것입니다. 실제 작업에서 사용되는 방법을 반영하는 시나리오로 실행 파일을 실행하여 연습을 수행합니다. 각 시나리오 실행의 결과는 PGO 카운트 파일(.PGC)입니다. 이 파일은 .PGD 파일과 같은 이름을 사용하며 그 끝에 번호가 붙습니다. 이 번호는 "1"로 시작하여 실행할 때마다 증가합니다. 특정 시나리오가 유용하지 않다고 판단될 경우 주어진 .PGC 파일을 제거할 수 있습니다.
최적화된 코드 컴파일
마지막 단계는 시나리오를 실행하여 수집된 프로필 정보에 실행 파일을 다시 연결하는 것입니다. 이번에는 응용 프로그램을 연결할 때 링커 스위치 /LTCG:PGOPTIMIZE(/LTCG:PGO라고도 함)를 사용합니다. 이 스위치는 생성된 프로필 데이터를 사용하여 최적화된 실행 파일을 만듭니다. 최적화하기 전에 링커는 pgomgr을 자동으로 호출합니다. 기본적으로 pgomgr은 .PGD 파일과 이름이 같은 현재 디렉터리의 모든 .PGC 파일을 .PGD 파일에 병합합니다.
- 소스 코드를 업데이트합니다. 컴파일된 응용 프로그램의 소스 코드가 .PGD 파일을 생성한 후 변경된 경우 /LTCG:PGO가 되돌려져 단순히 /LTCG 빌드를 수행하며 프로필 정보를 사용하지 않습니다. 그러므로 측정 코드(instrumented code)에서 프로필을 생성하는 데 많은 시간을 보낸 후 코드를 약간 변경해야 하지만 생성된 프로필을 다시 사용하려는 경우에는 /LTCG:PGUPDATE(/LTCG:PGU라고도 함)를 지정할 수 있습니다. PGUPDATE를 사용하면 원래 .PGD 파일을 사용하면서 링커에서 수정된 소스 코드를 컴파일할 수 있습니다.
PGO로 수행할 수 있는 작업
앞에서 PGO 응용 프로그램의 생성 방법에 대해 살펴보았습니다. 이제 PGO로 어떤 작업을 할 수 있는지, 가능한 최적화는 무엇인지 알아보겠습니다. 여기서는 부분적인 목록을 제공하며, 새로운 최적화를 찾고 더 나은 학습법을 배우면서 현재 수행하는 최적화 집합을 확장할 것입니다.
인라인 . 앞에서 설명했듯이 WPO는 더 많은 인라인 기회를 찾을 수 있는 기능을 응용 프로그램에 부여합니다. PGO를 사용하면 여기에 이러한 인라인 수행 결정을 돕는 추가 정보를 보완할 수 있습니다. 예를 들어 아래 그림 2, 3 및 4의 호출 그래프를 살펴보십시오.
그림 2에서 a, foo 및 bat는 모두 bar를 호출하며 이어서 bar는 baz를 호출합니다.
그림 2. 프로그램의 원래 호출 그래프
그림 3. PGO 를 사용하여 얻은 측정된 호출 빈도
그림 4. 그림 3 에서 얻은 프로필을 기반으로 최적화된 호출 그래프
부분
인라인. 다음은 대부분의 프로그래머에게 적어도 부분적으로는 친숙한 최적화입니다. 많은 hot 함수에서 hot이 아닌 함수 내에 코드의 경로가 있으며, 그 중 일부는 cold입니다. 아래 그림 5에서 코드의 자주색 섹션을 인라인하고 파란색 섹션은 인라인하지 않습니다.
그림 5. 자주색 노드를 인라인하고 파란색 노드는 인라인하지 않은 제어 흐름 그래프
Cold 코드 구분. 프로파일링 중 호출되지 않는 코드 블록인 cold 코드는 섹션 집합 끝에서 제거됩니다. 그러므로 작업 집합의 페이지는 프로필 정보에 따라 일반적으로 실행될 명령으로 구성됩니다.
그림 6. 최적화된 레이아웃에서 자주 사용하는 기본 블록은 모으고 cold 기본 블록은 멀리 옮기는 방법을 보여 주는 제어 흐름 그래프
크기 / 속도 최적화. 더 자주 호출되는 함수는 속도를 최적화하고 자주 호출되지 않는 함수는 크기를 최적화할 수 있습니다. 이 방법이 가장 효과적일 수 있습니다.
블록 레이아웃. 이 최적화에서는 함수를 통해 가장 가장 많이 사용되는 경로를 형성하고 배치하여 이러한 경로가 공간적으로 가까이 위치하도록 합니다. 그러면 명령 캐시 활용을 늘리고 사용되는 작업 집합 크기 및 사용되는 페이지 수를 줄일 수 있습니다.
가상 호출 예상. 가상 호출은 vtable을 통해 이동하여 메서드를 호출하기 때문에 비용이 많이 소모됩니다. PGO를 사용할 경우 컴파일러는 가상 호출의 호출 사이트에서 예상하여 예상된 개체의 메서드를 가상 호출 사이트에 인라인할 수 있습니다. 이러한 결정을 내리기 위한 데이터는 측정 응용 프로그램(instrumented application)을 사용하여 수집합니다. 최적화된 코드에서 인라인된 함수 주변의 가드는 예상된 개체 유형이 파생된 개체와 일치하도록 하는 검사입니다.
다음 pseudocode는 기본 클래스, 파생된 두 클래스 및 가상 함수를 호출하는 함수를 보여 줍니다.
class Base{
... virtual void call(); }
class Foo:Base{ ... void call(); }
class Bar:Base{ ... void call(); }
// PGO가 최적화하기 전의 Func 함수입니다. void Func(Base *A){ ... while(true) { ... A->call(); ... } }(참고: 프로그래머 코멘트는 샘플 프로그램 파일에는 영문으로 제공되며 기사에는 설명을 위해 번역문으로 제공됩니다.)
아래 코드는 "A"의 동적 형식이 거의 항상 Foo인 경우 위 코드를 최적화한 결과를 보여 줍니다.
<pre IsFakePre="true" xmlns="http://www.w3.org/1999/xhtml">// PGO가 최적화한 후의 Func 함수 호출입니다.
void Func(Base *A){ ... while(true) { ... if(type(A) == Foo:Base) { // A의 인라인->call(); } else A->call(); ... } }
DLL 사용
PGO 및 DLL에 대한 간단한 참고 사항: 실행 파일을 실행하여 DLL을 연습/프로파일링합니다. 그러면 대표적 시나리오 집합에서 DLL이 연결됩니다. 나아가 서로 다른 시나리오에 대해 다른 실행 파일을 사용하고 모든 시나리오를 단일 .PGD 파일에 병합할 수 있습니다. 현재 PGO 기술은 정적 라이브러리를 지원하지 않습니다.
일반적인 효율성
현재 PGO를 구현할 경우 실제로 성능을 향상시키는 데 매우 효과적이라는 것이 입증되었습니다. 예를 들어 Microsoft SQL Server와 같이 큰 실제 응용 프로그램에서는 성능이 30% 이상 향상되며 SPEC 벤치마크에서는 아키텍처에 따라 4%-15% 향상됩니다. 그림 7은 SPEC 벤치마크를 사용할 경우 가장 적절한 정적 컴파일 설정(연결 시간 코드 생성)보다 PGO가 성능을 더 많이 향상시키는 것을 보여 줍니다.
그림 7. 세 가지 플랫폼 모두에서 PGO 를 사용할 경우와 링크 시간 코드 생성을 사용할 경우의 SPEC 성능 향상 비교
기타 PGO 도구
Microsoft Visual C++에서 지원되는 PGO는 사용자가 필요한 작업을 정확히 수행할 수 있도록 하는 두 가지 도구와 함께 제공됩니다. 이 절에서는 이러한 각 도구에 대해 설명합니다.
pgomgr. pgomgr은 PGO가 생성하는 .pgd 파일에서 처리 후 작업을 수행하는 도구입니다. PSDK Itanium 컴파일러 사용자의 경우 Whidbey의 pgomgr은 pgmerge를 대신하며 이전 PSDK 컴파일러의 pgopt 일부를 대신합니다. 이 두 도구는 그 기능이 결합되어 더 이상 사용할 수 없습니다. .PGC 파일을 PGO의 최적화 단계에 사용하려면 .PGD로 병합해야 합니다. pgomgr 도구로 이 병합 작업을 수행합니다. 해당 구문은 다음과 같습니다.
pgomgr [options] [Profile-Count paths] <Profile-Database>
기본적으로 .PGC 파일이 있는 디렉터리에서 /LTCG:PGI가 실행될 경우, 연결 중인 프로그램의 .PGD 파일에 일치하면 .PGC 파일이 병합됩니다. 그러므로 항상 pgomgr을 사용해야 하는 것은 아닙니다. 다음은 옵션 목록입니다.
/Gets help
/helpSame as /
/clear Remove all merge data from the specified pgd
/detail Display verbose program statistics
/merge[:n] Merge the given PGC file(s), with optional integer weight
/summary Display program statistics
/unique Display decorated function names
pgosweep. pgosweep 도구는 PGO 측정(instrumentation)으로 빌드된 프로그램의 실행을 중단하고 현재 카운트를 새 .PGC 파일에 기록한 다음 런타임 데이터 구조에서 카운트를 지웁니다. 이 프로그램은 주로 두 가지 경우에 사용할 수 있습니다. 첫째는 끝나지 않는 코드에서 PGO를 사용하는 경우입니다. 그 예로는 OS 커널이 있습니다. 둘째는 특정 프로그램 부분에 대해 정확한 프로필 정보를 얻으려는 경우입니다. 예를 들어 응용 프로그램의 밤 시간 시나리오를 프로파일링하지 않으려는 경우에는 pgosweep를 사용한 다음 해당 시나리오 부분에서 .PGC 파일을 삭제합니다.
pgosweep의 사용은 다음과 같습니다.
pgosweep <instrumented image> <.PGC file to be created>
PGO 및 Visual Studio IDE
명령줄 도구를 유용하게 사용할 수 있지만, Microsoft Visual Studio IDE(Integrated Development Environment)에서 작업하는 경우에는 PGO와 같은 기능을 Visual Studio IDE에서 완전히 활용하고자 할 수 있습니다. Visual Studio 2005(이전의 Visual Studio "Whidbey")에서는 메뉴 항목 집합을 통해 PGO를 지원합니다. 그러므로 프로그래머는 측정 빌드(instrumented build)를 수행하거나 시나리오를 실행하거나 최적화된 빌드를 수행하거나 업데이트 빌드를 수행할 수 있습니다. 이 측정 빌드(instrumented build), 최적화된 빌드 및 업데이트 빌드는 모두 출력으로 .exe 또는 .dll 파일을 생성합니다. 최적화된 빌드와 업데이트 빌드를 수행하려면 .pgd 파일을 사용할 수 있어야 합니다. .pgd 파일은 Run Profiling Scenario 메뉴 항목을 실행하여 생성할 수 있습니다.
그림 8. Visual Studio 2005 IDE 의 PGO 지원 스크린 샷
PGO 사용을 위한 몇 가지 팁
다음은 사용자의 PGO 작업을 향상시킬 수 있는 몇 가지 기본 팁입니다.
프로필 데이터를 생성하는 데 사용되는 시나리오는 배포할 때 응용 프로그램에 나타나는 실제 시나리오와 비슷해야 합니다. 이 시나리오는 코드 커버리지를 수행할 때는 시도하지 않습니다.
실제 사용과 다른 시나리오를 사용하여 연습하면 PGO를 사용하지 않는 경우보다 코드 성능이 더 나빠질 수 있습니다.
app.opt.exe 및 app.inst.exe와 같이 최적화된 코드에는 측정 코드(instrumented code)와 다른 이름을 지정합니다. 이러한 방식으로 설치된 응용 프로그램을 다시 실행하면 모든 작업을 다시 실행하지 않고도 시나리오 프로필 집합을 보완할 수 있습니다.
결과를 조정하려면 pgomgr의 /clear 옵션을 사용하여 .PGD 파일을 지웁니다.
실행되는 데 걸리는 시간이 다른 두 시나리오에 가중치를 동일하게 지정하려면 .PGC 파일에서 가중치 스위치(pgomgr의 /merge:weight)를 사용하여 해당 시나리오를 조정할 수 있습니다.
속도 스위치를 사용하여 속도/크기 임계값을 변경할 수 있습니다.
인라인 임계값 스위치는 신중하게 사용합니다. 0에서 100까지의 값은 선형이 아닙니다.
결론
결론적으로 PGO 응용 프로그램을 생성하는 기본 단계는 다음과 같습니다.
PGO를 사용할 파일에서 전체 프로그램 최적화(/GL)로 컴파일합니다. 파일을 /GL로 컴파일하지 않으면 전체 프로그램 최적화 및 PGO를 사용하여 최적화하지 않을 파일을 선택할 수 있습니다.
/LTCG:PGINSTRUMENT를 사용하여 응용 프로그램을 연결 시간 코드 생성에 연결합니다. 그러면 측정 실행 파일(instrumented executable)이 생성됩니다.
.pgc 파일을 생성하는 시나리오로 응용 프로그램을 연습합니다.
링커를 호출하는 경우라도 /LTCG:PGOPTIMIZE를 사용하여 응용 프로그램을 다시 컴파일합니다. 그러면 프로필 데이터를 기준으로 실행 파일이 최적화됩니다.
결과적으로 프로그램 또는 라이브러리가 실행될 실제 상황에 맞게 최적화됩니다.
저자 소개
Kang Su Gatlin은 Microsoft Visual C++ 그룹의 프로그램 관리자이며 UC San Diego에서 박사 학위를 받았습니다. 그는 고성능 계산 및 최적화를 집중적으로 연구하고 있으며, 코드 실행 속도를 높이는 데 주력하고 있습니다.
최종 수정일: 2004년 8월 3일