시간 중심의 코드 성능 향상을 위한 팁
빠른 코드를 작성하려면 해당 응용 프로그램의 모든 측면을 이해하고 시스템과 상호 작용하는 방법을 이해해야 합니다.이 항목에서는 시간 중심의 코드 부분 성능을 향상시킬 수 있는 코딩 기법을 설명합니다.
요컨대, 시간 중심의 코드 성능을 향상시키려면 다음 사항을 알고 있어야 합니다.
프로그램에서 속도가 빨라야 하는 부분
코드의 크기와 속도
새로운 기능 사용에 따른 비용
작업 수행에 필요한 최소 작업
코드의 성능에 대한 정보는 성능 모니터(perfmon.exe)를 사용하여 얻을 수 있습니다.
캐시 적중 및 페이지 폴트
정렬 및 검색
MFC 및 클래스 라이브러리
공유 라이브러리
힙
스레드
작은 작업 집합
캐시 누락 및 페이지 폴트
프로그램 명령어 및 데이터를 찾아 보조 저장 장치로 이동하는 페이지 폴트뿐만 아니라 내부 및 외부 캐시에 대한 누락된 캐시 적중은 프로그램 성능을 저하시킵니다.
CPU 캐시 적중은 10–20 클럭 주기가 소요될 수 있습니다.외장 캐시 적중은 20–40 클럭 주기가 소요될 수 있습니다.프로세서가 초 당 5억개의 명령어를 처리하고 페이지 폴트 1회 당 2밀리초의 시간이 걸린다고 가정할 때 페이지 폴트는 100만 클럭 주기가 소요될 수 있습니다.따라서 누락된 캐시 적중 및 페이지 폴트 횟수를 줄이는 코드를 작성하면 프로그램 실행 성능이 향상됩니다.
프로그램의 속도가 느린 이유 중 하나는 필요 이상으로 페이지 폴트가 많거나 캐시가 누락되기 때문입니다.이러한 문제를 방지하려면 참조 집약성이 좋은 데이터 구조를 사용하는 것이 중요합니다. 참조 집약성이란 관련된 항목을 함께 두어 참조하기 쉽도록 하는 것을 의미합니다.보기 좋은 데이터 구조라도 참조 집약성이 미약하면 속도가 느릴 수 있고 그 반대인 경우도 있습니다.두 가지 예를 들겠습니다.
항목을 검색하거나 목록의 끝까지 이동할 때 건너 뛴 각 연결마다 캐시가 누락되거나 페이지 폴트가 발생할 수 있기 때문에 동적으로 할당된 연결 리스트는 프로그램 성능을 저하시킬 수 있습니다.간단한 배열을 기초로 목록을 구현하면 캐싱은 더 좋아지고 페이지 폴트는 줄어들기 때문에 실제로 속도는 더 빨라질 수 있습니다. 배열의 크기가 커지기는 더 어려울 것이라는 사실을 감안하더라도 속도는 여전히 더 빠를 것입니다.
동적으로 할당된 연결 리스트를 사용하는 해시 테이블은 성능을 저하시킬 수 있습니다.확장되는 경우 동적으로 할당된 연결 리스트를 사용하여 내용을 저장하는 해시 테이블의 성능은 그보다 상당히 더 떨어질 것입니다.실제로 최종 분석에서는 상황에 따라 배열을 통한 단순한 선행 검색이 실제로 더 빠를 수 있습니다.?폐쇄형 해시?라는 배열 기반 해시 테이블은 종종 간과되긴 하지만, 성능이 뛰어난 구현 방법입니다.
정렬 및 검색
정렬은 일반적인 많은 작업에 비해 시간이 많이 소모됩니다.불필요한 속도 저하를 방지하는 가장 좋은 방법은, 작업을 빨리 수행해야 하는 시간에는 정렬을 수행하지 않는 것입니다.다음과 같은 방법을 사용할 수 있습니다.
작업을 빨리 수행해야 하는 시간이 지난 후에 정렬을 수행합니다.
작업을 빨리 수행해야 하는 시간 이전에 미리 데이터를 정렬합니다.
반드시 정렬해야 하는 데이터 부분만 정렬합니다.
경우에 따라서는 목록을 정렬된 순서로 빌드할 수 있습니다.정렬된 순서로 데이터를 삽입해야 할 경우 참조 집약성이 떨어지는 더 복잡한 데이터 구조를 사용하게 되면 캐시 누락과 페이지 폴트가 발생할 수 있으므로 주의해야 합니다.모든 경우에 적용할 수 있는 방법은 없습니다.여러 방법을 사용해 보고 차이점을 평가해 보십시오.
다음은 정렬에 관한 몇 가지 일반적인 팁입니다.
스톡 정렬을 사용하여 버그를 최소화합니다.
정렬의 복잡성을 줄이기 위한 사전 작업을 하면 도움이 됩니다.데이터를 한 번 전달하여 비교를 단순화하고 정렬을 O(n log n)에서 O(n)로 줄이면 결과가 확실하게 미리 나타납니다.
실행하려고 하는 데이터와 정렬 알고리즘의 참조 집약성을 고려합니다.
검색은 정렬보다 사용할 수 있는 방법이 적습니다.검색에서 시간이 중요한 경우에는 이진 검색이나 해시 테이블 조회가 가장 좋은 방법이지만 정렬의 경우와 마찬가지로 집약성을 염두에 두어야 합니다.작은 배열을 통한 선형 검색은 페이지 폴트나 캐시 누락을 초래하는 포인터가 많은 데이터 구조를 통한 이진 검색보다 더 빠를 수 있습니다.
MFC 및 클래스 라이브러리
MFC(Microsoft Foundation Classes)를 사용하면 코드를 작성하는 일이 매우 간단해질 수 있습니다.시간 중심 코드를 작성할 때는 일부 클래스에 내재하는 오버헤드를 주의해야 합니다.시간 중심 코드에서 사용하는 MFC 코드를 검토하여 성능 요구 사항에 맞는지 확인하십시오.다음은 반드시 알고 있어야 하는 MFC 클래스 및 함수 목록입니다.
CString MFC는 C 런타임 라이브러리를 호출하여 CString에 메모리를 동적으로 할당합니다.일반적으로 CString은 동적으로 할당된 다른 문자열만큼 효율적이며,동적으로 할당된 모든 문자열과 마찬가지로 동적 할당 및 해제에 따르는 오버헤드를 지닙니다.스택에서의 간단한 char 배열을 사용하면 같은 작업을 더 빨리 수행할 수 있습니다.상수 문자열을 저장하려면 CString을 사용하지 말고**const char ***를 대신 사용합니다.CString 개체를 사용하여 수행하는 모든 작업에는 약간의 오버헤드가 따릅니다.런타임 라이브러리인 문자열 함수를 사용하는 것이 더 빠를 수 있습니다.
CArray CArray는 일반 배열과는 달리 융통성을 제공하지만 프로그램에 융통성이 필요하지 않을 수도 있습니다.배열의 구체적인 한계를 알면 대신 전역 고정 배열을 사용할 수 있습니다.CArray를 사용하는 경우에는 CArray::SetSize를 사용하여 크기를 설정하고, 재할당이 필요할 경우 몇 개 요소만큼씩 증가시킬지를 지정하십시오.이 값을 지정하지 않고 요소를 추가하면 배열이 자주 재할당되고 복사될 수 있는데, 이는 비효율적이며 메모리가 조각화될 수 있습니다.또한 항목을 배열에 삽입하면 Carray는 다음 항목을 메모리로 이동시켜 배열의 크기가 커질 수 있습니다.이로 인해 캐시 누락과 페이지 폴트가 발생할 수 있습니다.MFC에서 사용하는 코드를 검토하면 시나리오와 관려된 더 구체적인 코드를 작성하여 성능을 향상시킬 수 있습니다.예를 들어, Carray는 템플릿이므로 특정 형식에 Carray 특수화를 제공할 수 있습니다.
CList CList는 이중으로 연결된 목록이므로 목록의 맨 위, 맨 아래 및 알려진 위치(POSITION)에서 요소를 삽입하는 속도가 빠릅니다.요소를 값 또는 인덱스로 조회하려면 순차 검색이 필요하지만 목록이 길면 오래 걸릴 수 있습니다.코드에 이중으로 연결된 목록이 필요하지 않은 경우에는 CList 사용을 다시 고려할 수 있습니다.단일 연결 리스트를 사용하면 모든 작업에 대한 추가 포인터와 해당 포인터에 대한 메모리를 업데이트하는 오버헤드가 줄어듭니다.추가로 사용되는 메모리 양은 얼마 안되지만 캐시 누락이나 페이지 폴트의 또 다른 원인이 됩니다.
IsKindOf 이 함수는 많은 호출을 생성하고 서로 다른 데이터 영역의 많은 메모리에 액세스할 수 있으므로 참조 집약성이 떨어지게 됩니다.디버그 빌드(예: ASSERT 호출)에는 유용하지만 릴리스 빌드에서는 사용하지 않는 것이 좋습니다.
PreTranslateMessage 창의 특정 트리에 여러 키보드 액셀러레이터 키가 필요하거나 메시지 펌프에 메시지 처리를 삽입해야 할 때 PreTranslateMessage를 사용합니다.PreTranslateMessage는 MFC 디스패치 메시지를 변경합니다.PreTranslateMessage를 재정의하는 경우에는 필요한 수준에서만 재정의하십시오.예를 들어, 특정 뷰의 하위 뷰로 전달되는 메시지에만 관심이 있다면 CMainFrame::PreTranslateMessage를 재정의할 필요가 없습니다.대신 뷰 클래스의 PreTranslateMessage를 재정의하십시오.
창으로 보낸 메시지를 처리하려면 PreTranslateMessage를 사용하여 정상적인 디스패치 경로를 우회하지 마십시오.정상적인 디스패치 경로를 우회하려면 창 프로시저와 MFC 메시지 맵을 사용하십시오.
OnIdle WM_KEYDOWN과 WM_KEYUP 이벤트 사이처럼 예상치 않은 시간에 유휴 이벤트가 발생할 수 있습니다.코드를 트리거할 때는 타이머를 사용하는 것이 더 효율적인 방법일 수 있습니다.거짓 메시지를 작성하거나 OnIdle 재정의에서 항상 TRUE를 반환하여 OnIdle이 강제로 반복 호출되지 않도록 하십시오. 이렇게 하면 스레드가 대기할 수 없게 됩니다.이 경우에도 타이머나 별도의 스레드를 사용하는 것이 더 효율적일 수 있습니다.
공유 라이브러리
코드를 다시 사용하는 것이 바람직합니다.그러나 다른 사람이 작성한 코드를 사용하려는 경우, 성능이 중요한 작업에 어떤 영향을 미치는지 정확히 알아야 합니다.가장 좋은 방법은 소스 코드를 단계별로 실행하거나 PView 또는 성능 모니터 등의 도구를 사용하여 측정하는 것입니다.
힙
여러 개의 힙을 사용할 때는 신중해야 합니다.HeapCreate 및 HeapAlloc으로 만들어진 힙을 사용하여 관련된 할당 집합들을 관리한 다음 삭제할 수 있습니다.너무 많은 메모리를 커밋하지 않도록 하십시오.여러 힙을 사용하는 경우 처음에 커밋되는 메모리 양에 특히 주의하십시오.
여러 힙을 사용하는 대신 코드와 기본 힙 사이의 인터페이스에 대해 도우미 함수를 사용할 수 있습니다.도우미 함수를 사용하면 응용 프로그램의 성능을 향상시킬 수 있는 사용자 지정 할당 전략을 쉽게 작성할 수 있습니다.예를 들어, 작은 할당을 자주 수행하는 경우 기본 힙의 한 부분으로 할당을 지역화할 수 있습니다.큰 블록의 메모리를 할당한 다음 도우미 함수를 사용하여 그 블록에서 다시 할당할 수 있습니다.이 방법을 사용하면 기본 힙에서 할당이 이루어지므로 사용되지 않은 메모리를 지니는 추가 힙이 없어집니다.
그러나, 기본 힙을 사용하면 참조 집약성이 떨어지는 경우도 있습니다.프로세스 뷰어, Spy++ 또는 성능 모니터를 사용하여 힙 간에 이동하는 개체의 영향을 측정하십시오.
힙을 측정하면 힙에 대한 모든 할당을 설명할 수 있습니다.C 런타임 디버그 힙 루틴을 검사점으로 사용하고 힙을 덤프합니다.Microsoft Excel 등의 스프레드시트 프로그램으로 결과를 읽어 오고 피벗 테이블을 사용하여 결과를 볼 수 있습니다.할당의 전체 수, 크기 및 분포를 기록하고작업 집합 크기와 비교하십시오.관련 크기의 개체 클러스터링도 검토하십시오.
성능 카운터를 사용하여 메모리 사용을 모니터링할 수도 있습니다.
스레드
백그라운드 작업의 경우 이벤트 유휴 상태를 효율적으로 처리하는 것이 스레드를 사용하는 것보다 더 빠를 수 있습니다.단일 스레드 프로그램에서는 참조 집약성을 이해하기가 더 쉽습니다.
가장 좋은 방법은 차단한 운영 체제 알림이 백그라운드 작업의 루트에 있는 경우에만 스레드를 사용하는 것입니다.이러한 경우 이벤트에 대한 주 스레드를 차단하는 것이 비실용적이기 때문에 스레드를 사용하는 것이 가장 좋은 해결 방법입니다.
또한 스레드를 사용할 때는 통신 문제도 있습니다.메시지 목록을 사용하거나 공유 메모리를 할당하여 사용하여 스레드 사이의 통신 연결을 관리해야 합니다.통신 연결을 관리하려면 일반적으로 경쟁 상태와 교착 상태 문제를 방지하기 위해 동기화가 필요합니다.이러한 복잡성 때문에 버그와 성능 문제가 발생하기 쉽습니다.
자세한 내용은 유휴 루프 처리 및 다중 스레딩을 참조하십시오.
작은 작업 집합
작업 집합의 크기가 작아질수록 참조 집약성은 더 좋아지고, 페이지 폴트는 줄어들며, 캐시 적중은 더 높아집니다.프로세스 작업 집합은 운영 체제에서 참조 집약성을 측정하기 위해 직접 제공하는 가장 가까운 메트릭입니다.
작업 집합의 상한 및 하한을 설정하려면 SetProcessWorkingSetSize를 사용합니다.
작업 집합의 상한 및 하한을 구하려면 GetProcessWorkingSetSize를 사용합니다.
작업 집합의 크기를 보려면 Spy++를 사용합니다.