QueryPerformance을 이용해 프로그램 실행 속도 측정

컴퓨터 메인보드의 고해상도 타이머를 이용해 시간 간격을 측정한다.

 

 

 

 

현재 실행 속도를 측정하고 싶으면 QueryPerformanceCounter(=QPC)를 이용하면 된다.

하지만 QPC는 외부 시간 참조와 독립적이며 동기화되지 않으므로 현재 시간값을 구하고 싶으면 GetSystemTimePreciseAsFildTime을 이용하라고 MSDN에 나와있다. 

 

QueryPerformanceFrequency와 QueryPerformanceCounter를 이용하면 타이머, FPS 측정 등 여러 방면으로 활용할 수 있다. 

 

 

 

 

◎ QueryPerformanceFrequency

성능 카운터의 빈도를 검색한다. 

 

BOOL QueryPerformanceFrequency(
  LARGE_INTEGER *lpFrequency
);

 

lpFrequency = 현재 성능 카운터, 타이머의 주파수를 반환한다.

 

 

 

 

 

◎ QueryPerformanceCounter

시간 간격 측정에 사용할 수 있는 고해상도 타임 스탬프인 성능 카운터의 현재 값을 검색한다. 그러니까 현재 CPU의 틱을 받아오는 것이다. 

 

BOOL QueryPerformanceCounter(
  LARGE_INTEGER *lpPerformanceCount
);

 

lpPerformanceCount = 매개변수로 현재 성능 카운터 값을 계수로 받는 변수에 대한 포인터를 넘겨준다. 

 

반환 값은 함수가 성공하면 0 이외의 값을 리턴하고 실패하면 0을 리턴한다. 

(Windows XP 이상에서는 이 기능이 항상 성공해서 0을 리턴하는 경우는 없다.)

 

 

 

 

 

◎ LARGE_INTEGER

여기서 쓰이는 LARGE_INTEGER는 뭘까?

QueryPerformanceCounter나 QueryPerformanceFrequency같은 함수를 사용하기 위해선 크기가 큰 정수형이 필요하다. 

왜? 더 자세한 시간값을 저장하기 위해서 

그래서 windows.h에 포함돼 있는 LARGE_INTEGER가 그와 같은 것이다. 

부호가 있는 64비트 정수형 데이터를 저장하기 위해 선언된 사용자 정의 자료형.

 

LARGE_INTEGER는 구조체인데 그 안을 들여다 보면

typedef union _LARGE_INTEGER {
  struct {
    DWORD LowPart;	// 32bit 정수형
    LONG  HighPart;	// 32비트 정수형
  } DUMMYSTRUCTNAME;
  struct {
    DWORD LowPart;
    LONG  HighPart;
  } u;
  LONGLONG QuadPart;	// 64비트 정수형
} LARGE_INTEGER;

이렇게 되어 있다. 

컴파일러가 64비트를 지원할 땐 64비트 정수형 변수에, 32비트 지원 시에는 32비트 정수형 변수에 64비트 측정값을 나눠 저장한다. 

 

값은 64비트의 부호있는 정수형인 QuardPart에 저장되는 것이며 

LowPart는 하위 32비트 DWORD형, HighPart는 상위 32비트 LONG 형이다.

 

64비트 중 LowPart(32bit)와 HighPart(32bit)를 둘다 사용함으로써 더 큰값을 사용할 수 있다. 

 

 

 

 

 

◎ 사용법

#include <windows.h>

int main()
{
	LARGE_INTEGER timer, start, end;
	float DeltaTime;
    
	QueryPerformanceFrequency(&timer); // 타이머의 주파수를 얻어온다. 
   
   
	QueryPerformanceCounter(&start);  // 시작 시점의 CPU 클럭 수
    
    
    // 실행할 내용
    
    
	QueryPerformanceCounter(&end);	// 종료 시점의 CU 클럭 수
    
	DeltaTime = (end.QuadPart - start.QuadPart) / (float)timer.QuadPart; 	// 걸린 시간 계산
 
 }

 

 

 

 

 

 

참고 자료;

 

https://docs.microsoft.com/en-us/windows/win32/api/profileapi/nf-profileapi-queryperformancefrequency

 

QueryPerformanceFrequency function - Win32 apps

Retrieves the frequency of the performance counter.

docs.microsoft.com

 


https://docs.microsoft.com/en-us/windows/win32/api/profileapi/nf-profileapi-queryperformancecounter

 

QueryPerformanceCounter function - Win32 apps

Retrieves the current value of the performance counter, which is a high resolution (<1us) time stamp that can be used for time-interval measurements.

docs.microsoft.com

 

 

'Study > C++ , C#' 카테고리의 다른 글

[C++] 전처리기 지시문  (0) 2020.01.19
[C++] 클래스(Class)와 구조체(Struct)의 차이  (0) 2020.01.15
[C++] 매크로 함수와 인라인 함수(Inline)  (0) 2020.01.10
[C++] new 와 delete  (0) 2020.01.10
[C++] 참조자와 함수  (0) 2020.01.08

 

 

매크로 함수와 인라인(Inline) 함수

in은 '내부', line은 '프로그램 코드라인'을 의미한다.

즉 프로그램 코드라인 안으로 들어가 버린 함수라는 뜻이다.

매크로, 일반 함수, 인라인 함수 모두 결과는 동일하지만 결과를 위한 과정이나 성격은 상당히 다르다! 

 

 

 

 

◎ 매크로 함수

#define 선행 처리 지시문에 인수로 함수의 정의를 전달해서 함수처럼 동작하는 매크로를 만드는 것이다. 

물론 일반 함수와는 달리 단순 치환만을 해주므로, 일반 함수와 완전히 똑같은 방식으로 동작하지는 않는다. 

매크로 함수는 일반적인 함수에 비해 실행 속도에서 이점이 존재하지만 가독성도 떨어질 수 있고 디버깅도 어려우며 복잡한 함수를 매크로의 형태로 정의하는 것에 한계가 존재한다. 

빠른 이유?

더보기

일반 함수처럼 호출된 함수를 위한 스택 메모리의 할당을 할 필요도, 실행위치의 이동과 매개변수로의 인자를 전달할 필요도, return 문에 의한 값의 반환을 할 필요도 없기 때문이다. 

 

 

매크로 함수의 예시.

#define Add(x) ((x)+(x))

int main()
{
	int a = Add(3);
	cout << a << endl;
}

이렇게 실행하면 a의 출력값은 6이 나올 것이다. 

 

 

왜 '전처리 과정'을 거칠까?

더보기

#와 ## 연산자는 선행처리기 연산자이다. 

이 선행처리기 연산자는 전처리기-> 어셈블러로 넘어가기 전에 전처리기가 # 뒤에 붙은 코드들을 처리하기 때문이다. 그래서 전처리 과정에서 처리한다. 

 

 

그리고 이 과정은 

#define Add(x) ((x)+(x))

int main()
{
	int a = Add(3);
	cout << ((3)+(3)) << endl;
}

이런 과정을 거치게 되는데 위와 같이 함수의 몸체 부분이 함수 호출 문장을 완전히 대체했을 때 '함수가 인라인화 됐다'고 표현한다. 

 

 

 

 

◎ 인라인 함수

인라인 함수는 매크로 함수의 장점은 유지하고 단점은 제거했다고 볼 수 있다.

 

또한 인라인 함수는 inline 예약어를 빼면 기존 함수와 다를 바 없고 자료형 등이 지정돼 있어 문법적으로도 완벽한 함수이다. 

매크로를 이용한 함수의 인라인화는 전처리기에 의해 처리되지만 키워드 inline을 이용한 함수의 인라인화는 컴파일러에 의해 처리된다. 

 

inlineint Add(int x)
{
	return x+x;
 }

int main()
{
	int a = Add(3);
	cout << a << endl;
}

 

그러나 매크로 함수의 장점을 인라인 함수가 완벽하게 가져오지 못한 부분도 있다.

예를 들면 위의 매크로 함수의 예시에서 x는 자료형에 의존적이지 않다. 

즉 실수를 넣든, 정수를 넣든 데이터 손실이 발생하지 않는다.

3을 넣으면 6이 1.92를 넣으면 3.84가 나오게 된다. 

하지만 인라인 함수는 자료형 기반으로 정의된 함수이기 때문에 int 자료형 기반에서 1.92를 넣게 되면 0.84의 손실이 발생한다. 

 

물론 함수의 오버로딩을 통해 문제 해결이 가능하지만 간단하게 한 번만 정의하면 된다는 매크로 함수의 장점과는 멀어지게 된다.

그런데 C++에는 이에 대한 해결책이 또 하나 존재한다. 바로 템플릿을 이용하는 것이다. 템플릿을 이용하면 여러 번 정의하지 않아도 한 번에 해결된다!

 

 

 

 

 

◎ 추가

Visual Studio를 통해 컴파일할 경우, 인라인으로 선언하지 않았더라도 적합하면 인라인 함수로 확장하는 설정이 있다.

반대로 인라인으로 선언했다고 해도 적합하지 않다면 일반 함수로 컴파일한다. 

 

기본값은 '인라인 함수로 사용하는 데 적합한 함수는 모두 인라인 함수로 만든다'는 의미이다. 

설정값은 보통 '기본값'으로 지정돼 있다.

 

 

 

 

 

 

 

참고 출처;


http://tcpschool.com/c/c_prepro_macroFunc  (코딩의 시작, TCP School ; 매크로 함수)

 

코딩교육 티씨피스쿨

4차산업혁명, 코딩교육, 소프트웨어교육, 코딩기초, SW코딩, 기초코딩부터 자바 파이썬 등

tcpschool.com


윤성우의 열혈 C 프로그래밍 ( 저; 윤성우, 출판사 ; 오레지 미디어)


이것이 C++이다 ( 저; 최호성, 출판사 ; 한빛미디어 )

 

 

 

 

 

 

new 와 delete 

new는 C언어의 malloc을, delete는 free를 대신하는 키워드이다. 

 

 

 

 

new와 delete연산자는 내부에서 malloc()과 free()함수를 호출한다. 

 

 

굳이 왜 malloc과 free를 쓰지 않고 new와 delete를 사용하는 것일까?

malloc을 이용하여 동적할당을 할 때 할당할 대상의 정보를 무조건 바이트 크기 단위로 전달해야 한다는 것과 반환형이 void형 포인터이기 때문에 적절한 형 변환을 거쳐야 한다는 불편사항이 존재했었다. 

그러나 new는 메모리 크기를 정하지 않는다. 이것이 바로 new를 사용하는 가장 도드라진 특징이다.

 

또한 new와 malloc의 다른 점을 하나 더 뽑으라면 '생성자 함수 호출'이라는 특징이 존재한다. 

new를 통해 객체를 동적으로 생성하면서 객체의 생성자를 호출하기 때문이다. 

 

new와 delete는 단순히 메모리 관리 이상의 일들을 수행한다. 또한 문법적으로 연산자이며 malloc과 free는 객체지향 프로그래밍을 방해하는 원인이 될 수 있다.

 

 

 

 

 

new

자료형* 이름 = new 자료형;

int *testInt = new int;

int *testArray = new int[10];

int **testArray2 = new int*[10]; // 이중배열 동적할당

float *testFloat = new float;

double *testDouble = new double;

 

위와 같은 형태로 선언하면 된다. 

 

 

 

 

delete

delete 이름 ;

위와 같은 형태로 사용하면 되며 배열은 이름의 앞에 []를 붙여 준다. 

delete[] 배열 이름;

delete testInt;

delete []testArray;

delete testFloat;

delete testDouble;

 

 

 

 

 

 

 

 

 

참조자와 함수

 

Call-by-value와 Call-by-reference는 많이 들어봤을 것이다.

Call-by-value란 '값'을 인자로 전달하는 함수의 호출 방식, Call-by-reference란 '주소 값'을 인자로 전달하는 함수의 호출방식이다. 

 

 

 

 

 

int main()
{
	int a = 1;
	int b = 2;
    
	CallByValue(a,b);
	CallByReference(&a, &b);
	ReferenceFunction(a, b);
}

위와 같이 구성된 main함수가 있다고 가정하자. 

 

 

 

⊙ Call-by-value!

void CallByValue(int num1, int num2)
{
	num1 = 50;
	num2 = 30; 
}

CallByValue함수는 위와 같이 함수의 내부에서 a와 b의 값을 바꿔도 main함수의 a, b의 값은 바뀌지 않는다.

 

왜?

CallByValue함수의 인자값인 int num1와 int num2는 CallByValue라는 함수의 지역변수이다. 따라서 int num1과 int num2의 값을 바꿔도 해당 num1과 num2는 CallByValue함수가 끝나면 함께 사라지게 되는 것이다.

 

그래서 main함수의 a,b. 즉, 함수 외부에 선언된 변수에 접근이 불가하다. 

 

그래서 필요한 것이 주소 값을 인자로 전달하는 Call-By-Reference이다. 

 

 

 

 Call-by-Reference

void CallByReference(int *num1, int *num2)
{
      *num1 = 30;
      *num2 = 50;
}

 이렇게 되면 int *num1과 int *num2가 주소 값을 넘겨받아서 a와 b의 주소 값에 저장된 값을 '직접' 변경하게 되므로 함수 외부에 선언된 변수의 값을 바꿀 수 있는 것이다. 

 

정리하자면 Call-by-Reference는 주소 값을 전달받아서, 함수 외부에 선언된 변수에 접근하는 형태의 함수 호출이다. 

 

주소 값이 전달되었다는 사실이 중요한 것이 아닌 주소 값이 참조의 도구로 사용되었다는 사실이 중요한 것이며 이것이 Call-by-Value와 Call-by-Reference를 구분하는 기준이 된다.

 

 

 

Call-by-Value와 Call-by-Reference를 구분하는 기준에 대한 추가 설명

더보기

예를 들어

int *Test(int *ptr)
{
	return ptr+1;
}

이 함수는 분명 인자값을 주소로 전달하고 있음에도 Call-By-Reference가 아닌 Call-By-Value이다. 

왜냐하면 이 함수의 연산의 주체는 값이기 때문이다. 다만 그 값이 주소 값일 뿐인 것이다. 주소 값을 이용해 외부에 선언된 변수에 접근하는 Call-By-Reference와 거리가 멀다.

 

int *Test2(int *ptr)
{
	*ptr = 20;
	return ptr;
}

그러나 이런 식으로 사용되었다면 이 함수는 Call-By-Reference이다. 주소 값을 이용해서 함수 외부에 선언된 변수를 참조했기 때문이다. 

 

 

 

C++에서는 함수 외부에 선언된 변수의 접근 방법으로 두 가지가 존재한다. 

하나는 '주소 값'을, 다른 하나는 '참조자'를 이용하는 방식이다.

 

 

 

 참조자를 이용한 Call-By-Reference? 

Call-By-Reference의 가장 큰 핵심은 함수 내에서 함수 외부에 선언된 변수에 접근할 수 있다는 것이었다. 

void ReferenceFunction(int &num1, int &num2)
{
	num1 = 10;
	num2 = 20;
 }

매개변수로 참조자가 들어와 있다. 참조자는 선언과 동시에 변수로 초기화가 되어야 하지 않나?라고 생각할 수 있다.

하지만 매개변수는 함수가 호출되어야 초기화가 진행되는 변수들이다. 즉, 초기화가 이뤄지지 않은 것이 아니라 함수 호출 시 전달되는 인자로 초기화를 하겠다는 의미이다.

 

 

이와 같은 코드는 포인터로 주소값을 넘겨받는 Call-By-Reference 보다 함수의 특성이 한눈에 파악되지 않는다는 단점이 존재한다. 참조자로 넘겨줄 때 함수는 ReferenceFunction(a, b)로 Call-By-Value함수와 동일해 보인다.

 

그러나 실제 함수 외부의 변수 값에 영향을 끼칠 수 없는 CallByValue함수와 달리 이 함수는 함수 외부의 값을 변경할 수 있으므로 이 함수 내부에서 어떤 일이 일어날 지 직관적으로 파악이 불가능하기 때문에 단점이라고 하는 것이다.

 

 

이 단점은 const를 사용함으로써 극복이 가능하다. 함수의 매개 변수 선언 시 const를 붙여줌으로써 함수 내에서 참조자를 이용한 값의 변경을 하지 않겠다고 선언하는 것이다.

 

이렇게 하면 함수 내부에서 참조자에 값을 저장하는 경우 컴파일 에러가 발생하며 함수의 원형만 봐도 값의 변경이 이루어지지 않음을 확신할수 있다. 

 

 

 

 

 

 

 

 

 

 

참조자(Reference)

 

 

변수를 선언하면 해당 변수의 이름은 할당된 메모리 공간을 가리키는, 구분 짓는 이름이 된다. 

그래서 변수를 선언할 때 int a; 를 선언했다면?

이미 a라는 이름이 가리키고 있는 메모리 공간이 있으므로 해당 범위(scope, { })에서 동일한 이름의 변수를 선언할 수가 없다. 

 

 

 

그런데 이 상황에서 number가 가리키는 공간에 또 하나의 이름을 부여하고 싶다면

int &b = a;

위와 같이 b앞에 &를 붙여 선언하면 된다.

이 의미는 b라는 이름도 a라는 메모리 공간을 가리키는 또 하나의 이름이 된 것이다. 

 

 

왜 그럴까? 

보통 & 연산자는 변수의 주소값을 반환하는 연산자이다. 그러나 변수를 선언할 때 &를 붙이게 되면 이는 참조자의 선언을 의미하게 된다. 

 

int *c = &a;  → 변수 a의 주소 값을 포인터 c에 저장
int &b = a;  →  변수 a에 대한 참조자 b를 선언.

 

또한 참조자를 대상으로 참조자를 선언하는 것도 가능하지만 바람직하지는 않으며 딱히 크게 사용할 일이 없다.

int a;

int &b = a;

int &c = b;

 

 

참조자의 중요한 성질 중 하나는 변수에 대해서만 선언이 가능하다는 것과 선언됨과 동시에 참조할 대상이 있어야 한다는 것이다.

또한 NULL로 초기화하는 것도 불가능하다. 

 

int &b;  (X)  → 선언과 동시에 참조할 대상이 있어야 한다.
int &b = NULL;  (X)  → NULL로 초기화 하는 것은 불가능하다.
int &b = 100;  (X)  → 변수에 대해서만 선언이 가능하므로 상수를 넣을 수 없다. 

 

단, 다음 게시글에 나올 const 참조자는 상수도 참조가 가능하다.

+ Recent posts