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일 때는 또 어떨까낭...흠냐...

댓글 없음:

댓글 쓰기