我正在用C ++编写一个用“滑动窗口”扫描文件的算法,这意味着它会扫描字节0到n,做一些事情,然后扫描字节1到n + 1,做某事,等等,直到结束。
我的第一个算法是读取前n个字节,做一些事情,转储一个字节,读取一个新字节,然后重复。这非常慢,因为来自HDD的“ReadFile”一次一个字节是低效的。 (约100kB / s)
我的第二个算法涉及将一个文件块(可能是n * 1000个字节,意味着整个文件,如果它不是太大)读入缓冲区并从缓冲区读取单个字节。现在我大约10MB / s(体面的SSD + Core i5,1.6GHz笔记本电脑)。
我的问题:你对更快的型号有什么建议吗?
修改:我的大缓冲区(相对于窗口大小)实现如下:
- 对于5kB的滚动窗口,缓冲区初始化为5MB
- 将文件的前5MB读入缓冲区
- 窗口指针从缓冲区的开头开始
- 在移动时,窗口指针递增
- 当窗口指针接近5MB缓冲区的末尾时(例如4.99MB),将剩余的0.01MB复制到缓冲区的开头,将窗口指针重置为开头,并将另外的4.99MB读入缓冲区。
- 重复
编辑2 - 实际实施(已移除)
谢谢所有人都有很多有见地的回应。选择“最佳答案”很难;他们都很优秀,并帮助我编码。
答案 0 :(得分:3)
我在我的一个应用程序中使用滑动窗口(实际上,几层滑动窗口彼此叠加,但这超出了本讨论的范围)。该窗口通过CreateFileMapping()
和MapViewOfFile()
使用内存映射文件视图,然后我有一个抽象层。我向抽象层询问我需要的任何字节范围,并确保相应地调整文件映射和文件视图,以便这些字节在内存中。每次请求新的字节范围时,仅在需要时调整文件视图。
文件视图在页面边界上定位和调整大小,这些边界甚至是GetSystemInfo()
报告的系统粒度的倍数。仅仅因为扫描到达给定字节范围的末尾并不一定意味着它已经到达页面边界的末尾,因此下一次扫描可能根本不需要改变文件视图,下一个字节已经在内存中。如果范围的第一个请求字节超出映射页面的右边界,则文件视图的左边缘将调整为所请求页面的左边界,而左边的任何页面都将取消映射。如果范围中最后请求的字节超出最右侧映射页面的右侧边界,则会映射新页面并将其添加到文件视图中。
一旦你进入编码,这听起来比实际更复杂:
这听起来像是在固定大小的块中扫描字节,因此这种方法非常快速且非常有效。基于这种技术,我可以在最慢的机器上从头到尾顺序扫描多个 GIGBYTE 文件,通常是一分钟或更短时间。如果您的文件小于系统粒度,甚至只有几兆字节,您几乎不会注意到任何时间过去(除非您的扫描本身很慢)。
更新:这是我使用的简化版本:
class FileView
{
private:
DWORD m_AllocGran;
DWORD m_PageSize;
HANDLE m_File;
unsigned __int64 m_FileSize;
HANDLE m_Map;
unsigned __int64 m_MapSize;
LPBYTE m_View;
unsigned __int64 m_ViewOffset;
DWORD m_ViewSize;
void CloseMap()
{
CloseView();
if (m_Map != NULL)
{
CloseHandle(m_Map);
m_Map = NULL;
}
m_MapSize = 0;
}
void CloseView()
{
if (m_View != NULL)
{
UnmapViewOfFile(m_View);
m_View = NULL;
}
m_ViewOffset = 0;
m_ViewSize = 0;
}
bool EnsureMap(unsigned __int64 Size)
{
// do not exceed EOF or else the file on disk will grow!
Size = min(Size, m_FileSize);
if ((m_Map == NULL) ||
(m_MapSize != Size))
{
// a new map is needed...
CloseMap();
ULARGE_INTEGER ul;
ul.QuadPart = Size;
m_Map = CreateFileMapping(m_File, NULL, PAGE_READONLY, ul.HighPart, ul.LowPart, NULL);
if (m_Map == NULL)
return false;
m_MapSize = Size;
}
return true;
}
bool EnsureView(unsigned __int64 Offset, DWORD Size)
{
if ((m_View == NULL) ||
(Offset < m_ViewOffset) ||
((Offset + Size) > (m_ViewOffset + m_ViewSize)))
{
// the requested range is not already in view...
// round down the offset to the nearest allocation boundary
unsigned __int64 ulNewOffset = ((Offset / m_AllocGran) * m_AllocGran);
// round up the size to the next page boundary
DWORD dwNewSize = ((((Offset - ulNewOffset) + Size) + (m_PageSize-1)) & ~(m_PageSize-1));
// if the new view will exceed EOF, truncate it
unsigned __int64 ulOffsetInFile = (ulNewOffset + dwNewSize);
if (ulOffsetInFile > m_FileSize)
dwNewViewSize -= (ulOffsetInFile - m_FileSize);
if ((m_View == NULL) ||
(m_ViewOffset != ulNewOffset) ||
(m_ViewSize != ulNewSize))
{
// a new view is needed...
CloseView();
// make sure the memory map is large enough to contain the entire view
if (!EnsureMap(ulNewOffset + dwNewSize))
return false;
ULARGE_INTEGER ul;
ul.QuadPart = ulNewOffset;
m_View = (LPBYTE) MapViewOfFile(m_Map, FILE_MAP_READ, ul.HighPart, ul.LowPart, dwNewSize);
if (m_View == NULL)
return false;
m_ViewOffset = ulNewOffset;
m_ViewSize = dwNewSize;
}
}
return true;
}
public:
FileView() :
m_AllocGran(0),
m_PageSize(0),
m_File(INVALID_HANDLE_VALUE),
m_FileSize(0),
m_Map(NULL),
m_MapSize(0),
m_View(NULL),
m_ViewOffset(0),
m_ViewSize(0)
{
// map views need to be positioned on even multiples
// of the system allocation granularity. let's size
// them on even multiples of the system page size...
SYSTEM_INFO si = {0};
if (GetSystemInfo(&si))
{
m_AllocGran = si.dwAllocationGranularity;
m_PageSize = si.dwPageSize;
}
}
~FileView()
{
CloseFile();
}
bool OpenFile(LPTSTR FileName)
{
CloseFile();
if ((m_AllocGran == 0) || (m_PageSize == 0))
return false;
HANDLE hFile = CreateFile(FileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE)
return false;
ULARGE_INTEGER ul;
ul.LowPart = GetFileSize(hFile, &ul.HighPart);
if ((ul.LowPart == INVALID_FILE_SIZE) && (GetLastError() != 0))
{
CloseHandle(hFile);
return false;
}
m_File = hFile;
m_FileSize = ul.QuadPart;
return true;
}
void CloseFile()
{
CloseMap();
if (m_File != INVALID_HANDLE_VALUE)
{
CloseHandle(m_File);
m_File = INVALID_HANDLE_VALUE;
}
m_FileSize = 0;
}
bool AccessBytes(unsigned __int64 Offset, DWORD Size, LPBYTE *Bytes, DWORD *Available)
{
if (Bytes) *Bytes = NULL;
if (Available) *Available = 0;
if ((m_FileSize != 0) && (offset < m_FileSize))
{
// make sure the requested range is in view
if (!EnsureView(Offset, Size))
return false;
// near EOF, the available bytes may be less than requested
DWORD dwOffsetInView = (Offset - m_ViewOffset);
if (Bytes) *Bytes = &m_View[dwOffsetInView];
if (Available) *Available = min(m_ViewSize - dwOffsetInView, Size);
}
return true;
}
};
FileView fv;
if (fv.OpenFile(TEXT("C:\\path\\file.ext")))
{
LPBYTE data;
DWORD len;
unsigned __int64 offset = 0, filesize = fv.FileSize();
while (offset < filesize)
{
if (!fv.AccessBytes(offset, some size here, &data, &len))
break; // error
if (len == 0)
break; // unexpected EOF
// use data up to len bytes as needed...
offset += len;
}
fv.CloseFile();
}
此代码旨在允许以任何数据大小随机跳转文件中的任何位置。由于您按顺序读取字节,因此可以根据需要简化某些逻辑。
答案 1 :(得分:2)
您的新算法仅支付0.1%的I / O效率......不值得担心。
为了进一步提高吞吐量,您应该仔细查看“做某事”步骤。查看是否可以重用重叠窗口中的部分结果。检查缓存行为。检查是否有更好的算法用于相同的计算。
答案 2 :(得分:1)
你有基本的I / O技术。您现在可以做的最简单的改进是选择一个好的缓冲区大小。通过一些实验,您会发现读取性能随缓冲区大小的增加而快速增加,直到达到约16k,然后性能开始趋于平稳。
您的下一个任务可能是分析您的代码,并查看它花费的时间。在处理性能时,总是最好衡量而不是猜测。您没有提到您正在使用的操作系统,因此我不会提供任何探查器建议。
您还可以尝试减少缓冲区和工作区之间的数据复制/移动量。减少复制通常会更好。如果您可以就地处理数据而不是将其移动到新位置,那就是胜利。 (我从你的编辑中看到你已经在做这件事了。)
最后,如果您正在处理许多千兆字节的存档信息,那么您应该考虑保持数据的压缩。对于许多人来说,令人惊讶的是,读取压缩数据然后解压缩比读取解压缩数据更快。我最喜欢的算法是LZO,它不像其他算法一样压缩,但是解压缩速度非常快。在以下情况下,这种设置仅值得工程努力: