reference: 시스템 프로그래밍 수업 / 최종무 교수님
목차
- 최적화에 대한 개념
- 성능 표현 방법에 대한 이해
- CPU 독립 최적화 기법 <= 어느 CPU에서든 적용 가능하다.
- CPU 의존 최적화 기법 <= CPU 특성에 따라 다르다.
- 메모리 계층 구조와 성능 효과
최적화된 프로그램이란?
1) 문제 상황에 맞는 적절한 알고리즘과 자료구조을 선택한다.
ex) 주소록 프로그램의 검색 기능에 어떤 알고리즘을 도입할 것인가?
2) Divide a job into portions that can be computed in parrel! (ex, Map-Reduce Programming model in Google)
3) Generate an efficient <= 이번 포스팅 설명 내용
동일한 자료구조, 알고리즘, 동일한 병렬화 => 코드를 보다 더 효율적으로 작성한다!
a = a * 4; vs a = a << 2;
위 두 코드 모두 결과는 같다. CPU 입장에서는 곱하기 연산이 쉬프트 연산보다 4~5배 정도 느리다. 곱하기는 multiplication. 쉬프트 연산은 integer 연산. interger 연산은 최근 CPU에선 거의 1클록에 수행. multiplication은 더 많은 클록이 필요.
for(i=0; i<10; i++){
a = 5;
b[i] = c[i] + a;
}
위 코드에서 a = 5;
이 대입연산은 굳이 루프문 안에 있을 필요가 없다. 이를 뽑아낸다. "Code Motion" (cpu dependent optimization)
code motion: 반복문 안에 있을 필요가 없는 것을 반복문 밖으로 뺴내어 한번만 수행하도록 하는 테크닉
Tradeoff 존재
: 최적화된 코드는 대게 가독성이 떨어질 수도 있다. 따라서 readability vs optimization 이라는 trade-off가 존재한다.
Deeply CPU dependent
: CPU 내부 구조(이외 메모리 자원 구조까지)에 대한 이해가 필요하며, 어셈블리 레벨까지의 분석도 필요하다.
Not straightforward!
: 약간의 변화로도 큰 성능 차이를 가져올 수 있지만, 명확한 기법이라도 큰 효율성을 가져오지 못할 수 있다. 즉 직관적인 결과를 기대할 수 없다.
void vsum1(int n){
int i;
for(i=0; i<n; i++){
c[i] = a[i] + b[i];
}
}
void vsum2(int n){
int i;
for(i=0; i<n; i+=2){
// "loop unrolling" 기법 적용.
c[i] = a[i] + b[i];
c[i+1] = a[i+1] + b[i+1];
}
}
예를 들어 a[10] => a[11]
과 같이 하나의 요소가 늘어날 때마다 증가되는 사이클을 뜻한다.
typedef struct {
int len;
data_t *data; // data_t는 int or float
} vec_rec, *vec_ptr;
#include <stdlib.h> // NULL 메크로 사용
#include <stdbool.h>
#define data_t int // or float
// 사용 자료구조
typedef struct {
int len;
data_t *data;
}vec_rec, *vec_ptr;
// 사용 함수들 선언
vec_ptr new_vec(int len);
bool get_vec_element(vec_ptr v, int idx, data_t *dest);
int vec_length(vec_ptr v);
// 함수들 구현
vec_ptr new_vec(int len){
vec_ptr result = (vec_ptr)malloc(sizeof(vec_rec));
if(!result)
return NULL;
result->len = len;
if(len > 0){
data_t *data = (data_t*)calloc(len, sizeof(data_t));
/*
malloc과 calloc은 할당하고자 하는 메모리 크기를 바이트 단위로 할당받을 수 있는 함수.
malloc을 통해 할당받게 되면 기억공간의 값들은 쓰레기값으로 채워져있고,
calloc은 값들이 0으로 초기화되어 있다.
*/
if(!data){
free((void *)result);
return NULL;
}
result->data = data;
}
else{
result->data = NULL;
}
return result;
}
bool get_vec_element(vec_ptr v, int idx, data_t *dest){
if(idx < 0 || idx >= v->len)
return false;
*dest = v->data[idx];
return true;
}
int vec_length(vec_ptr v){
return v->len;
}
void combine1(vec_ptr v, data_t *dest){
int i;
*dest = IDENT; // 0(덧셈) or 1(곱셈)
for(i=0; i<vec_length(v); i++){
data_t val;
get_vec_element(v, i, &val);
*dest = *dest OPER val;
}
// 수행 후 caller의 dest에 총합이 쓰여짐.
}
(실험환경: 펜티엄 III, 펜티엄 III는 P6 마이크로 아키텍처를 가짐)
앞서 루프 내 비효울적 대입연산(a=5)을 제거하는 code motion을 보였다. 마찬가지로 반복되는 함수 호출을 없앤다.
int length = vec_length(v);
// eliminating loop ineffcient(code motion)
// Reducing procedure calls
void combine2(vec_ptr v, data_t *data){
int i;
int length = vec_length(v); // 루프 안이라면 의미없는 함수 호출에 여러 번 일어남.
*dest = IDENT;
for(i=0; i<length; i++){
data_t val; // 블록 내에서 의미있는 지역변수를 선언한 것.
// 스택에 자리만 잡힐 뿐 루프 밖으로 배떠라도 성능은 동일
get_vec_element(v, i, &val);
}
}
data_t *data = get_vec_start(vec_ptr);
함수는 프로그램을 모듈화하기에 도움을 주지만, 호출의 오버헤드로 인해 성능 측면에선 나쁘다.
=> "may impair program modularity"
data_t* get_vec_start(vec_ptr){
return v->data;
}
void combine3(vec_ptr, data_t* dest){
int i;
int length = vec_length(v);
// 한번의 함수 호출
// 벡터의 시작 포인터를 먼저 빼놓아 함수 호출이 아닌, 포인터 연산을 통해 데이터를 가져온다.
data_t *data = get_vec_start(v);
*dest = IDENT;
for(i=0; i<length; i++){
*dest = *dest OPER data[i];
// 여러번의 get_vec_element() 메서드 호출을 줄인다.
/*
함수는 프로그램을 모듈화하기에 도움을 주지만, 호출의 오버에드로 인해 성능 측면에서 나쁘다.
*/
}
}
레이턴시가 긴 메모리 참조가 아닌 레지스터를 최대한 활용하도록 한다.
void combine4(vec_ptr v, data_t *dest){
int i;
int length = vec_length(v);
data_t *data = get_vec_start(v);
// 지역변수
data_t x = IDENT; // *dest = IDENT;
// 지역변수는 함수 내에서만 의미가 있다. 따라서 변수 x 공간을 메모리에 할당하지 않고 레지스터에 할당한다.
// 이에 메모리 접근이 감소할 것이다.
// 예를들어 eax 레지스터를 반복해서 사용할 것이다.
for(i=0; i<length; i++){
x = x OPER data[i];
}
// 마지막에 한번 결과를 반영해주면 된다.
*dest = x;
}
combine4 어셈블리 코드
addl %eax, %edx
<= 하나의 명령어가 하나의 operation으로addl %eas, 4(%edx)
<= 하나의 명령어가 3개의 operation으로위의 정보들을 바탕으로 4 CPE를 계산해본다.
4개의 instruction은 5개의 u-operation들로 쪼개져 수행된다.
(추가로 더하기 연산시에는 CPE가 2가 된다.)
(예전 Intel CPU 기준 unit의 제약)
자원이 무한힐 떄,