|
|
Fast File Loading...
등록자 : rawmania (김정수), 2008-09-05
http://www.gamasutra.com/features/20070419/garcia_01.shtml
하드 디스크와 DVD 유닛들에서부터 효과적으로 데이터를 읽는 것은 비디오 게임들에서 필수적인 것이고 다음 세대의 게임들에서 해결해야할 하나이상의 중요한 문제들입니다. 우리가 프로세싱 파워와 메모리 크기에서 20배의 속도를 얻는 반면, 우리는 데이터 디바이스들에서 4배의 속도 향상만을 얻었습니다 (콘소들에서의 DVD).
이글에서는 실시간 어플리케이션에서 파일들을 스트리밍하여 디스크에서 부터 (hdd, dvd) 로우 데이터를 어떻게 효과적으로 읽는가를 설명합니다 (다른 영역에서도 유용한 개념입니다). 사용되는 플랫폼은 Win32이지만, 모든 주제들은 다른 플랫폼들에 쉽게 포팅되어질 수 있도록 다루어집니다.
여 기서의 기술되는 모든 코드와 다른 기법들을 테스트하기 위한 모든 프레임 웍은 Visual Studio 2005를 위한 프로젝트로 포함됩니다. 당신은 여기에서 프로젝트를 다운로드할 수 있습니다. 여기서 테스트가 이루어진 머신은 3.2GHz Pentium 4입니다. 테스트에 사용된 디바이스들 입니다 :
윈도우즈 (그리고 일반적인 모든 운영 체제들)은 파일 캐싱을 위해 물리적인 메모리의 일부분 (프로세스들에의해 사용되지 않는)을 사용합니다 (당신은 작업 관리자에서 파일 시스템 캐쉬를 위해 얼마나 많은 메모리가 사용되는지를 볼 수 있습니다). 테스트 파일들에서 윈도우즈 캐싱을 피하기 위해, 시간을 측정하기 전에 커다란 파일들을 릭어 캐쉬를 비우는 함수를 구현하였습니다. 테스트들은 10번씩 실행되었고, 최종적으로 각 하나는 몇분씩 소용되었습니다.
이제 여행을 시작하도록 하겠습니다… 목적 : 가능한 빨리 CPU 메모리에서 100MB 파일을 가지게 합니다.
처음 옵션은 표준 C 라이브러리에서의 이식가능한 코드를 사용하는 것입니다. 이식성의 장점을 우리 모두는 알고 있습니다. 그래서 우리는 버퍼를 할당하고 파일을 fread()로 읽습니다.
FILE* fp = fopen(FileName, "rb");
fread(&g_buffer[0], 1, FileSize, fp);
fclose(fp);
| Stats | Min (MB/s) | Max (MB/s) | Average(MB/s) |
| HDD | 47.847 | 48.828 | 48.527 |
| DVD | 2.381 | 2.386 | 2.383 |
Win32 기반 파일 함수들을 위한 향상된 접근방법을 시도하도록 합니다 : CreateFile과 ReadFile.
HANDLE hFile = CreateFile(FileName, GENERIC_READ, 0, 0, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, 0);
DWORD dwNumberOfBytesRead = 0;
ReadFile(hFile, &g_buffer[0], FileSize, &dwNumberOfBytesRead, 0);
CloseHandle(hFile);
| Stats | Min (MB/s) | Max (MB/s) | Average(MB/s) |
| HDD | 47.843 | 48.852 | 48.497 |
| DVD | 2.383 | 2.390 | 2.386 |
거의 같은 속도입니다. FILE_FLAG_SEQUENTIAL_SCAN은 순차적인 파일 접근을 하도록 캐쉬 관리자 (Cache Manager)를 직접 사용하도록 합니다. 이것은 순차적인 접근으로 큰 파일들을 읽을 때 사용하도록 추천됩니다. 이것보다 더 나은 향상이 있도록 FILE_FLAG_SEQUENTIAL_SCAN을 주도록 하지만, 명백하게 Win32에서 구현되는 fread는 잘 동작합니다.
우리의 다음 접근 방법은 가상 메모리에서 사용되는 것과 같은 구조를 사용하여 시스템이 디스크에서 데이터를 읽도록 요청하는 곳에서 메모리에 맵핑된 파일들을 사용하도록 하는 것입니다.
HANDLE hFile = CreateFile(FileName, GENERIC_READ, 0, 0, OPEN_EXISTING,
FIEL_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, 0);
HANDLE hFileMapping = CreateFileMapping(hFile, 0, PAGE_READONLY, 0, FileSize, 0);
int iPos = 0;
const unsigned int BlockSize = 128 * 1024;
while (iPos < FileSize)
{
int iLeft = FileSize - iPos;
int iBytesToread = iLeft > BlockSize ? BlockSize : iLeft;
void *rawBuffer = MapViewOffile(hFileMapping, FILE_MAP_READ, 0, iPos, iBytesToRead);
memcpy(&g_buffer[iPos], rawBuffer, iBytesToRead);
unmapViewOfFile(rawBuffer);
iPos += iBytesToRead;
}
CloseHandle(hFileMapping);
CloseHandle(hFile);
| Stats | Min (MB/s) | Max (MB/s) | Average(MB/s) |
| HDD | 45.830 | 48.828 | 48.190 |
| DVD | 2.524 | 2.528 | 2.526 |
우리는 DVD에서 읽을 때 눈에 띄는 향상을 얻었습니다. 하드 디스크에서 읽는 것은 거의 같은 속도입니다.
비동기 I/O는 디스크 컨트롤러를 위한 큐에 디스크 요청들을 위치하도록 하고, 즉시 반환합니다. 비동기 I/O의 최대 장점 중 하나는 디스크는 커널 모드로 들어가고 나가는 것 없이 항상 바쁘게 된다는 것입니다. 여기서 주목해야할 중요한 것은 어떠한 불필요한 메모리 복사 연산을 피하도록 FILE_FLAG_NO_BUFFERING를 Win32 캐쉬가 사용하도록 넘겨주는 것입니다. 데이터는 직접 DMA에서 어플리케이션으로 이동합니다. 저는 FILE_FLAG_NO_BUFFERING 없이 FILE_FLAG_OVERLAPPED (비동기 I/O를 위한) 플래그를 사용하는 어렵다는 것을 찾았습니다. 대부분의 시간에, 윈도우즈는 제 디스크 요청들을 오버랩핑하지 못했습니다.
전반적으로 8개의 I/O 요청들을 활성화하도록 유지하는게 가장 좋은 결과를 나왔습니다. 처리되어지는 요청들이 있는 동안 CPU는 이미 처리된 하나를 복사하고 있습니다.
그런데, FILE_FLAG_OVERLAPPED를 사용할 때, 당신은 섹터로 정렬된 버퍼들로 읽는 것이 필요합니다. 저는 이러한 목적을 위해 VirtualAlloc를 사용하였습니다.
for (int i = 0; i < NumBlocks; i++)
{
// VirtualAlloc() creates stroage that is page aligned
// and so is disk sector aligned
blocks[i] = static_cast<char *>(VirtualAlloc(0, BlockSize, MEM_COMMIT, PAGE_READWRITE));
ZeroMemory(&overlapped[i], sizeof(OVERLAPPED));
overlapped[i].hEvent = CreateEvent(0, false, flase, 0);
}
HANDLE hFile = CreateFile(FileName, GENERIC_READ, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_NO_BUFFERING | FILE_FLAG_OVERLAPPED | FILE_FLAG_SEQUENTIAL_SCAN, 0);
int iWriterPos = 0;
int iReaderPos = 0;
int iIOPos = 0;
int iPos = 0;
do
{
while (iwriterPos - iReaderPos != NumBlocks && iIOPos < FileSize)
{
overlapped[iWriterPos & NumBlocksMask].Offset = iIOPos;
int iLeft = FileSize - iIOPos;
int iBytesToRead = iLeft > BlockSize ? BlockSize : iLeft;
const int iMaskedWriterPos = iWriterPos & NumBlocksMask;
ReadFile(hFile, blocks[iMaskedWriterPos], iBytesToRead, 0, &overlapped[iMaksedWriterPos]);
iWriterPos++;
iIOPos += iBytesToRead;
}
const int iMaskedReaderPos = iReaderPos & NumBlocksMask;
WaitForSingleObject(overlapped[iMaskedReaderPos].hEvent, INFINITE);
int iLeft = FileSize - iPos;
int iBytesToRead = iLeft > BlockSize ? BlockSize : iLeft;
memcpy(&g_buffer[iPos], blocks[iMaskedReaderPos], iBytesToRead);
iReaderPos++;
iPos += iBytesToRead;
}
while (iPos < FileSize);
CloseHandle(hFile);
for (int i = 0; i < NumBlocks; i++)
{
VirtualFree(blocks[i], BlockSize, MEM_COMMIT);
CloseHandle(overlapped[i].hEvent);
}
| Stats | Min (MB/s) | Max (MB/s) | Average(MB/s) |
| HDD | 48.239 | 48.852 | 48.700 |
| DVD | 2.384 | 2.408 | 2.399 |
우리는 하드 디스크에서 이전 속도와 같은 속도를 얻었지만 DVD 에서 메모리 맵된 파일들이 아래로 내려갔습니다. 어쨋던, 이 기법으로 우리가 얻을 수 있는 흥미로운 것이 있습니다 : 안전성 (stability) (최소와 최대값 사이에 차이가 적습니다).
만일 우리가 CPU를 사용한다면 더 좋은 속도 향상을 가질 수 있습니다 (이전 테스트들에서 대부분에서 CPU는 idle 입니다). 데이터 압축은 디시크가 읽는 동안 CPU에게 작업을 주게 되고 전송되어져야하는 데이터의 양이 줄어들도록 합니다. 이상적으로 우리는 I/O 연산 보다 빠르게 압축을 해제할 수 있어서 압축 해제는 비용이 들지 않습니다 (예제 코드에서, 당신은 I/O가 CPU 활성화를 위해 LOG_IO_STALLS 매크로를 기다릴 때를 감지할 수 있습니다).
여기서는 LZO와 ZLIB로 데이터를 압축하도록 시도하였습니다. LZO는 ZLIB보다 빠르지만 데이터를 스트리밍하는 것을 허용하지 않아서 저는 ZLIB를 선택하였습니다. 100Mb 파일을 압축하여 64Mb로 줄였습니다.
이제 압축으로 인해 각 테스트의 장점을 보도록 하겠습니다.
| HardDisk | Min (MB/s) | Max (MB/s) | Average (MB/s) | Improvement % |
| ANSIC | 59.067 | 69.348 | 67.558 | +39% |
| W32 | 67.797 | 69.396 | 68.446 | +41% |
| MMapped | 52.247 | 53.562 | 52.980 | +10% |
| Async I/O | 68.634 | 69.832 | 69.242 | +42% |
| DVD | Min (MB/s) | Max (MB/s) | Average (MB/s) | Improvement % |
| ANSIC | 2.469 | 2.476 | 2.472 | +3% |
| W32 | 3.437 | 3.467 | 3.455 | +44% |
| MMapped | 3.713 | 3.724 | 3.720 | +47% |
| Async I/O | 3.464 | 3.475 | 3.470 | +44% |
명확하게, 압축이 이겼습니다. 우리는 하드 디스크에서의 메모리 맵을 제외한 모든 테스트에서 (반면에 DVD에서는 가장 좋은 속도향상을 얻었습니다) 이론상의 향상을 얻었습니다 (64 / 100 압축에서 56%).
우리는 디스크에서 파일들을 로드하는 여러 방법들을 살펴 보았고 속도의 향상을 위한 압축을 다루었습니다. 압축을 사용한 비동기 I/O가 가능한 빠르게 파일들을 로드하도록 추천되는 방법입니다 :
코드가 쉽습니다. 우리는 스레드들에 대한 처리를 하지 않았습니다.
모든 상황에서 가장 좋은 방법이 아닐지라도, 평균적으로 좋은 속도를 가지게 됩니다.
디스크는 항상 커널 모드로의 들어가고 나가는 것 없이 바쁘게 됩니다. 이것은 검색 시간이 비싼 DVD와 같은 디바이스에서 읽을 때 치명적입니다. 이 특징은 실제로 당신이 DVD에서 많은 파일들을 스트리밍 인 할 때 중요합니다. 만일 당신이 DVD에서 읽을는다고 할지라도 당신은 당신의 데이터 파일들을 일괄적으로 처리해야 하지만, 그것은 다른 이야기 입니다.
비동기 I/O가 또다른 스레드를 필요로하지 않을지라도, 저는 압축 해제를 하도록 또다른 스레드에서 비동기 I/O로 일기 쓰기를 하도록 하고 메인 스레드가 이미 로드된 아이템들을 분리하는 것을 추천합니다.
http://codesarang.com. mail to cpueblo cpueblo.com
|
|