어떻게 하면 포인터를 보다 안전하게 사용할 수 있을까?
어떻게 하면 포인터를 이용해 내 프로그램의 성능을 더 높일 수 있을까?
선수과목 < Computer Architecture, Operating System, Data structure, Signal Process, Numerical Analysis >
(우선순위 높은 순) - 뭐 위 과목들을 못 배웠어도 읽어나갈 순 있을거야. 용어는 좀 찾아보길.
1. 최적화 (Optimization)
측정할 수 없는 것은 공학이 아니다.
우리가 흔히 사용하는 OS 인 Windows system 은 RTOS (real-time OS) 가 아니다.
그 말은, 같은 연산의 수행시간도 노이즈 요인에 의해 쉽사리 뒤바뀐다는거다.
그 노이즈 요인에 대해 모두 분석해낼 수 있지만, 그런 화이트박스 분석은 시간이 너무 오래 걸린다.
확률적으로 해석하는게 도움이 된다는 것이지.
어쨌건 CPU 내부에는 전원이 들어오면서부터 매 클럭 카운팅 되는 레지스터가 있다.
그걸 읽어서 64비트를 구현하기 위한 32비트 레지스터 조합인 EDX:EAX 에 넣어주는 메카니즘이
Read Time Stamp Counter, 즉 RDTSC 다.
QueryPerformanceFrequency 와 QueryPerformanceCounter 를 이용해 성능을 측정할 수도 있지만,
시스템마다 다르지만 대체로 QueryPerformanceCounter 를 한 번 부르기 위해
사용하는 클럭의 수는 1000 클럭 약간 밑돈다.
반면, rdtsc 를 intrinsic 으로 호출 할 경우, 이에 소비되는 클럭은 17 clock 정도다.
어딘가는 담아야 할테니 저장하는데 드는 비용까지 18 clock 정도다.
우리는 timeGetTime() 같은 멀티미디어 타이머나
- 사운드 카드에 달려 있던 카운터라서 그렇게 부른다.
- legacy PC 의 보드 내장 타이머 같은 경우는 1 / 18.2 초 정밀도 밖에 안되어서
사운드 카드는 고정밀도 클럭을 갖게 되었다.
그 밖의 느린 타이머들을 이용해 수만번 더 반복해 코드의 수행시간을 측정해서 성능을 판단할 수도 있다.
하지만 루프 자체의 비용이 만만찮고, branch hazard 때문에 다른 코드들과 화합할때의 성능을 판단하긴 어렵다.
그래서 내가 사용하는 방법은, N 번 수행동안 측정된 최소 클럭을 선택하는 것이다.
RTOS 가 아니라서 커널과 다른 프로세스의 자원 독점 간섭 때문에 latency 가 생길 순 있지만,
멀쩡히 실행을 하면서도 최소 필요 클럭보다 적게 측정되는 노이즈가 발생하는 경우는 없기 때문이다.
즉, impulse noise (신호처리에 나온다) 유형의 다른 별명인,
pepper & salt noise 중에 salt noise 는 있어도 pepper 는 없으니
- 기대값 보다 턱없이 낮은 값이 나올때 까만 점이라고 후춧가루, 높은 값이 나올때 흰 점이라고 소금이라고 부른다
min filter 가 유효하다는 거다.
우리는 앞으로 이 과정을 진행하면서, 틈틈이 성능측정을 할텐데,
RDTSC 를 사용해 해당 코드를 N 번 수행할때 각각의 최소 경과시간을 얻어내는 미시적인 방법을 쓸 것이라는 말이다.
기억해라. 눈감고 코딩할거면 이딴거 필요없다. C / C++ 쓸거면 이딴거 알아둬라.
2. Base offset 측정
카메라로 따지면 white balance calibration 같은거다.
실제론 Quantum Efficiency 를 위한 준비를 하는 셈이지만. 이 말은 몰라도 된다.
(광학에서 다루는 용어)
Intel 80x86 family Architecture 에서,
펜티엄 모델로 들어가면서 Pipeline 과 Scalar 개념이 도입된 것으로 기억한다.
그 뒤 super-pipeline 과 super-scalar 가 도입 되면서 어마어마해 졌다.
펜티엄2 로 가면서 그 단계가 2배 심화되었고, 그 후 몇 십단계까지 늘었다고 들었다.
(40 단계 란 소릴 듣고 오잉? 했던게 언 십여년 전)
파이프라인의 깊이를 알지 못하면 미시적인 측정이 불가능하다.
몇 번 계산해봐야 파이프라인에 묻혀서 티도 안날테니까 말야.
특히나 이런 준비는 새로운 하드웨어를 접하게 될 때도 필수적이다.
니가 어떤 embedded machine 에서 프로그래밍을 해야 되는데,
곱셉 나눗셈이 얼마나 빠른지, 덧셈과 비트연산이 얼마나 차이나는지, 부동소수점 연산이 개차반인지 아닌지 알고,
대입 연산에 드는 비용까지, 철저히 알고있어야 애초에 하드웨어 설계가 잘못된,
혹은 요구사항 기준이 잘못된 시스템에서 개노가다 털다 회사가 망하는 수준에 이르지 않을 수 있다.
즉, 예를 들어 어떤 USB 의 대역폭이 초당 40MBs 정도일때,
거기다 DMA 로 데이타를 실어 주는 녀석의 throughput 이 40MBs 가 되지 않는데
멍청하게 어디서 오류지 하고 삽질하지 말란 소리다.
"아 이부분은 spec out 이니 하드웨어 사양을 올려야 합니다."
라는 정보를 전해주기 위한 지표로 활용하기 위해서도 기본 연산 성능 측정은 중요하다.
Hyper Threading 이 아닐경우는 언급한(할) 수치들의 절반 정도가 예상된다.
자. 내 컴퓨터의 pipeline 깊이를 측정해 보자. += 연산자 정도면 reasonable 하다고 생각한다.
ADD some_register, another_register 혹은 immediate 가 1클럭에 실행되니까 말이다,
(some_register += another_register 란 뜻의 instruction)
#include <iostream>
using namespace std;
#ifdef _MSC_VER
#include <intrin.h>
#define clock() __rdtsc()
#endif
// 측정 클럭의 최소값을 다룰거니까 아래가 필요해지지
#define min(a, b) ( (a) < (b)? (a): (b) )
#define update_min(a, b) ( (a) = min( (a), (b) ) )
// 튀어오르는 값이 좀 적어질 만큼 확률적으로 반복할 횟수를 정해봤다
#define LOOP_COUNT 1000000
int main()
{
// RDTSC recipie
tick_t elapsed0, elapsed1, elapsed2;
volatile size_t sum0 = 0, sum1 = 0, sum2 = 0;
// volatile 을 이용해 루프 최적화를 막아야 여러번의 연산과 같은 조건에서의 두 함수간의 비교가 가능하다
// set to the max
elapsed0 = elapsed1 = elapsed2 = -1;
for( int i = 0; i < LOOP_COUNT; ++i )
{
tick_t begin = clock();
sum0 += 1; // 이걸 넣으나 안넣으나 실행결과는 내 컴퓨터에서 18 이 나온다.
tick_t elapsed = clock() - begin;
update_min( elapsed0, elapsed );
}
cout << "RDTSC cost : " << elapsed0 << " clocks" << endl << endl;
getchar();
return 0;
}
왜 sum0 += 1; 을 넣으나 안넣으나 같은 클럭이 소요되는 것인가?
앞에서 설명했지만, 파이프라인에 파묻혔기 때문이다.
그럼 저걸 늘려서 언제 18이 아닌 값이 나오는지 측정해보면 파이프라인의 깊이를 알 수 있겠지?
패러렐즈 라는 virtual machine 에서 구동중인 내 맥프로(연탄맥) 를 시험해 봤다.
sum0 += 1; 을 복붙해서 늘려 보자.
sum0 += 1; // 1
sum0 += 1; // 2
...
sum0 += 1; // 9
sum0 += 1; // 10
sum0 += 1; // 1
...
sum0 += 1; // 90
sum0 += 1; // 1
//sum0 += 1; // 2 stall
91 번까지 동일하게 18 클럭이 나오다 92번째 에서 21클럭이 나왔다.
(몇 번 실행해 봐야 안다.
한 번 실행에선 처음 초기화 비용과 커널의 작업간 간섭땜에 cpu 내부 명령처리 순서가 엇박자가 되는 경우가 있다)
즉, 약 내 CPU 는 코어 하나당 96개 정도의 명령어 병렬화가 가능한 것이다.
(WOW 로 돌렸다 64비트 운영체제의 32비트 실행 기준이다)
64비트 RDTSC 값 저장하느라 한 번 써먹고 있을테고 4의 배수로 잡아보면 저정도 수치란 거지.
니들도 해 봐라.
이 것의 의미는
내컴에서 96개의 기본 명령조합으로 만들어진 코드는 거의 항상 기본 rdtsc 소요 클럭이 측정될 것이란 거다.
즉, 자그마한 두 함수를 성능비교하기 위해선 복붙으로 부피 좀 불려둬야 한다는 뜻이다.
for 문 안에서 개개의 소요시간을 측정하고 있음에 주의해라.
(하이퍼쓰레딩 풀면 192개 나오려나? 나중에 실험해봐야지)