14

考虑到磁盘上有一个非常大的文件(可能超过 4GB),我想扫描这个文件并计算特定二进制模式发生的时间。

我的想法是:

  1. 使用内存映射文件(CreateFileMap 或 boost mapped_file)将文件加载到虚拟内存。

  2. 对于每 100MB 映射内存,创建一个线程来扫描和计算结果。

这可行吗?有更好的方法吗?

更新
内存映射文件是个不错的选择,扫描一个 1.6GB 的文件可以在 11 秒内完成。

谢谢。

4

8 回答 8

10

创建 20 个线程,每个线程假设处理大约 100 MB 的文件可能只会降低性能,因为 HD 将不得不同时从几个不相关的地方读取。

HD 性能在读取顺序数据时达到顶峰。因此,假设您的大文件没有碎片化,最好的办法可能是只使用一个线程并以几个(比如 4)MB 的块从头到尾读取。

但我知道什么。文件系统和缓存很复杂。做一些测试,看看什么效果最好。

于 2010-01-31T12:32:55.357 回答
5

尽管您可以使用内存映射,但您不必这样做。如果您以小块顺序读取文件,例如每个 1 MB,则该文件将永远不会一次全部出现在内存中。

如果您的搜索代码实际上比您的硬盘慢,您仍然可以根据需要将块交给工作线程。

于 2010-01-31T12:21:11.253 回答
4

多线程只会让这个过程变慢,除非你想扫描多个文件,每个文件都在不同的硬盘上。否则你只是去寻找。

我使用内存映射文件编写了一个简单的测试函数,用一个线程扫描一个 1.4 Gb 的文件大约需要 20 秒。使用两个线程,每个线程占用一半文件(一个线程甚至 1MB 块,另一个线程奇数),花费了 80 多秒。

  • 1个线程:20015毫秒
  • 2个线程:83985毫秒

没错,2 个线程比 1 个线程慢四倍

这是我使用的代码,这是单线程版本,我使用了 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;
}
于 2010-02-10T05:33:31.193 回答
2

我会让一个线程将文件(可能作为流)读入一个数组并让另一个线程处理它。由于磁盘寻道,我不会一次映射多个。我可能会有一个 ManualResetEvent 来告诉我的线程什么时候下一个?字节已准备好进行处理。假设您的进程代码更快,那么 hdd 我将有 2 个缓冲区,一个要填充,另一个要处理,每次都在它们之间切换。

于 2010-02-04T20:52:42.707 回答
1

我也只使用一个线程,不仅是为了解决高清性能问题,而且因为您在分割文件时可能无法管理副作用:如果在分割文件的地方出现了您的模式怎么办?

于 2010-02-04T12:56:38.687 回答
0

如果您使用只读视图(尽管您必须使用 byte* 指针来访问内存),使用内存映射文件还有一个额外的好处,即避免从文件系统缓存内存复制到(托管)应用程序内存。而不是创建许多线程,而是使用一个线程顺序扫描文件,例如使用 100MB 内存映射视图到文件中(不要一次将整个文件映射到进程地址空间)。

于 2010-02-04T11:04:01.917 回答
0

我会通过异步读取双缓冲区来做到这一点。因此,当从文件中读取一个缓冲区时,在扫描第一个缓冲区时开始读取下一个缓冲区。这意味着您并行执行 CPU 和 IO。另一个优点是您将始终拥有围绕数据边界的数据。但是我不知道内存映射文件是否可以进行双缓冲。

于 2010-02-04T11:25:39.173 回答
0

Tim Bray(和他的读者)在他的Wide Finder ProjectWide Finder 2中深入探讨了这一点。基准测试结果表明,多线程实现在大型 Sun 多核服务器上的性能优于单线程解决方案。在通常的 PC 硬件上,多线程不会给你带来太多好处,如果有的话。

于 2010-02-06T17:11:57.477 回答