设计一个快速的“滚动窗口”文件阅读器

我正在用C ++编写一个用“滑动窗口”扫描文件的算法,这意味着它将扫描字节0到n,做一些事情,然后扫描字节1到n + 1,做某事,等等,直到结束到达了。

我的第一个算法是读取前n个字节,做一些事情,转储一个字节,读取一个新字节,然后重复。 这非常慢,因为来自HDD的“ReadFile”一次一个字节是低效的。 (约100kB / s)

我的第二个算法涉及将一个文件块(可能是n * 1000个字节,意味着整个文件,如果它不是太大)读入缓冲区并从缓冲区读取单个字节。 现在我得到大约10MB / s(体面的SSD +酷睿i5,1.6GHz笔记本电脑)。

我的问题:你对更快的模型有什么建议吗?

编辑我的大缓冲区(相对于窗口大小)实现如下:
– 对于5kB的滚动窗口,缓冲区初始化为5MB
– 将文件的前5MB读入缓冲区
– 窗口指针从缓冲区的开头开始
– 移动时,窗口指针递增
– 当窗口指针接近5MB缓冲区的末尾时(例如4.99MB),将剩余的0.01MB复制到缓冲区的开头,将窗口指针复位到开头,并将另外的4.99MB读入缓冲区。 – 重复一遍

编辑2 – 实际实施(删除)

感谢大家多多有见地的回应。 选择“最佳答案”很难; 他们都很优秀,并帮助我编码。

我在我的一个应用程序中使用了一个滑动窗口(实际上,几层滑动窗口在彼此之上工作,但这超出了本讨论的范围)。 该窗口通过CreateFileMapping()MapViewOfFile()使用内存映射文件视图,然后我有一个抽象层。 我向抽象层询问我需要的任何字节范围,并确保相应地调整文件映射和文件视图,以便这些字节在内存中。 每次请求新的字节范围时,仅在需要时调整文件视图。

文件视图在页面边界上定位和resize,这些边界甚至是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(); } 

此代码旨在允许以任何数据大小随机跳转文件中的任何位置。 由于您按顺序读取字节,因此可以根据需要简化某些逻辑。

您的新算法仅支付0.1%的I / O低效率……不值得担心。

为了进一步提高吞吐量,您应该仔细研究“做某事”步骤。 查看是否可以重用重叠窗口中的部分结果。 检查缓存行为。 检查相同计算是否有更好的算法。

你有基本的I / O技术。 您现在可以做的最简单的改进是选择一个好的缓冲区大小。 通过一些实验,您会发现读取性能随着缓冲区大小的增加而迅速增加,直到达到约16k,然后性能开始趋于平稳。

您的下一个任务可能是分析您的代码,并查看它花费的时间。 在处理性能时, 最好是测量而不是猜测。 你没有提到你正在使用什么操作系统,所以我不会提出任何探查器建议。

您还可以尝试减少缓冲区和工作区之间的数据复制/移动量。 减少复制通常会更好。 如果您可以就地处理数据而不是将其移动到新位置,那就是胜利。 ( 我从你的编辑中看到你已经这样做了。

最后,如果您正在处理许多千兆字节的归档信息,那么您应该考虑保持数据的压缩。 对于许多人来说,令人惊讶的是,读取压缩数据然后解压缩比读取解压缩数据更快。 我最喜欢的算法是LZO ,它不像其他算法那样压缩,但是能够快速地解压缩。 在以下情况下,这种设置仅值得工程努力:

  • 你的工作是受I / O约束的。
  • 您正在阅读许多G数据。
  • 您经常运行该程序,因此可以节省大量时间让它运行得更快。