TN_1205: TechNote 1205 성능 프로파일러를 사용하여 제네릭 리스트와 ArrayList 비교
Bill Gibson, 프로그램 관리자
Microsoft Corporation
적용 대상 :
Microsoft Visual Studio 2005 Team System
프로파일러를 사용하여, 제네릭 리스트 구현과 ArrayList 구현의 성능 검토
Visual Studio Team System 2005 에는 새롭고 강력한 성능 프로파일러 도구가 있습니다. 이 프로파일러에서는 샘플링 (정기적인 간격으로 프로그램 상태의 스냅샷을 생성)과 계측 (함수의 시작 포인트와 종료 포인트를 모두 파악하기 위해서 어셈블리에 코드 삽입) 이라는 두 가지 방법으로 데이터를 캡쳐 할 수 있습니다. 또, 이 프로파일러는 네이티브 및 매니지 실행가능 파일, DLL 파일, ASP.NET 웹사이트에 대해서 기능합니다.
.NET Framework 2.0에서는 새로운 기능으로서 제네릭 컬렉션 구현이 추가되었습니다. 이러한 컬렉션을 사용하면, 엄밀하게 형식 지정된 개체의 컬렉션을 생성할 수 있어(많은 경우) ArrayList 에 비해 큰 폭으로 성능이 향상되었습니다. 여기에서는 제네릭 리스트를 사용하는 경우와 ArrayList 를 사용하는 경우의 차이를 간단한 시나리오를 사용하여 조사하여 새로운 성능 프로파일러 도입에 도움이 됩니다. 이번 프로파일을 실행하는 단순한 C# 콘솔 프로그램을 다음에서 보여줍니다.
static void Main(string[] args)
{
Program myProgram = new Program();
//myProgram.GenericsTest();
myProgram.ArrayListTest();
}
public void GenericsTest()
{
int total = 0;
List li = new List();
for (int i = 0; i < 20; i++)
li.Add(i);
for (int i = 0; i < 10000000; i++)
foreach (int el in li)
total += el;
}
public void ArrayListTest()
{
int total = 0;
ArrayList al = new ArrayList();
for (int i = 0; i < 20; i++)
al.Add(i);
for (int i = 0; i < 10000000; i++)
foreach (int el in al)
total += el;
}
콘솔 프로그램에 새로운 성능 세션을 추가하려면, [Tools] 메뉴의 [Performance Tools] 를 포인트하여, [performance wizard] 를 클릭합니다. 다음에 성능 마법사에서 프로파일의 실행 대상으로 콘솔 프로그램을 선택하여, 프로파일 방법으로 샘플링을 선택합니다. 이렇게 하여, 프로파일의 대상이 바르게 선택된 성능 세션이 생성되어 새로운 [performance explorer] 창에 추가됩니다. 프로파일 세션을 실행하려면, [performance explorer] 창의 상단 왼쪽에 있는 실행 버튼을 클릭합니다. 시나리오를 실행하면, 리포트 파일이 생성되어 성능 세션의 "리포트"폴더에 배치됩니다.
샘플링 모드로 각 시나리오 (ArrayList 와 제네릭 리스트)를 2 회 실행하여 (결과를 비교하여, 같은 결과가 되었는지 확인합니다), 개체의 매핑과 수명 프로파일을 유효하게 하고, 샘플링 모드로 각 시나리오를 1 회 실행했습니다. 매니지 응용 프로그램에서는 프로파일러를 사용하여, 매니지 개체의 매핑과 수명에 관한 정보를 수집할 수 있습니다. 성능 세션 속성의 첫 페이지에서 이 옵션을 유효하게 할 수 있습니다. 속성 메뉴를 표시하려면, 성능 탐색기로 성능 세션을 오른쪽 클릭합니다. 이것을 실제로 시험할 때, 다음과 같은 점에 주의해야 합니다. ArrayList 구현을 사용하여 개체 매핑을 실행하면, 실행 및 분석에 매우 오랜 시간이 소요됩니다. 따라서 개체 매핑의 실행은 필요에 따라 루프의 사이즈를 작게 하는 것을 권장합니다. 이유는 결과를 설명할 때에 설명하겠습니다.
결과
먼저 주목할 것은 매핑을 유효하게 하지 않았던 실행으로 수집된 샘플의 총수입니다. 성능 리포트 파일을 어떤 것이나 선택하여 [properties]창을 표시하면, 샘플의 총수를 확인할 수 있습니다. 이번은 10000000 클록 주기 마다 샘플을 수집하도록 프로파일러를 설정하여 (이것은 기본값의 설정입니다), 샘플 수를 기본으로 실행 시간을 매우 정확하게 추정할 수 있습니다. 다음에 샘플 소개를 보여줍니다 (비교 때문에 디버그 버전의 숫자 값도 기재했습니다).
제네릭을 사용했을 경우 (디버그): 샘플 총수 828
제네릭을 사용했을 경우 (출시): 샘플 총수 556
ArrayList 를 사용했을 경우 (디버그): 샘플 총수 1998
ArrayList 를 사용했을 경우 (출시): 샘플 총수 1907
이러한 숫자 값을 보면, 제네릭을 사용하여 wall clock 속도가 크게 향상된 것을 알 수 있습니다. 제네릭을 사용하는 경우는 정수 목록의 추가와 취득은 boxing 할 필요가 없는 것은 알고 있기 때문에 예상할 수 있습니다. 다만, boxing를 회피하여 속도가 향상되어도 제네릭을 사용할 때의 코드 크기 증가 가능성에 의해 상쇄될 수 있습니다.
다음은 각 구현의 어디서 실행 시간의 대부분이 소비되었는지를 보겠습니다. 리포트 파일을 ( 아직 열려 있지 않은 경우는) 열어, 함수 뷰로 이동합니다. 다음 그림과 같이, 함수 뷰에는 프로파일러에 의해서 샘플 수집을 한 모든 함수가 목록 표시됩니다 (이 그림에서는 실제 표시되는 함수의 일부를 생략했습니다).표시되는 함수는 배타 시간의 순서로 정렬됩니다. (배타 시간과는 그 함수에 소비한 시간입니다. 반면, 포괄 시간에는 그 함수의 손자 함수에 소비된 시간도 포함됩니다).위의 screen shot는 제네릭 실행으로, 아래의 screen shot는 ArrayList 실행에 의한 것입니다.
.jpg)
다음에서 ArrayList 의 결과를 보여줍니다.
.jpg)
백분율을 살펴보면, 두번째 구현에서는 같은 일을 하는데 거의 동일한 시간이 소요되었습니다. 어느 쪽의 구현에서도 메인 함수 (GenericTest 과 ArrayListTest)에 많은 시간이 소요되고 Enumerator.GetNext 호출에 가장 많은 시간이 소요됩니다. ArrayList 구현에서는 백분율이 두 번째로 높은 함수는 CLRStubOrUnknownAddress 인 것을 알 수 있습니다. 이것은 네이티브에서 매니지에의 마이그레이션 시에 프로파일러가 함수명을 해결할 수 없는 경우에 나타나는 함수입니다. CLRStub 로 매우 많은 샘플이 수집되는 이유의 해명에 관심이 있기 때문에 이 함수를 오른쪽 클릭하여, CLRStubOrUnknownAddress 에 의해서 호출된 함수를 표시하도록 선택했습니다. 그러면, 루트 함수로서 CLRStubOrUnknownAddress 가 선택된, 호출자/호출처 뷰 (이하 참조)에 바뀌었습니다. 호출자/호출처 뷰에는 현재의 함수를 호출한 모든 함수 (위의 창)와 현재의 함수에서 호출된 모든 함수 (아래의 창)가 표시됩니다.
.jpg)
이 목록을 보면, ArrayList 메서드에서는 CLRStub 로 466 개의 샘플이 수집되었으며, 이러한 샘플은 주로 ArrayListEnumeratorSimple.get_Current 함수 (이 함수의 샘플은 154 개)와 ArrayListTest 함수 (이 함수의 샘플은 285 개) 인 것을 알 수 있습니다. 이 경우, 최적의 가설은 ArrayList에서는 실행 속도를 올리기 위해서 네이티브 코드로의 신속한 전환을 사용하여, 이러한 긴밀한 루프에서는 CLRStub 호출을 다수 확인하여 정리 (CLRStub 호출은 네이티브 코드의 마이그레이션을 나타내므로) 하는 것입니다.
지금까지 제네릭 구현이 실행 속도가 빠른 것과 양쪽의 구현에서 같은 함수에 거의 같은 비율의 시간이 소요되는 것을 알 수 있었습니다. 또, 성능 향상을 위해서, ArrayList 로 네이티브 코드에의 마이그레이션을 생각할 수 있는 것도 확인했습니다. 그럼 이것들 두 가지의 구현으로 무엇이 매핑되는지 확인하기 위해서, 개체 매핑 리포트에 관심을 가져 봅시다. 매핑 소개 리포트를 다음에서 보여줍니다. 위가 제네릭 구현에 대한 리포트로, 아래의 ArrayList 구현에 대한 리포트입니다. 이미 설명한 것처럼, 이 리포트는 .NET 개체의 매핑과 수명의 수집을 유효하게 한 상태로 생성했습니다.
.jpg)
ArrayList 의 결과를 다음에서 보여줍니다.
.jpg)
여기서 알 수 있는 것은 방대한 수의 Enumerator 매핑이 실행되어, ArrayList 실행에 많은 시간이 소요되는 이유입니다. ArrayList 구현에서는 foreach 루프가 실행될 때마다 매핑이 1 회 실행됩니다. 이것은 흥미로운 일이지만, ArrayList 구현의 속도가 늦은 원인은 아닙니다. ArrayList 실행의 함수 뷰를 확인하면 JIT 매핑 함수에 소비되는 시간은 그만큼 많지 않기 때문입니다.
또, int 의 경우 unboxing는 단지 간단한 형식 체크 뿐이므로, 문제는 ArrayList 의 unboxing이 아닙니다. (다만, 이것도 조금은 성능에 영향을 줍니다) 실제의 성능의 문제를 확인하려면, 다음 성능 리포트의 호출 트리 뷰에 있는 두 종류의 구현을 봐 주세요. 리포트의 호출 트리 뷰에서는 모든 함수가 계층창에 표시되어 상하로 이동하여 코드 경로를 확인할 수 있습니다. ArrayList 구현에서는 제네릭 버전에는 존재하지 않는 많은 가상 함수가 존재하여, 호출 트리는 훨씬 깊고 복잡합니다. 알기 쉽게 하기 위해, 비교하는 두 개의 함수에 붉은 표시를 했습니다. 또, 파란색으로 표시된것 처럼, ArrayList 구현은 아직 전부 배포 되지는 않습니다. 새로운 컬렉션 클래스는 훨씬 간소화된 코드 경로로 생성 되어, 단순하게 실행하는 명령 수가 지금보다 적어집니다.
.jpg)
이 백서가 실제로 생성 한 코드의 성능 문제를 조사하기 위해 프로파일러를 사용할 수 있다고 생각할 수 있는 계기가 되기를 바랍니다.