考虑到磁盘上一个非常大的文件(可能超过4GB),我想扫描这个文件并计算出特定二进制模式的出现次数。
我的想法是:
使用内存映射文件(CreateFileMap 或者提升mapped_file)来加载 文件到虚拟内存。
对于每个100MB映射内存,创建一个线程进行扫描并计算结果。
这可行吗?有没有更好的方法呢?
更新:
内存映射文件是一个不错的选择,通过1.6GB文件扫描可以在11s内处理。
感谢。
答案 0 :(得分:10)
创建20个线程,每个假设处理大约100 MB的文件可能只会恶化性能,因为HD必须同时从几个不相关的位置读取。
高清性能在读取顺序数据时达到峰值。因此,假设您的大文件没有碎片,最好的办法可能是只使用一个线程并从头到尾读取几个(比如4个)MB的块。
但我知道什么。文件系统和缓存很复杂。做一些测试,看看什么效果最好。
答案 1 :(得分:5)
虽然您可以使用内存映射,但您不必这样做。如果你以小块的形式顺序读取文件,比如说每个1 MB,那么文件将永远不会同时出现在内存中。
如果您的搜索代码实际上比您的硬盘慢,您仍然可以根据需要将块移交给工作线程。
答案 2 :(得分:4)
我使用内存映射文件编写了一个简单的测试函数,单个线程一个1.4 Gb文件需要大约20秒才能扫描。有两个线程,每个占用文件的一半(甚至1MB块到一个线程,奇数到另一个),花了80多秒。
没错,2个线程比1个线程慢4倍
!这是我使用的代码,这是单线程版本,我使用了1字节扫描模式,因此用于定位匹配跨越地图边界的代码未经测试。
HRESULT ScanForPattern(LPCTSTR pszFilename, LPBYTE pbPattern, UINT cbPattern, LONGLONG * pcFound)
{
HRESULT hr = S_OK;
*pcFound = 0;
if ( ! pbPattern || ! cbPattern)
return E_INVALIDARG;
// Open the file
//
HANDLE hf = CreateFile(pszFilename,
GENERIC_READ,
FILE_SHARE_READ, NULL,
OPEN_EXISTING,
FILE_FLAG_SEQUENTIAL_SCAN,
NULL);
if (INVALID_HANDLE_VALUE == hf)
{
hr = HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND);
// catch an open file that exists but is in use
if (ERROR_SHARING_VIOLATION == GetLastError())
hr = HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION);
return hr;
}
// get the file length
//
ULARGE_INTEGER uli;
uli.LowPart = GetFileSize(hf, &uli.HighPart);
LONGLONG cbFileSize = uli.QuadPart;
if (0 == cbFileSize)
{
CloseHandle (hf);
return S_OK;
}
const LONGLONG cbStride = 1 * 1024 * 1024; // 1 MB stride.
LONGLONG cFound = 0;
LPBYTE pbGap = (LPBYTE) malloc(cbPattern * 2);
// Create a mapping of the file.
//
HANDLE hmap = CreateFileMapping(hf, NULL, PAGE_READONLY, 0, 0, NULL);
if (NULL != hmap)
{
for (LONGLONG ix = 0; ix < cbFileSize; ix += cbStride)
{
uli.QuadPart = ix;
UINT cbMap = (UINT) min(cbFileSize - ix, cbStride);
LPCBYTE pb = (LPCBYTE) MapViewOfFile(hmap, FILE_MAP_READ, uli.HighPart, uli.LowPart, cbMap);
if ( ! pb)
{
hr = HRESULT_FROM_WIN32(GetLastError());
break;
}
// handle pattern scanning over the gap.
if (cbPattern > 1 && ix > 0)
{
CopyMemory(pbGap + cbPattern - 1, &pb[0], cbPattern - 1);
for (UINT ii = 1; ii < cbPattern; ++ii)
{
if (pb[ii] == pbPattern[0] && 0 == memcmp(&pb[ii], pbPattern, cbPattern))
{
++cFound;
// advance by cbPattern-1 to avoid detecting overlapping patterns
}
}
}
for (UINT ii = 0; ii < cbMap - cbPattern + 1; ++ii)
{
if (pb[ii] == pbPattern[0] &&
((cbPattern == 1) || 0 == memcmp(&pb[ii], pbPattern, cbPattern)))
{
++cFound;
// advance by cbPattern-1 to avoid detecting overlapping patterns
}
}
if (cbPattern > 1 && cbMap >= cbPattern)
{
// save end of the view in our gap buffer so we can detect map-straddling patterns
CopyMemory(pbGap, &pb[cbMap - cbPattern + 1], cbPattern - 1);
}
UnmapViewOfFile(pb);
}
CloseHandle (hmap);
}
CloseHandle (hf);
*pcFound = cFound;
return hr;
}
答案 3 :(得分:2)
我会让一个线程将文件(可能是一个流)读入一个数组,并让另一个线程处理它。由于磁盘搜索,我不会同时映射几个。我可能会有一个ManualResetEvent告诉我的线程什么时候下一个?字节已准备好进行处理。假设您的流程代码更快,那么hdd我将有2个缓冲区,一个用于填充,另一个用于处理,每次只在它们之间切换。
答案 4 :(得分:1)
我也只使用一个线程,不仅仅是因为HD性能问题,而且因为在拆分文件时可能无法管理副作用:如果你的模式出现在你拆分文件的地方怎么办? / p>
答案 5 :(得分:0)
如果使用只读视图,则使用内存映射文件还有一个额外的好处,即避免从文件系统高速缓存内存复制到(托管)应用程序内存(尽管您必须使用byte *指针然后才能访问内存) )。而不是创建多个线程使用一个线程来使用例如100MB内存映射视图顺序扫描文件(不要一次将整个文件映射到进程地址空间)。
答案 6 :(得分:0)
我会通过异步读取到双缓冲区来实现。因此,当从文件中读取一个缓冲区时,在扫描第一个缓冲区时开始读取下一个缓冲区。这意味着您可以并行执行CPU和IO。另一个优点是您将始终拥有围绕数据边界的数据。但是我不知道内存映射文件是否可以进行双缓冲。
答案 7 :(得分:0)