Public/tip & tech

프로그래밍 성능 향상을 위한 C/C++ 튜닝

quantapia 2013. 6. 4. 20:35

1 . 소프트웨어 최적화

  1. 소프트웨어 최적화란?

    - S/W가 보다 빠르게 실행되거나 자원(메모리)를 적게 사용하도록 만들기 위해서 S/W를 변경하는 것이다.


  1. 하드웨어와 S/W 성능 향상의 관계

    - “무어의 법칙”에 따르면 하드웨어의 성능 약 2년마다 2배씩 증가한다.

    - 같은 하드웨어에서 실행하더라도 보다 빠르게 실행될 수 있는 소프트웨어가 필요하다.

    - 하드웨어 성능 향상에 비례해서 성능이 향상될 수 있도록 소프트웨어를 개발해야 한다

    . 확장성(scalability)


  1. 최적화의 레벨

    1) 디자인 레벨 : 적절한 알고리즘의 선택이 S/W 성능에 가장 크게 영향을 준다.

    2) 소스 코드 레벨 : 성능 저하를 일으키는 코딩 기법을 피하고, 컴파일러가 최적화하기 좋은 스타일의 코딩 기법을 사용한다.

    3) 컴파일 레벨 : 최적화 컴파일러를 사용함으로써 성능을 향상 시킨다.

    4) 어셈블리 레벨 : 특정 하드웨어에서만 사용되는 어셈블리어로 코딩함으로써 성능을 최적화한다.


  1. 최적화 요소 간의 트레이드 오프

    - 성능 최적화의 요소 : 실행시간 , 메모리 사용량 등등 …

    - 최적화의 한가지 요소를 충족시키면, 상대적으로 다른 요소가 희생됨.

    실행 시간을 최소화하기 위해 추가적인 메모리를 사용하는 경우

    코드 최적화를 해서 실행 시간은 감소되었지만 코드의 가독성이 줄어들어 유지보수가 어려워지는 경우

=> 가독성(코드가쉽다 : 유지보수에 좋음) ↔ 성능 최적화

  1. 최적화 시기

    1) 보통 S/W 개발의 마지막 단계에서 성능 최적화를 수행한다.

    - 최적화를 위해서 추가된 코드는 코드를 읽기 어렵게 만들기 때문에 S/W의 버그 를 찾거나 수정하기가 어렵기 때문이다.

    - 코드를 읽기 어럽게 만드는 소스 코드 레벨의 최적화는 나중으로 미루는 것이 더 낫다.

  2. Bottleneck , hotspot

    - 프로그램이 실행 시간의 대부분을 소요하는 곳을 bottleneck 또는 hotspot 이라고 한다.

    - hotspot의 중요성

    프로그램 수행 시간에 중요한 영향을 미치는 코드블록을 최적하는데 노력을 집중해야 한다.

    S/W 최적화를 위해서는 프로그램에 대한 정확한 성능 평가가 우선적으로 필요하다

    hotspot를 찾아야 한다.


 










2.
최적화시 고려사항 (프로세서 구조)

    * 성능 향상을 위한 CPU 구조


(1) 파이프라인 구조

    - CPU 명령어 수행 시 명령어 수행의 각 단계를 파이프라인으로 처리함으로써 명령어 처리량을 최대화할 수 있다.

    - 문제점 : 명령어 간의 데이터 종속성이 있는 경우

    분기명령어에 의해 파이프라인이 멈추는 경우 (분기화를 최소화 해야됨.)

  • 파이프라인을 사용하지 않는 경우

  • 파이프라인을 사용하는 경우 ( 레지스터를 바로 바로 읽어옴)


 

    (2) 슈퍼스칼라 구조

    - 한 스레드내의 여러 명령어를 동시에 처리하는 구조

    여러 명령어들이 대기 상태를 거치지 않고 동시에 실행 될 수 있음.

    (3) Hyper – Threading 구조 (멀티쓰레딩)


 

    (4) 메모리 계층 구조

    - 컴퓨터의 시스템은 속도/가격 때문에 다양한 계층 구조의 메모리를 사용한다.

    CPU 레지스터 → 1차 캐시 → 2차 캐시 → 주기억장치 → 보조기억장치

    순으로 속도는 느려지고, 가격은 저렴해진다.



    (5) SIMD 명렁어 – vectorization

    • 큰 데이터 집합에 대하여 같은 연산을 반복적으로 수행해야 하는 경우 여러 데이터 를 패킹한 다음 각 데이터 부분에 대한 연산을 동시에 수행한다.

  1. 간단한 성능 평가

    - CPU클럭을 측정하는 clock() 함수 이용

    - 가장 정확하고 오버헤드가 적다.

#include <time.h>

time_t startTime, End Time;

startTime = clock();

/ *[ ….

…. ] //-> 성능을 측정하는 구간 */

EndTime = clock() – startTime;


 

  1. 분기 최적화

    (1) 분기 예측

    - 다음에 수행될 명령어를 결정하기 위해서 프로세서는 분기 예측을 수행한다.

    - 예측 가능한 패턴이 있으면 프로세서는 패턴을 인식하고 올바른 분기 예측을 할 수 있다.

    - 윈도우 메시지 루프처러 분기 예측이 힘든 경우에도 분기 예측이 필요하다.

    분기 예측을 하지 않는다면 프로세서는 분기 명령어를 만날 때마다 일단 실행을 멈추고 분기 명령어 수행을 위한 명령어 파이프라인을 모두 완료될 때까지 여러 사이클을 기다려야 한다.

      • 코드를 일부 수정해서 보다 예측하기 쉬운 분기를 만든다.

      • 조건문의 순서를 바꾸거나 조건문을 수정해서 분기 예측을 향상시킬 수 있다.


If( t1 = 0 && t2 ==0 && t3 ==0)

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. 루프 최적화

    (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의 소멸자 호출


정적 멤버 변수

    - 객체마다 생성되지 않고 클래스 전체에 대해서 한번만 만들어지는 변수

    - 같은 클래스의 객체들 사이에 공유되는 변수

    - “객체의 소유”가 아니라 “클래스의 소유”인 변수

    - 객체 생성 시 메모리에 할당되지 않으므로 별도로 할당해야 한다.  전역변수처럼 메모리 할당(프로- 그램 시작 시 할당되고,
    프로그램 종료 시 해제)

    문자열처리

  1. 프로그램 실행 중에 변경되지 않는 문자열을 const char* 형을 사용한다.

const char* errorMessage = “잘못 입력하셨습니다.”;

  1. 자주 변경되거나 문자열의 길이가 정해지지 않은 경우에는 문자열 클래스를 사용한다.

  2. 자주 변경되지 않거나 문자열의 길이가 정해져 있고, 다수의 문자열이 필요한 경우


조용한 실행(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”) 호출


객체의 생성과 소멸

  1. 객체의 생성 : 메모리 할당 + 생성자(constructor) 호출

    1. 객체를 메모리에 할당할 때는 객체가 가진 멤버 변수들을 선언된 순서대로 할당한다.

객체의 멤버변수(멤버객체)가 메모리에 할당될 때 초기화를 하려면

초기화 리스트”를 이용한다.

    1. 파생 클래스의 객체를 메모리에 할당할 때는 기본 클래스로부터 상속받은 멤버변수들이 먼저 메모리에 할당되고, 그 다음 파생 클래스에 추가된 멤버변수들이 메모리에 할당된다.

기본 클래스 생성자에 인자를 전달하려면 “초기화 리스트”를 이용한다.

  1. 객체의 소멸 : 소멸자(destructor) 호출 + 메모리 해제

    1. 소멸자 호출 순서는 생성자 호출 순서의 역순이다.

    2. 포함의 경우 Outer 객체의 소멸자가 호출된 다음 Inner 객체의 소멸자가 호출된다.

    3. 상속의 경우 파생 클래스 소멸자가 호출된 다음 기본 클래스 소멸자가 호출된다.


연관 관계(Association)

  1. 집합체(Aggregation) : 전체-부분의 관계

멤버 객체의 생성과 소멸이 항상 자신을 포함하는 클래스의 객체와 함께 이루어진다.

class Line {

protected:

Point _ptStart, __ptEnd;

};



  1. 합성(Composition) : Outer 객체가 Inner 객체를 직접 필요한 시점에 생성하고, 해제할 수 있다.

class Trace {

private:

string *theFunctionName;

public:

Trace(const char* s) : theFunctionName(0)

{

if( traceIsActive )

theFunctionName = new string(s);

}

};


불필요한 생성자/소멸자 호출을 최소화하기 위한 가이드라인

  1. 상속 : 상속의 계층 구조에서 불필요한 클래스를 제거한다.

  2. 포함 : 집합체(Aggregation)과 합성(Composition)을 구별해서 사용한다.

    Inner 객체가 Outer 객체에서 항상 필요한 것이 아니라면 필요할 때 생성하도록 합성으로 구현한다.


객체 관리 기법

  1. 객체 배열은 사용하지 않는 것이 좋다!!!

Point arr[10000]; 메모리 낭비 sizeof(Point)*10000바이트 할당

+ 생성자 10000번 호출

  1. 객체에 대한 포인터 배열

Point *arr[10000]; 메모리 낭비 sizeof(Point*)*10000바이트 할당

+ 생성자는 호출되지 않음!!!

  1. 라이브러리 클래스를 이용한다.(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++ 기능

  1. 클래스 형 변환 규칙

    1. 파생클래스의 포인터/레퍼런스는 기본클래스의 포인터/레퍼런스로 형변환 가능하다.

    2. 기본클래스의 포인터/레퍼런스는 파생클래스 객체를 가리킬/참조할 수 있다.

    3. 파생클래스 객체를 기본클래스 객체인 것처럼 사용한다는 의미

  2. 가상함수

    1. 기본클래스 포인터/레퍼런스로 호출되더라도 파생클래스에 재정의된 함수가 호출될 수 있다.

    2. 기본클래스 포인터/레퍼런스가 가리키는 객체의 형에 따라서 호출될 함수가 실행시간에 결정된다.





가상함수 매커니즘

  • 가상함수는 동적 바인딩을 사용한다.  실행 시간에 호출될 함수를 결정한다.

  • 가상 함수를 가진 클래스마다 가상함수테이블(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;  ps1의 주소

}

레퍼런스에 의한 전달

(Call by reference)

객체를 복사하지 않고,

객체의 별명을 전달

void f(string &s) { }  s가 참조하는 객체 변경 가능

int main() {

string s1;

f(s1); // 함수 호출 시 string &s = s1;  ss1의 별명

}

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);

}


임시 객체의 활용

  1. 함수의 인자를 전달하거나 리턴값을 리턴할 때 임시 객체를 사용하면 “생성자 호출 최적화”에 의해 불필요한 객체 생성을 막을 수 있다.

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를 직접 초기화

}

  1. 임시객체를 이용해서 클래스형으로의 형변환을 수행할 수 있다. (변환 생성자)

string s = “abc”; // string s = string(“abc”);로 처리

생성자를 암시적인 형변환에 이용하지 않으려면 explicit 키워드를 지정한다.

명시적인 형변환만 가능하다!!!


C++에서 형변환이 일어나는 경우

  1. 서로 다른 값을 혼합 연산할 때

Complex c1(1, 1);

Complex c2 = c1 + 12.34; // Complex c2 = c1 + Complex(12.34);

c2 = 12.34; // c2 = Complex(12.34);

  1. 함수의 인자를 전달하거나 리턴값을 리턴할 때

void SomeFunc(const Complex& c);

SomeFunc(12.34); // SomeFunc( Complex(12.34) );

  1. 형변환 연산자를 명시적으로 사용할 때  변환 생성자를 사용

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)

출력에 대한 기능 제공. DCGDI 객체인 펜, 브러시, 폰트) 관리

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 의 집합

  1. 개발 환경 설정

    - 프로젝트속성 → 구성 속성 → 언어 → OpenMP 지원을 “예(/openmp)”

    - #include <omp.h> 추가

  2. OpenMp 구성

    - #pragma omp directive_name [clause[ [,] clause]...]


    directive_name : parallel, for, parallel for, sectin 등등..

    clasue: if, num_threads , private, reduction 등등..

  1. 간단한 예

#prgma omp parallel for

for(int i=0; i<200000000; i++)

{

pi += 4*(i%2 ? -1 :1)/(2.0 *i +1.0);

}


- 위의 같은 경우 반복 횟수가 200000000 회 정도 되는데, Thread4개라면, 200000000/4회 정도로 나누어 수행된다.Thread 별로 0 ~ 50000000, 50000000 ~ 100000000, 100000000 ~ 150000000, 150000000 ~200000000 이렇게 나누어 수행하게 된다.

< OpenMP에 대한 자세한 내용은 http://seolis.tistory.com/category/programming/OpenMP 여기 참조하세요>