1 . 소프트웨어 최적화
-
소프트웨어 최적화란?
- S/W가 보다 빠르게 실행되거나 자원(메모리)를 적게 사용하도록 만들기 위해서 S/W를 변경하는 것이다.
-
하드웨어와 S/W 성능 향상의 관계
- “무어의 법칙”에 따르면 하드웨어의 성능 약 2년마다 2배씩 증가한다.
- 같은 하드웨어에서 실행하더라도 보다 빠르게 실행될 수 있는 소프트웨어가 필요하다.
- 하드웨어 성능 향상에 비례해서 성능이 향상될 수 있도록 소프트웨어를 개발해야 한다
. → 확장성(scalability)
-
최적화의 레벨
1) 디자인 레벨 : 적절한 알고리즘의 선택이 S/W 성능에 가장 크게 영향을 준다.
2) 소스 코드 레벨 : 성능 저하를 일으키는 코딩 기법을 피하고, 컴파일러가 최적화하기 좋은 스타일의 코딩 기법을 사용한다.
3) 컴파일 레벨 : 최적화 컴파일러를 사용함으로써 성능을 향상 시킨다.
4) 어셈블리 레벨 : 특정 하드웨어에서만 사용되는 어셈블리어로 코딩함으로써 성능을 최적화한다.
-
최적화 요소 간의 트레이드 오프
- 성능 최적화의 요소 : 실행시간 , 메모리 사용량 등등 …
- 최적화의 한가지 요소를 충족시키면, 상대적으로 다른 요소가 희생됨.
→ 실행 시간을 최소화하기 위해 추가적인 메모리를 사용하는 경우
→ 코드 최적화를 해서 실행 시간은 감소되었지만 코드의 가독성이 줄어들어 유지보수가 어려워지는 경우
=> 가독성(코드가쉽다 : 유지보수에 좋음) ↔ 성능 최적화
-
최적화 시기
1) 보통 S/W 개발의 마지막 단계에서 성능 최적화를 수행한다.
- 최적화를 위해서 추가된 코드는 코드를 읽기 어렵게 만들기 때문에 S/W의 버그 를 찾거나 수정하기가 어렵기 때문이다.
- 코드를 읽기 어럽게 만드는 소스 코드 레벨의 최적화는 나중으로 미루는 것이 더 낫다.
-
Bottleneck , hotspot
- 프로그램이 실행 시간의 대부분을 소요하는 곳을 bottleneck 또는 hotspot 이라고 한다.
- hotspot의 중요성
→ 프로그램 수행 시간에 중요한 영향을 미치는 코드블록을 최적하는데 노력을 집중해야 한다.
→ S/W 최적화를 위해서는 프로그램에 대한 정확한 성능 평가가 우선적으로 필요하다
→ hotspot를 찾아야 한다.
2. 최적화시 고려사항 (프로세서 구조)
* 성능 향상을 위한 CPU 구조
(1) 파이프라인 구조
- CPU 명령어 수행 시 명령어 수행의 각 단계를 파이프라인으로 처리함으로써 명령어 처리량을 최대화할 수 있다.
- 문제점 : 명령어 간의 데이터 종속성이 있는 경우
분기명령어에 의해 파이프라인이 멈추는 경우 (분기화를 최소화 해야됨.)
-
파이프라인을 사용하지 않는 경우
-
파이프라인을 사용하는 경우 ( 레지스터를 바로 바로 읽어옴)
(2) 슈퍼스칼라 구조
- 한 스레드내의 여러 명령어를 동시에 처리하는 구조
→ 여러 명령어들이 대기 상태를 거치지 않고 동시에 실행 될 수 있음.
(3) Hyper – Threading 구조 (멀티쓰레딩)
(4) 메모리 계층 구조
- 컴퓨터의 시스템은 속도/가격 때문에 다양한 계층 구조의 메모리를 사용한다.
→ CPU 레지스터 → 1차 캐시 → 2차 캐시 → 주기억장치 → 보조기억장치
순으로 속도는 느려지고, 가격은 저렴해진다.
(5) SIMD 명렁어 – vectorization
-
큰 데이터 집합에 대하여 같은 연산을 반복적으로 수행해야 하는 경우 여러 데이터 를 패킹한 다음 각 데이터 부분에 대한 연산을 동시에 수행한다.
-
간단한 성능 평가
- CPU클럭을 측정하는 clock() 함수 이용
- 가장 정확하고 오버헤드가 적다.
time_t startTime, End Time;
startTime = clock();
/ *[ ….
…. ] //-> 성능을 측정하는 구간 */
EndTime = clock() – startTime;
-
분기 최적화
(1) 분기 예측
- 다음에 수행될 명령어를 결정하기 위해서 프로세서는 분기 예측을 수행한다.
- 예측 가능한 패턴이 있으면 프로세서는 패턴을 인식하고 올바른 분기 예측을 할 수 있다.
- 윈도우 메시지 루프처러 분기 예측이 힘든 경우에도 분기 예측이 필요하다.
→ 분기 예측을 하지 않는다면 프로세서는 분기 명령어를 만날 때마다 일단 실행을 멈추고 분기 명령어 수행을 위한 명령어 파이프라인을 모두 완료될 때까지 여러 사이클을 기다려야 한다.
-
코드를 일부 수정해서 보다 예측하기 쉬운 분기를 만든다.
-
조건문의 순서를 바꾸거나 조건문을 수정해서 분기 예측을 향상시킬 수 있다.
if( t1| t2| t3 ) == 0 ) → 조건문을 하나로 만들었기 때문에 분기 예측이 더 쉬워진다.
(2) 분기 최적화
- else if문 보다는 switch 문을 사용하는 것이 더 좋다.
→ if문은 다 걸쳐서 진행 되지만, switch문은 해당 조건에만 진행됨
- 논리 연산자의 lazy evaluation
1) if( a&&b)
→ a가 거짓인 경우 b를 테스트 하지 않는다.
→ 거짓이 될 확률이 높은 조건을 먼저 지정한다.
→ a, b 확률이 비슷하면 검사하기 쉬운 조건을 먼저 지정한다.
2) if( a|| b)
→ a가 참인 경우 b를 테스트 하지 않는다.
→ 참이 될 확률이 높은 조건을 먼저 지정한다.
→ a,b 확률이 비슷하면 검사하기 쉬운 조건을 먼저 지정한다.
3) 산술 연산자의 수행 속도
- * / 연산보다는 << >> - + 연산이 더 빠르다
-
루프 최적화
(1) 루프와 성능 최적화
- 소프트웨어의 성능 평가 결과로 찾은 hotspot이 루프일 가능성이 높다.
→ 프로그램이 실행 시간의 대부분을 소요하는 곳을 hotspot 이라고 한다.
- 루프가 무조건 hotspot인 것은 아니다.
→ 루프를 사용하면 프로그램의 크기가 감소하므로 메모리 사용량이 줄어서 프로그램 실행 속도가 향상 될 수 있으며, 컴파일러가 최적화를 수행할 가능성이 높아진다.
→ 루프 구조에 의해 데이터 종속성이 발생해서 속도가 저하될 수도 있다.
(2) 루프 변형
- 대부분의 컴파일러가 최저화를 위해서 루프변형을 수행한다
- 데이터 종속성(data dependency)이 루프 변형에 중요한 영향을 미치므로 먼저 데이터 종속성을 파악해야 한다.
(3) 데이터의 종속성 (data dependency)
- 루프의 특정 iteration에서는 변수의 값을 변경하고, 다른 iteration에서는 변수의 값을 읽으려고 드는 경우를 말한다.
→ 루프의 i번째와 i+1 번째를 동시에 수행할 수 없다.
1 )데이터 종속성을 제거할 수 없는 경우
2) 데이터의 종속성을 제거할 수 있는 경우
(4) 루프 변형 (loop transformation)
1) loop distribution ( 또는 loop fisiion)
- 하나의 루프를 여러 개의 루프로 나눈 것이다.
2) loop peeling
- 루프의 특정 iteration을 루프 밖에 별도의 코드로 꺼내느 것을 말한다.
3) loop unrolling
- 루프의 iteration들을 펼쳐서 루프 반복 횟수를 줄이는 것을 말한다.
- 루프의 iteration 횟수는 많지만 각 iteration에서 처리할 문장은 적을 때 사용한다.
- 직접 loop unrolling 을 하는 것보다 SIMD 명령어를 사용하는 것이 더 좋다.
4) loop interchanging
- 루프 안에 중첩된 루프의 순서를 바꾸는 것이다.
→ 데이터의 지역성(locality)를 높임으로써 cache miss 를 줄일 수 있고, 결과적으로 메모리 접근 속도를 높일 수 있다.
5) loop invariant computation
- 루프 불변식이란 루프의 각 iteration에서 변경되지 않는 연산식을 말한다.
6) loop invariant branch
- 루프 내의 분기문을 제거하는 것을 말한다
7) loop invariant result
- 읽기 전용으로만 사용되는 배열을 초기화하기 위해서 루프를 사용하는 경우에는 미리 계산된 결과를 구해서 배열을 초기화하고 더 이상 필요없는 루프를 제거한다.
C++ 최적화 방법
◈ 생성자와 소멸자
-
클래스 이름과 같은 이름의 함수
-
객체를 초기화
-
생성자는 인자를 가질 수 있다. 오버로딩 가능
-
클래스 이름과 같은 이름의 함수 : ~클래스이름()
-
객체를 정리
-
소멸자는 인자를 가질 수 없다. 오버로딩 불가
(1) 생성자(Constructor) : 객체가 생성될 때 자동으로 호출되는 함수
(2) 소멸자(Destructor) : 객체가 소멸될 때 자동으로 호출되는 함수
void f() {
string s1; // s1의 생성자 호출
string s2(“abc”); // s2의 생성자 호출
string s3(100, ‘-‘); // s3의 생성자 호출
…
} // s3, s2, s1의 소멸자 호출
◈ 정적 멤버 변수
- 객체마다 생성되지 않고 클래스 전체에 대해서 한번만 만들어지는 변수
- 같은 클래스의 객체들 사이에 공유되는 변수
- “객체의 소유”가 아니라 “클래스의 소유”인 변수
- 객체 생성 시 메모리에 할당되지 않으므로 별도로 할당해야 한다. 전역변수처럼 메모리 할당(프로- 그램 시작 시 할당되고,
프로그램 종료 시 해제)
◈ 문자열처리
-
프로그램 실행 중에 변경되지 않는 문자열을 const char* 형을 사용한다.
const char* errorMessage = “잘못 입력하셨습니다.”;
-
자주 변경되거나 문자열의 길이가 정해지지 않은 경우에는 문자열 클래스를 사용한다.
-
자주 변경되지 않거나 문자열의 길이가 정해져 있고, 다수의 문자열이 필요한 경우
◈ 조용한 실행(silent execution)
- 생성자 : 객체 생성 시 따로 지정하지 않으면(인자 없이 이름만으로 객체를 생성하면) 디폴트 생성자(인자 없는 생성자)를 호출한다.
string s1(“abc”); // string(“abc”)를 호출
string s2; // 디폴트 생성자 호출
-
소멸자 : 객체가 소멸될 때 호출된다.
-
복사 생성자(Copy Constructor) : 같은 클래스의 객체로 초기할 때 호출된다.
string s1(“abc”);
string s2 = s1; // string s2(s1);의 의미 -> 생성자의 인자로 s1 전달 -> string(s1) 호출
-
대입 연산자(Assignment Operator) : 같은 클래스의 객체를 대입할 때 호출된다.
string s1(“abc”)’;
string s2; // string() 호출
s2 = s1; // s2.operator=(s1);으로 처리 연산자 함수 operator=(s1) 호출
※ 객체가 생성된 다음에 값을 변경하는 것보다, 객체가 생성될 때부터 특정 값으로 초기화하는 것이 더 효율적이다.
일반 변수 |
객체 | ||
int a; a = 10; |
성능 상 차이가 없다! |
string s; s = “abc”; |
string() 호출 operator=(“abc”) 호출 |
int a = 10; |
string s(“abc”); |
string(“abc”) 호출 |
◈ 객체의 생성과 소멸
-
객체의 생성 : 메모리 할당 + 생성자(constructor) 호출
-
객체를 메모리에 할당할 때는 객체가 가진 멤버 변수들을 선언된 순서대로 할당한다.
-
객체의 멤버변수(멤버객체)가 메모리에 할당될 때 초기화를 하려면
“초기화 리스트”를 이용한다.
-
파생 클래스의 객체를 메모리에 할당할 때는 기본 클래스로부터 상속받은 멤버변수들이 먼저 메모리에 할당되고, 그 다음 파생 클래스에 추가된 멤버변수들이 메모리에 할당된다.
기본 클래스 생성자에 인자를 전달하려면 “초기화 리스트”를 이용한다.
-
객체의 소멸 : 소멸자(destructor) 호출 + 메모리 해제
-
소멸자 호출 순서는 생성자 호출 순서의 역순이다.
-
포함의 경우 Outer 객체의 소멸자가 호출된 다음 Inner 객체의 소멸자가 호출된다.
-
상속의 경우 파생 클래스 소멸자가 호출된 다음 기본 클래스 소멸자가 호출된다.
-
◈ 연관 관계(Association)
-
집합체(Aggregation) : 전체-부분의 관계
멤버 객체의 생성과 소멸이 항상 자신을 포함하는 클래스의 객체와 함께 이루어진다.
class Line {
protected:
Point _ptStart, __ptEnd;
};
-
합성(Composition) : Outer 객체가 Inner 객체를 직접 필요한 시점에 생성하고, 해제할 수 있다.
class Trace {
private:
string *theFunctionName;
public:
Trace(const char* s) : theFunctionName(0)
{
if( traceIsActive )
theFunctionName = new string(s);
}
};
◈ 불필요한 생성자/소멸자 호출을 최소화하기 위한 가이드라인
-
상속 : 상속의 계층 구조에서 불필요한 클래스를 제거한다.
-
포함 : 집합체(Aggregation)과 합성(Composition)을 구별해서 사용한다.
Inner 객체가 Outer 객체에서 항상 필요한 것이 아니라면 필요할 때 생성하도록 합성으로 구현한다.
◈ 객체 관리 기법
-
객체 배열은 사용하지 않는 것이 좋다!!!
Point arr[10000]; 메모리 낭비 sizeof(Point)*10000바이트 할당
+ 생성자 10000번 호출
-
객체에 대한 포인터 배열
Point *arr[10000]; 메모리 낭비 sizeof(Point*)*10000바이트 할당
+ 생성자는 호출되지 않음!!!
-
라이브러리 클래스를 이용한다.(STL 컨테이너나 MFC 컬렉션 클래스 이용)
-
종류
STL 컨테이너
MFC 컬렉션
특징
가변크기 배열
vector
CArray
Random access가 빠르다.
시작위치에서의 원소 삽입/ 삭제는 느리다.
연결리스트
list
CList
Random access가 느리다.
원소 삽입/삭제는 빠르다.
맵
map
CMap
검색이 빠르다.
set, multiset
- 직접 객체를 원소(element)로 저장하는 경우
→ 객체를 복사해서 저장
→ 객체의 크기가 비교적 작은 경우에 사용 (상속을 사용하지 않고, 기본형의 멤버 변수만 갖는 경우)
→ 단순 데이터형 클래스의 객체를 저장할 때 사용
vector<Point> arr;
Point p1;
arr.push_back(p1);
Point p2;
arr.push_back(p2);
for(int i = 0 ; i < arr.size() ; ++i)
arr[i].Move(i, i);
vector<Point>::iterator it;
for(it = arr.begin() ; it != arr.end() ; ++it)
(*it).Move(i, i);
- 객체를 동적으로 생성하고 그 주소만 원소(element)로 저장하는 경우
- 불필요한 객체 복사를 막을 수 있으므로 효율적
- 객체의 크기가 비교적 큰 경우에 사용
- 상속을 받는 클래스나 클래스에 멤버 객체가 여러 개 사용되는 경우
vector<Point*> arr;
arr.push_back(new Point);
arr.push_back(new Point);
arr.push_back(new Point);
vector<Point>::iterator it;
for(it = arr.begin() ; it != arr.end() ; ++it)
(*it)->Move(i, i);
for(it = arr.begin() ; it != arr.end() ; ++it)
delete (*it);
arr.clear();
◈ 다형성을 위한 C++ 기능
-
클래스 형 변환 규칙
-
파생클래스의 포인터/레퍼런스는 기본클래스의 포인터/레퍼런스로 형변환 가능하다.
-
기본클래스의 포인터/레퍼런스는 파생클래스 객체를 가리킬/참조할 수 있다.
-
파생클래스 객체를 기본클래스 객체인 것처럼 사용한다는 의미
-
-
가상함수
-
기본클래스 포인터/레퍼런스로 호출되더라도 파생클래스에 재정의된 함수가 호출될 수 있다.
-
기본클래스 포인터/레퍼런스가 가리키는 객체의 형에 따라서 호출될 함수가 실행시간에 결정된다.
-
◈ 가상함수 매커니즘
-
가상함수는 동적 바인딩을 사용한다. 실행 시간에 호출될 함수를 결정한다.
-
가상 함수를 가진 클래스마다 가상함수테이블(vTable)이라는 함수 포인터 배열이 만들어진다.
-
가상 함수를 가진 클래스의 객체를 생성하면 객체마다 가상함수테이블포인터(vptr)가 숨겨진 멤버 변수로 만들어진다.
-
파생 클래스의 vTable의 기본클래스의 vTable을 상속받아서 수정, 확장해서 만들어진다.
◈ 가상함수가 성능에 영향을 미치는 요소
-
객체 크기가 vptr 크기만큼 커진다.
-
객체의 생성자에서 vptr 초기화를 수행한다.
☞ VC++의 __declspec(novtable)을 사용하면 생성자에서 vptr 초기화를 막을 수 있다.
-
가상함수는 실행 중에 호출될 함수를 결정하며, 이 때 vptr을 참조한다.
-
클래스의 모든 멤버 함수를 가상함수로 만드는 대신 다형성이 필요한 경우에만 가상함수로 선언한다.
-
가상함수를 사용하는 클래스의 소멸자도 가상함수로 선언해야 하는데, 클래스에 다른 가상함수가 없을 때는 소멸자를 가상함수로 선언하지 않는다.
-
가상함수는 인라인화되지 않는다. 최적화를 저해하는 요인이 된다.
◈ 클래스의 객체를 함수의 인자로 사용하는 경우
값에 의한 전달(Call by value) 객체를 복사해서 전달 (복사 생성자 호출) |
void f(string s) { } int main() { string s1; f(s1); // 함수 호출 시 string s = s1; 객체간의 복사 } |
포인터에 의한 전달 (Call by pointer) 객체를 복사하지 않고, 객체의 주소을 전달 |
void f(string *p) { } p가 가리키는 객체 변경 가능 int main() { string s1; f(&s1); // 함수 호출 시 string *p = &s1; p는 s1의 주소 } |
레퍼런스에 의한 전달 (Call by reference) 객체를 복사하지 않고, 객체의 별명을 전달 |
void f(string &s) { } s가 참조하는 객체 변경 가능 int main() { string s1; f(s1); // 함수 호출 시 string &s = s1; s는 s1의 별명 } |
const 포인터, const 레퍼런스 입력인자로 객체를 전달할 때 사용 |
void f(const string *p) { } p가 가리키는 객체 변경 불가 void f(const string &s) { } s가 참조하는 객체 변경 불가 |
※ 함수 안에서 변경되지 않는 인자를 전달할 때는 const 포인터, const 레퍼런스로 전달한다.
◈ 클래스의 객체를 리턴하는 경우
값으로 리턴 복사해서 리턴 (복사생성자 호출) |
string Test() { string retVal; … return retVal; retVal를 복사해서 리턴 } int main() { cout << Test(); } | |
포인터나 레퍼런스를 리턴 복사하지 않고 리턴 |
지역객체의 주소나 별명을 리턴해서는 안된다!!!! | |
string& Test() { string retVal; …. return retVal; } int main(){ string& s = Test(); 문제발생!!! } |
string* Test(){ string retVal; …. return &retVal; } int main(){ string* p = Test(); 문제발생!!! } | |
포인터나 레퍼런스를 리턴하는 함수는 함수가 리턴해도 사라지지 않는 객체의 주소나 별명을 리턴해야 한다. 해결 방법 (1) static 지역 변수(전역 변수) 사용 멀티스레드에서는 동기화 문제 유발 string& Test() { static string retVal; …. return retVal; } (2) 동적 메모리 사용 함수 호출 측(caller)에서 동적 메모리를 해제해야 한다. string* Test() { string *retVal = new string; …. return retVal; } (3) 출력인자를 사용해서 처리(함수 호출 측이 결과를 받아올 변수/객체 준비) void Test(string& retval) { // retval 변경 } int main() { string t; Test(t); } |
◈ 임시 객체의 활용
-
함수의 인자를 전달하거나 리턴값을 리턴할 때 임시 객체를 사용하면 “생성자 호출 최적화”에 의해 불필요한 객체 생성을 막을 수 있다.
void SomeFunc(const string& s);
void AnotherFunc(string s);
int main() {
SomeFunc( string(“abc”) ); // const string& s = string(“abc”);
임시 객체가 함수 호출되는 동안 살아있음
AnotherFunc( string(“def”) ); // string s = string(“def”);
임시객체를 생성해서 복사하는 대신 s를 직접 초기화
}
-
임시객체를 이용해서 클래스형으로의 형변환을 수행할 수 있다. (변환 생성자)
string s = “abc”; // string s = string(“abc”);로 처리
☞ 생성자를 암시적인 형변환에 이용하지 않으려면 explicit 키워드를 지정한다.
명시적인 형변환만 가능하다!!!
◈ C++에서 형변환이 일어나는 경우
-
서로 다른 값을 혼합 연산할 때
Complex c1(1, 1);
Complex c2 = c1 + 12.34; // Complex c2 = c1 + Complex(12.34);
c2 = 12.34; // c2 = Complex(12.34);
-
함수의 인자를 전달하거나 리턴값을 리턴할 때
void SomeFunc(const Complex& c);
SomeFunc(12.34); // SomeFunc( Complex(12.34) );
-
형변환 연산자를 명시적으로 사용할 때 변환 생성자를 사용
c2 = Complex(12.34);
◈ 임시객체가 불필요하게 생성되는 경우
임시객체가 생성되는 경우 |
임시객체 생성을 막는 방법 |
연산의 결과로 임시객체가 생성되는 경우 string s1(“abc”), s2(“def”), s3(“ghi”), s4; s4 = s1 + s2 + s3; 연산자 함수 3번 호출 + 임시객체 2개 생성(생성자2번/소멸자2번 호출) |
복합 대입 연산자를 대신 이용한다. s4 = s1; s4 += s2; s4 += s3; 연산자 함수 3번 호출 |
함수의 인자를 전달할 때 형변환이 필요해서 임시객체가 생성되는 경우 class Complex { public: Complex operator+(const Complex& c) const; }; Complex c1, c2; c2 = c1 + 12.34; // c2 = c1 + Complex(12.34); |
형이 정확히 일치하는 함수를 오버로드해서 임시객체를 이용한 형변환이 일어나지 않게 막는다. class Complex { public: Complex operator+(const Complex& c) const; Complex operator+(double d) const; }; |
증감연산자의 후위형을 사용할 때 class Complex { public: Complex operator++(int) const { return Complex(_real++, _imag++); } }; Complex c1; c1++; // ++ 연산의 결과로 임시객체 생성 |
전위형 증감연산자를 대신 사용한다. class Complex { public: Complex& operator++() const { ++_real; ++imag; return *this; } }; Complex c1; ++c1; // 임시객체 생성 안됨!!! |
◈ C++의 형변환 연산자
(type) value 또는 type(value) |
C의 형변환 연산자 형변환의 의도를 파악하기 힘들다. |
const_cast<type>(value) |
const 속성 제거 const string* s = new string(“ABC”); ((string*)s)->append(…); 또는 const_cast<string*>(s)->append(…); |
static_cast<type>(value) |
형변환 가능한지를 컴파일시 검사한다. 형변환할수 없으면 컴파일 에러 |
dynamic_cast<type>(value) |
형변환 가능한지를 실행시 검사한다. 다운캐스트에 사용 |
reinterpret_cast<type>(value) |
강제형변환 char data[4]; int *p = (int*) data; 또는 int *p = reinterpret_cast<int *>(data); |
◈ 윈도우 시스템 오브젝트(Windows system object)
커널 오브젝트 커널 모듈에서 사용 (Kernel32.dll) |
프로세스, 스레드, 메모리, 파일 매핑 객체 관리 “커널 오브젝트” HANDLE 형 사용 프로세스 한정적 핸들 값 사용(프로세스마다 같은 커널 오브젝트에 대해서 다른 핸들 값을 사용) 여러 프로세스에 의해 공유될 수 있으므로 reference counting으로 관리한다. (커널 오브젝트를 사용하는 프로세스마다 커널 오브젝트의 사용이 끝나면 CloseHandle을 해야 한다.) 커널 오브젝트를 생성할 때는 인자로 SECURITY_ATTRIBUTES를 전달한다. |
유저 오브젝트 유저 모듈에서 사용 (User32.dll) |
윈도우, 액셀러레이터, 커서, 아이콘 관리 HWND, HACCEL, HCURSOR, HICON 등 사용 시스템 전역적인 핸들 값 사용 |
GDI 오브젝트 GDI 모듈에서 사용 (Gdi32.dll) |
출력에 대한 기능 제공. DC나 GDI 객체인 펜, 브러시, 폰트) 관리 HDC, HPEN, HBRUSH, HFONT 등 사용 프로세스 지역적 핸들 값 사용 |
◈ 동기화 방법
|
특징 |
API 함수 |
MFC 클래스 |
원자적 연산 |
가장 빠르다. |
InterlockedIncrement, InterlockedDecrement, InterlockedExchange, InterlockedExchangeAdd |
|
크리티컬 섹션 |
커널 객체가 아니므로 프로세스 사이에 공유될 수 없다. 오버헤드가 적다. |
CRITICAL_SECTION 구조체 InitializeCriticalSection EnterCriticalSection LeaveCriticalSection |
CCriticalSection 클래스 Lock/Unlock 멤버함수 |
이벤트 |
스레드 간의 타이밍 조절 |
CreateEvent OpenEvent SetEvent ResetEvent |
CEvent 클래스 Lock/Unlock 멤버함수 |
세마포어 |
카운터 개념이 있어서 여러 스레드가 동시에 공유자원에 접근할 수 있다. |
CreateSemaphore OpenSemaphore ReleaseSemaphore |
CSemaphore 클래스 Lock/Unlock 멤버함수 |
뮤텍스 |
커널 객체이므로 여러 프로세스에서 공유될 수 있다. |
CreateMutex OpenMutex ReleaseMutex |
CMutex 클래스 Lock/Unlock 멤버함수 |
대기 함수 |
|
WaitForSingleObject WaitForMultipleObjects |
CSingleLock 클래스 CMultiLock 클래스 Lock/Unlock 멤버함수 |
◈ OpenMP
-
멀티 쓰레드 프로그래밍을 간단하게 하기 위해서 개발된 기법
-
OpenMP는 컴파일러 지시자만으로서 블록을 멀티 쓰레드로 작동하게 할 수 있음.
-
다양한 Platform에서 비전문가가 병렬 수행이 가능한 application을 개발할 수 있도록 해주기 위한 compiler directive, library function, environment variable 의 집합
-
개발 환경 설정
- 프로젝트속성 → 구성 속성 → 언어 → OpenMP 지원을 “예(/openmp)”
- #include <omp.h> 추가
-
OpenMp 구성
- #pragma omp directive_name [clause[ [,] clause]...]
-
간단한 예
directive_name : parallel, for, parallel for, sectin 등등..
clasue: if, num_threads , private, reduction 등등..
#prgma omp parallel for
for(int i=0; i<200000000; i++)
{
pi += 4*(i%2 ? -1 :1)/(2.0 *i +1.0);
}
- 위의 같은 경우 반복 횟수가 200000000 회 정도 되는데, Thread가 4개라면, 200000000/4회 정도로 나누어 수행된다.즉 Thread 별로 0 ~ 50000000, 50000000 ~ 100000000, 100000000 ~ 150000000, 150000000 ~200000000 이렇게 나누어 수행하게 된다.
< OpenMP에 대한 자세한 내용은 http://seolis.tistory.com/category/programming/OpenMP 여기 참조하세요>