2012년 3월 21일 수요일

[MFC] memory mapped file I/O

이번 글은 memory mapped file I/O을 통한 대용량 파일의 I/O를 설명하고자 한다. 시작하기 전에, memory mapping의 이점에 대해서는 굳이 설명하지 않더라도, 간단한 구글검색으로 확인할 수 있으리라고 생각한다.

다음의 링크를 통해 memory mapped file I/O에 대한 전반적인 내용을 확인할 수 있다. http://en.wikipedia.org/wiki/Memory-mapped_file

아래의 링크는 MSDN에서 제공되는 memory mapped file I/O를 위한 함수의 설명이다. http://msdn.microsoft.com/en-us/library/aa366537.aspx 

자, 이제 전반적인 내용에 대해 이해했다면, 바로 설명에 들어간다. 우선 나의 경험은 다음과 같다. 대용량(약 7GB이상)의 파일에 접근하여 데이터를 읽어 들인다. 보다 자세하게는 대용량의 YUV시퀀스를 한프레임씩 읽어 화면에 출력한다. 불행하게도, standard I/O 함수 (e.g., fopen, fread, etc.)에서는 (운영체제에 따라 다르지만) 4GB 이상의 크기를 지원하지 않는다. 그렇다면, 대용량 파일을 위해 CreateFile을 이용할 수 있다. 문제는 성능이다. 앞의 링크에서 확인할 수 있듯이, file I/O는 다른 동작에 비해 부하가 크다. 이를 개선하기 위한 방법을 모색하던 중 오늘 다루고자 하는 memory mapped file I/O를 사용하기로 하였다.

windows에서는 memory mapped file I/O를 위한 함수로 크게 세 가지 함수를 제공한다.
1. CreateFile
2. CreateFileMapping
3. MapViewOfFile

각각의 사용법은 위의 MSDN 링크를 확인하면 되겠다. 여기까지 확인한 이후, 아~ 이제 되었다! 라고 안도하던 중, 또 다른 문제를 발견했다. CreateFile, CreateFileMapping이후, MapViewOfFile을 실행하면, 정상적인 포인터를 리턴받지 못하는 상황이 발생하였다.

또 다시 문제를 해결하고자 구글링을 하던 중, 해결의 단초가 될만한 자료를 찾았다. (Thanks to Google) 역시 운영체제에 따라 다르지만, memory mapping을 수행할 수 있는 최대 크기에 한계가 있다는 점이었다. 나는 직접 실험해보지 않았지만, 500MB까지는 한번에 수행가능하지만, 그 이상은 동작하지 않는다는 결과가 있었다. 어차피 나는 500MB를 훨씬 웃도는 파일의 I/O가 목적이었으므로, 문제를 해결해야 했다.

실마리는 I/O를 위한 파일의 일부씩 memory mapping을 수행하는 것이다.
 MapViewOfFile의 파라메터를 확인해 보면, dwFileOffsetHigh, dwFileOffsetLow 그리고 dwNumberOfBytesToMap가 존재한다. 이 파라메터를 적절하게 사용함으로써 성공적으로 memory mapped file I/O를 수행할 수 있었다. 이 때 offset 부분에서 주의해야할 점은, offset은 임의의 숫자를 넣을 경우, 에러가 발생한다(참으로 까다로운 녀석이다). offset은 오직 각자의 시스템에서 지원하는 단위의 배수로만 가능하다. 이 단위는 GetSystemInfo() API를 통해 확인 할 수 있는데, 여기에 파라메터로 들어가는 SYSTEM_INFO 스트럭처의 dwAllocationGranularity를 통해 확인 할 수 있다.

 자! 이제 모든 준비가 끝났다! 이제 문제를 해결해 보자. 앞서 설명한 것과 같이, 내가 구현하고자 하는 것은 무지막지하게 큰 영상을 한장씩 읽어들이는 것이다. 한 프레임은 가로 3840, 세로 2160이며, YV12(4:2:0 YUV)포멧의 영상이다. 그러므로 한 프레임의 크기는 3840 * 2160 * 3 / 2 = 12,467,520 Byte 이다. (크기 계산 방법에 의문이 드시는 분은 역시 Google을 검색해 보시길...^^;;) 그리고, dwAllocationGranularity는 65536 이었다. 따라서 MapViewOfFile을 다음과 같이 호출하였다.

m_hYUVFile = CreateFile(m_cFileName, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if(m_hYUVFile == NULL)
{
 printf("CreateFile() file. Err=%d\n", GetLastError());
 return -1;
}
DWORD dwFileSize = ::GetFileSize(m_hYUVFile, NULL);

m_hYUVMapFile = CreateFileMapping(m_hYUVFile, NULL, PAGE_READONLY, 0, dwFileSize, NULL);
if(m_hYUVMapFile == NULL)
{
 printf("CreateFileMapping() fail. Err=%d\n", GetLastError());
 return NULL;
}

DWORD m_dwMapUnit = systeminfo.dwAllocationGranularity  //65536
DWORD m_dwFrameSize = dwWidth * dwHeight * 3 / 2;  //한 프레임의 크기
DWORD dwMultiple = (DWORD)floor(((double)m_nPresentationCnt*(double)m_dwFrameSize)/(double)m_dwMapUnit);
//m_nPresentationCnt는 몇 번째 프레임인지를 지시한다(순차 증가).
//그러므로 m_nPresentationCnt*m_dwFrameSize는 실제 파일 내에서의 offset을 지시한다.
//앞서 말한대로, 지원하는 단위(m_dwMapUnit)로만 offset을 줄 수 있으므로, m_dwMapUnit에 대한 배수를 계산한다.
DWORD dwDiff = (m_dwFrameSize*m_nPresentationCnt) - (m_dwMapUnit*dwMultiple);
//dwDiff는 실제 파일의 offset과의 차이를 계산한다.

m_pcYUVFile = (char *)MapViewOfFile(m_hYUVMapFile,  //CreateFileMapping을 통해 생성한 핸들러 
                                    FILE_MAP_READ, 0,
                                    (dwMultiple*m_dwMapUnit),  //m_dwMapUnit의 배수로 offset 지정
                                    (m_dwFrameSize+dwDiff));  //파일에서 접근하고자 하는 크기

//m_pcYUVFile에 대한 처리 동작 수행

UnmapViewOfFile(m_pcYUVFile);  //Unmaps a mapped view of a file from the calling process's address space
CloseHandle(m_hYUVMapFile);  //Close file map handle
CloseHandle(m_hYUVFile);  //Close file handle


후아~ 이로써 성공적으로 문제를 해결 하였다. 주의할 점은 full source가 아니므로, 상황에 맞게 수정한 뒤, 사용해야 한다. standard file I/O와 성능 비교는 수행하지 않았으므로, 혹시 성능을 비교해본 분이 있다면, 댓글로 남겨주신다면 매우 감사...요즘 대세인 SSD일 때는 또 어떨까낭...흠냐...

2012년 3월 20일 화요일

Multi-thread 환경에서의 메모리 공유

스레드를 사용하는 프로그램을 작성하던 초기에는, 하나의 스레드 이상을
사용할 일이 없었으므로, 문제가 없었다 (문제가 있더라도 어렵지 않았다).

최근들어 멀티 스레드로 프로그램을 작성하면서 생각하지 못했던 문제와
마주하게 되었다. 바로 스레드 동기화에 관련된 부분이다.

음?! 수업시간에 들었던 기억은 나는데...실제 개발시에 사용해본 일이
없다보니, 기억도 가물가물하고...언제 써야할지 떠오르지도 않고...아래의
코드는 뮤텍스를 통한 두 개의 스레드간 동기화에 대한 예제이다.

#include <stdio.h>
#include <process.h>
#include <windows.h>
//#include <pthread.h>  //유닉스/리눅스 계열에서 뮤텍스를 위한 헤더

#define MUTEX

int cnt = 0;  //각 스레드에서 접근하는 전역변수
#ifdef MUTEX
HANDLE hMutex;  //뮤텍스의 핸들러
//pthread_mutex_t hMutex;  //유닉스/리눅스 계열에서의 뮤텍스 핸들러
#endif

DWORD WINAPI Thread1(void *arg)
{
    int i, tmp;
    for(i=0; i<1000; i++)
    {
#ifdef MUTEX
        WaitForSingleObject(hMutex, INFINITE);  //뮤텍스에 대한 Lock
        //pthread_mutex_lock(hMutex)  //유닉스/리눅스 계열에서의 Lock
#endif
        tmp = cnt;
        Sleep(1000);
        cnt = tmp + 1;
        printf("[1]: %d\n", cnt);
#ifdef MUTEX
        ReleaseMutex(hMutex);  //뮤텍스에 대한 Unlock
        //pthread_mutex_unlock(hMutex)  //유닉스/리눅스 계열에서의 Unlock
#endif
    }
    printf("Thread1 End\n");
    return 0;
}

DWORD WINAPI Thread2(void *arg)
{
    int i, tmp;
    for(i=0; i<1000; i++)
    {
#ifdef MUTEX
        WaitForSingleObject(hMutex, INFINITE);  //뮤텍스에 대한 Lock
        //pthread_mutex_lock(hMutex)  //유닉스/리눅스 계열에서의 Lock
#endif
        tmp = cnt;
        Sleep(1000);
        cnt = tmp + 1;
        printf("[2]: %d\n", cnt);
#ifdef MUTEX
        ReleaseMutex(hMutex);  //뮤텍스에 대한 Unlock
        //pthread_mutex_unlock(hMutex)  //유닉스/리눅스 계열에서의 Unlock
#endif
    }
    printf("Thread2 End\n");
    return 0;
}

int main(int argc, char *argv[])
{
    HANDLE hThread[2];

#ifdef MUTEX
    hMutex = CreateMutex(NULL, FALSE, NULL);  //뮤텍스 생성 및 초기화
    //pthread_mutex_t hMutex = PTHREAD_MUTEX_INITIALIZER;  //유닉스/리눅스 계열에서의 뮤텍스 생성 및 초기화
    //pthread_mutex_t hMutex = pthread_mutex_init();  //유닉스/리눅스 계열에서의 뮤텍스 초기화(동적 초기화)
#endif

    hThread[0]=CreateThread(NULL, 0, Thread1, NULL, 0, NULL);
    hThread[1]=CreateThread(NULL, 0, Thread2, NULL, 0, NULL);

    WaitForMultipleObjects(2, hThread, TRUE, INFINITE);  //두 개의 스레드가 종료될때까지 대기

    printf("%d\n", cnt);  //결과값 출력

    CloseHandle(hThread[0]);
    CloseHandle(hThread[1]);
#ifdef MUTEX
    CloseHandle(hMutex);  //뮤텍스 핸들러 파괴
    //pthread_mutex_destroy(hMutex);  //유닉스/리눅스 계열에서의 뮤텍스 핸들러 파괴
#endif

    return 0;
}

코드를 실행해보면, #define MUTEX를 활성화 시킨 경우, 2000이 출력되며, 비활성화 시킨 경우, 1000이 출력됨을 확인할 수 있다. 비활성화 시킨 경우에는 두 개의 스레드가 하나의 변수(cnt)에 접근하여 1씩 증가 시키는 동작을 수행하는 도중, 스레드1 에서 2로(또는 반대)의 context switching이 발생됨으로 인해 원래의 값을 잃어버리는(또는 변형되는) 상황이 발생하기 때문이다. 반대로 뮤텍스를 사용하는 경우에는 하나의 스레드가 작업을 수행하고 마칠 때까지 (wait(lock)에서 release(unlock)까지) 다른 스레드는 대기(wait(release))하기 때문에 원하는 대로 2000의 결과를 확인할 수 있다.

2012년 3월 15일 목요일

[MFC] Unicode환경에서 CString to char* 변환

Unicode 개발 환경에서, CString과 char* 간의 형변환은 지극히 귀찮은 일이다.

특히 console이 아닌 윈도우 UI기반으로 프로그램을 작성할 때, CFileDialog를
통해 파일이름 및 경로를 획득하고, 이를 이용하여 fopen (사실 다른 입출력
함수 (e.g., CreateFile을 쓰면 해결됨)) 등의 ANSI-C기반의 파일 입출력 함수에
활용하는 경우 위와 같은 형변환이 요구된다. 

Google등으로 검색해보면 많은 방법들이 제시되는데, 그중에서 실제로 
정확하게 동작한 것을 남겨둔다. 나중을 위해...

Unicode 환경에서 CString --> char* 형변환은 두 단계로 이루어 진다.
1. CString to wchar_t* 간의 형변환
2. wchar_t* to char* 간의 형변환


CString  str;           //형변환할 문자열이 저장된 CString 변수
wchar_t* wchar_str;     //첫번째 단계(CString to wchar_t*)를 위한 변수
char*    char_str;      //char* 형의 변수
int      char_str_len;  //char* 형 변수의 길이를 위한 변수

//1. CString to wchar_t* conversion
wchar_str = str.GetBuffer(str.GetLength());

//2. wchar_t* to char* conversion
//char* 형에 대한길이를 구함
char_str_len = WideCharToMultiByte(CP_ACP, 0, wchar_str, -1, NULL, 0, NULL, NULL);
char_str = new char[char_str_len];  //메모리 할당
//wchar_t* to char* conversion
WideCharToMultiByte(CP_ACP, 0, wchar_str, -1, char_str, char_str_len, 0,0);  

//Done.

2012년 3월 14일 수요일

[MFC] Win32 Console Application에서 CFileDialog 이용하기

제목 그대로 이다. Win32 Console Application에서 argument를 통해 파일명 입력이 귀찮은 경우, MFC에서와 같이 CFileDialog를 이용할 수 있다.

우선 Visual Studio(VS2010 기준으로 작성)를 실행한다.

새 프로젝트를 선택하고, "Win32 Console Application"을 선택한다. (그림 참고)



다음으로 Application Setting 창에서 Console application과 MFC를 선택한다. (그림 참고)


Finish를 선택하고, 자동으로 생성되는 파일에 다음과 같은 코드를 추가한다.

//stdafx.h에 추가
#include <stdio.h>
#include <tchar.h>
#include <iostream>
#include <fstream>
#include <cctype>
#include <conio.h>
#include <iomanip>
#include <afxdlgs.h>
#include <atlstr.h>


다음으로 cpp 파일에 다음과 같이 추가하면 끝!!
//_tmain 함수에 추가
CString szFileName = "";
CString szFilter = "Text Files(*.txt)|*.txt|All Files(*.*)|*.*";
char ch;
cout << "Open File 'y' or 'Y' ? ";
cin >> ch;
if((ch == 'y') || (ch == 'Y'))
{
 CWnd* pWnd = CWnd::FromHandle(GetForegroundWindow()); 
 CFileDialog dlg(TRUE, _T("*.txt"), _T("*.txt"), NULL, szFilter, pWnd);
 dlg.m_ofn.Flags |= OFN_FILEMUSTEXIST;
 dlg.m_ofn.lpstrTitle = _T("Read a text file");
 if(dlg.DoModal() == IDOK)
 {
  szFileName = dlg.GetPathName();
 }
}
cin.get();
if(szFileName.IsEmpty())
{
 szFileName = "ReadMe.txt";
}
ifstream in(szFileName, ios::in | ios::binary);
if(!in) {
 cout << "Cannot open input file.\n";
 cout << "Press any key to Exit!";
 while(!_kbhit())
  Sleep(2);
 return 1;
}

<실행 화면>



Console application으로 프로젝트를 설정한 이후에 Filedialog가 웬말이냐 라는 사람도 있겠지만, 가끔은 필요할 때도 있다.

※ 추가사항
반드시, 메인함수의 초기에 다음의 초기화 과정을 거쳐야 한다.

HMODULE hModule = ::GetModuleHandle(NULL);
if (hModule == NULL) return 1;
if (!AfxWinInit(hModule, NULL, ::GetCommandLine(), 0)) return 1;