multithreading - 为什么这段代码的多线程会导致如此不一致的计时?

标签 multithreading benchmarking

我有一个处理大图像的函数。根据规范,该图像最大可为 55mb。该处理需要将图像分成几个不同的波段,然后通过将这些波段添加回输出图像来重构图像。由于图像太大,我无法在 32 位系统上同时将所有四个图像以及输入和输出图像保留在内存中。因此,我将每个图像放在磁盘上,然后将其分部分读回。

在多线程之前,伪代码如下所示:

for (y is 0 to ysize)
   unsigned short* ptr1 = ReadLineFromDisk(image1, y)
   unsigned short* ptr2 = ReadLineFromDisk(image2, y)
   unsigned short* ptr3 = ReadLineFromDisk(image3, y)
   unsigned short* ptr4 = ReadLineFromDisk(image4, y)
   unsigned short* outPtr = &(outImage[y*inXSize])
   for (x is 0 to xsize, ++x, ++ptr1, ++ptr2, ++ptr3, ++ptr4, ++outPtr){
        outPtr = combination of ptr1, ptr2, ptr3, ptr4;
   }
}

此代码在配备标准 500 GB 硬盘并使用高性能计数器的双核计算机上运行只需 3 秒。

如果将从磁盘读取的行数增加到 100 之类,然后使用如下代码逐步执行该操作:

chunksize = 100;
for (y is 0 to ysize by chunksize)
   unsigned short* ptr1 = ReadChunkFromDisk(image1, y)
   unsigned short* ptr2 = ReadChunkFromDisk(image2, y)
   unsigned short* ptr3 = ReadChunkFromDisk(image3, y)
   unsigned short* ptr4 = ReadChunkFromDisk(image4, y)
   unsigned short* outPtr = &(outImage[y*inXSize])
   for (x is 0 to xsize*chunk, ++x, ++ptr1, ++ptr2, ++ptr3, ++ptr4, ++outPtr){
        outPtr = combination of ptr1, ptr2, ptr3, ptr4;
   }
}

此代码比之前的代码更快,缩短至 1.5 秒。

问题 1:为什么该代码更快?

我假设它更快,因为根据我的经验,对于相同数量的数据,大型连续读取比较小的读取更快。也就是说,如果我一次读取 100 行数据,这比 100 次单独读取要快,至少对于常规(非 SSD)硬盘来说是这样。我的假设接近正确吗?

即便如此,处理器在这里也没有被密集使用。增加缓存大小实际上是令人望而却步的,因为 1.5 是我能得到的最好值,然后该值似乎会下降一点(也不知道为什么会这样,除了可能有一些磁盘缓存发挥了作用)。这让我想到

问题 2:为什么 block 大小会有最佳点?

如果我理解这里的事情(我不认为我真的理解),如果所有内容都可以在内存中,那么那将非常快,因为不会有磁盘命中。如果读取更多也会使速度更快,那么一次读取四分之一的图像不是只会稍微影响速度吗?

然后我转而将外循环放在 lambda 表达式中并使用 Intel's TBB线程化代码,例如:

chunksize = 100;
parallel_for (y is 0 to ysize by chunksize in a lambda expression)
   unsigned short* ptr1 = ReadChunkFromDisk(image1, y)
   unsigned short* ptr2 = ReadChunkFromDisk(image2, y)
   unsigned short* ptr3 = ReadChunkFromDisk(image3, y)
   unsigned short* ptr4 = ReadChunkFromDisk(image4, y)
   unsigned short* outPtr = &(outImage[y*inXSize])
   for (x is 0 to xsize*chunk, ++x, ++ptr1, ++ptr2, ++ptr3, ++ptr4, ++outPtr){
        outPtr = combination of ptr1, ptr2, ptr3, ptr4;
   }
}

此代码的速度范围为 0.4 秒到 1.6 秒。

这让我想到:

问题 3:速度提升最多不是 2 倍,而不是 4 倍吗?

这是我运行这些基准测试的一台双核机器,因此在完美的世界中,一个线程从磁盘读取,而其他进程则进行。即使以 4 倍速度提升运行,它也只使用 80% 的处理器,而不是 100%,因此仍然存在磁盘瓶颈。但 4 倍的增长意味着其他事情也在发生。

我还假设,速度差异的大范围是因为线程在读取时没有完全同步,如果速度增加就是这样发生的。真正的最后一个问题是:

问题 4:如何才能持续实现 4 倍的速度提升?

最佳答案

答案 1: 是的,您受到磁盘限制,因此 CPU 不会被固定那么多,是的,读取较大的 block 会更有效(只要 block 与磁盘缓存)

答案 2: 具有 8 MB 缓存且以 10k RPM 速度旋转的磁盘可能会获得 60 到 80 MB/秒的吞吐量,因此“最佳位置”是读取 block 与缓存大小对齐。 您可以增加缓冲区,但要使其与缓存大小保持一致:即 8MB、16MB、32MB 等。

答案 3: 理想情况下,您希望将一个线程专用于从磁盘读取数据,将另一个线程专用于处理数据(您可能希望使用多个线程进行处理)。多线程磁盘读取可能会有一些小的性能提升,但一般情况并非如此。我不知道为什么当您获得 4 倍的增长时,您会认为“其他事情”正在发生。

答案 3 更新: 坦率地说,我也不完全知道为什么会发生这种情况,但我也在 .NET 应用程序上的多线程磁盘 I/O 中看到过这种情况。事实上我什至have a C# test example which demonstrates the same kind of performance increase你注意到了。请注意,在我的测试中,我加载的 HTML 页面与您在“狂野”中看到的内容大致相同(每个页面大约 80 到 160 KB),因此我没有将读取与磁盘缓存对齐。 可能多个线程同时读取实际上更有效,因为尽管您正在进行多次读取,但您仍在利用磁盘缓存。当然,这只是一个即兴假设,我还没有证据支持所以请持保留态度!我认为,如果您的文件足够大,并且您的磁盘读取线程实际上有一个与磁盘缓存对齐的缓冲区,那么添加更多线程根本不会提高您的速度。 如果您仍然发现速度有所提高,请告诉我们!

答案 4: 请尝试以下操作:

  1. 使缓冲区与磁盘的缓存大小保持一致。
  2. 减少同时尝试访问磁盘的应用程序数量。
  3. 在内存中加载尽可能多的图像,并运行足够多的线程以充分利用您的 CPU(您将临时调整线程数量,尝试一下,看看“最佳点”在哪里)。
  4. 仅使用一个磁盘读取线程并确保其持续读取!!!

再说一遍,您受到磁盘限制,因此您可能永远无法真正获得 100% 的 CPU 利用率。

答案 4 更新:
我不认为英特尔的 TBB 实际上是导致您(和我)看到的性能提升的原因...正如我所说,我最好的猜测是多线程实际上可能更高效如果它们提供了更好的磁盘缓存利用率。我什至不确定这是否是一个正确的假设,所以不要在没有测试的情况下引用我!

阅读:
我找到了一篇非常详细的论文,标题为Asynchronous/Multi Threaded I/O on Commodity Systems with Multiple Disks – a performance study ,它对多线程 I/O 优于单线程 I/O 的情况进行了一些令人惊奇的分析和测试。看看第 86 页。

Dr. Dobbs also has an article on the subject ,虽然我没有机会阅读全文,但我只是浏览了一下。

关于multithreading - 为什么这段代码的多线程会导致如此不一致的计时?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/6339216/

相关文章:

c - 终止具有临界区代码的 POSIX 多线程应用程序的最佳方法是什么?

C++ boost 线程 : having a worker thread pause and unpause based on mutexes/conditions using a concurrent queue

python - 通过多处理减少内存占用?

benchmarking - 如何加载测试服务器发送事件?

performance - 对阻塞命令(例如 blpop)的 Redis 延迟进行基准测试?

java - 使用 join 和 InterruptedException 确定线程状态?

java - 同步块(synchronized block)内的 IllegalMonitorStateException

c++ - 写入数组时,最后一个线程比第一个线程执行得慢

javascript - CSS 动画性能基准

java - 有没有办法用 Java 代码对您的计算机进行基准测试?