在C++中真正的异步文件IO

13

我有一个超级快的M.2驱动器,它有多快并不重要,因为我无论如何都不能利用这个速度。这就是为什么我问这个问题的原因。

我有一个需要大量内存的应用程序,很多时候它无法全部放入RAM中,但幸运的是,它并不需要一次性全部使用。相反,它用于保存计算过程中的中间结果。

不幸的是,该应用程序无法快速读写这些数据。我尝试使用多个读取器和写入器线程,但结果只会更糟(后来我看到了这个)。

因此,我的问题是:在C++中是否可能拥有真正的异步文件IO以充分利用那些宣传的每秒几十亿字节?如果可以的话,如何实现(以跨平台的方式)?

如果您知道一个好的支持这种任务的库,也可以给我推荐一个,因为我相信没有必要重新发明轮子。

编辑:

这里有一个展示我如何在程序中进行文件IO的代码示例。它不是来自上述程序,因为那样不太简洁。尽管如此,这个示例仍然说明了问题。不用在意Windows.h,它仅用于设置线程亲和力。在实际程序中我也设置了亲和力,这就是为什么我包含它的原因。

#include <fstream>
#include <thread>
#include <memory>
#include <string>

#include <Windows.h> // for SetThreadAffinityMask()

void stress_write(unsigned bytes, int num)
{
    std::ofstream out("temp" + std::to_string(num));
    for (unsigned i = 0; i < bytes; ++i)
    {
        out << char(i);
    }
}

void lock_thread(unsigned core_idx)
{
    SetThreadAffinityMask(GetCurrentThread(), 1LL << core_idx);
}

int main()
{
    std::ios_base::sync_with_stdio(false);
    lock_thread(0);

    auto worker_count = std::thread::hardware_concurrency() - 1;

    std::unique_ptr<std::thread[]> threads = std::make_unique<std::thread[]>(worker_count); // faster than std::vector

    for (int i = 0; i < worker_count; ++i)
    {
        threads[i] = std::thread(
            [](unsigned idx) {
                lock_thread(idx);
                stress_write(1'000'000'000, idx);
            },
            i + 1
        );
    }
    stress_write(1'000'000'000, 0);

    for (int i = 0; i < worker_count; ++i)
    {
        threads[i].join();
    }
}

正如你所看到的,这只是普通的fstream。在我的机器上,它使用了100%的CPU,但只使用了7-9%的磁盘(约190MB/s)。我想知道它是否可以提高。


3
请发布您正在使用的代码,也许我们可以发现性能问题? - cds84
2
你有没有想过除了显式地读写文件之外的其他方法?比如将文件进行内存映射 - Some programmer dude
1
(a) 硬盘的速度和使用量确实很重要。很容易误读性能信息(比特与字节,随机与顺序,读取与写入)。 (b) 许多读写线程使情况更糟可能是您实际上饱和了硬盘的症状。 (c) 解决问题的方法可能涉及理解您的业务逻辑。如果您使用了1%、10%、50%或80%的带宽,则下一步改善带宽使用的方法将会非常不同。 - Yakk - Adam Nevraumont
4
如果您能提供一个具体、完整、最小化的带宽问题实例(其他人可以复制/粘贴并重现!),并附上展示您接近饱和带宽的基准测试,那么您将从Stack Overflow的问题中获得最佳结果。我(以及其他人)可以给出大量加快速度的建议,但是哪个建议适合您取决于您没有分享的细节,因此我们会在黑暗中摸索。(例如,找到或扩展具有.then方法的future,将该future附加到伪执行器并排队一堆工作) - Yakk - Adam Nevraumont
1
唯一接近任何驱动器列出的最大传输速率的方法是使用特定于操作系统的未缓冲I/O例程。如果您使用普通的C++库,您会得到很多内存复制:可能C++例程有一个缓冲区,而操作系统I/O调用(将由C++库调用)有一个缓冲区(磁盘缓存)。如果您提前知道需要什么数据,还可以利用操作系统支持的异步I/O调用。 - 1201ProgramAlarm
显示剩余10条评论
3个回答

14

要获得(最多)10倍的加速,最简单的方法是更改以下内容:

void stress_write(unsigned bytes, int num)
{
  std::ofstream out("temp" + std::to_string(num));
  for (unsigned i = 0; i < bytes; ++i)
  {
    out << char(i);
  }
}

转化为:

void stress_write(unsigned bytes, int num)
{
  constexpr auto chunk_size = (1u << 12u); // tune as needed
  std::ofstream out("temp" + std::to_string(num));
  for (unsigned chunk = 0; chunk < (bytes+chunk_size-1)/chunk_size; ++chunk)
  {
    char chunk_buff[chunk_size];
    auto count = (std::min)( bytes - chunk_size*chunk, chunk_size );
    for (unsigned j = 0; j < count; ++j)
    {
      unsigned i = j + chunk_size*chunk;
      chunk_buff[j] = char(i); // processing
    }
    out.write( chunk_buff, count );
  }
}

我们将写入的数据分组,每组最多4096个字节,然后再发送到标准ofstream。

流操作有一些令人讨厌的虚拟调用,在每次只写入少量字节时对编译器难以优化,这些调用会影响性能。

通过将数据分块成较大的片段,我们使得虚函数表(vtable)查找变得不再频繁,从而不会影响性能。

详见此StackOverflow帖子


要获得最佳性能,您可能需要使用boost.asio或访问平台原始的异步文件IO库。

但是,当您使用CPU占用率高的情况下只使用了驱动器带宽的<10%时,首先应该考虑简单易行的方法。


2

在这里,分块 I/O 确实是最重要的优化,而且在大多数情况下应该足够了。然而,对于关于 异步 I/O 的确切问题的直接答案如下。

Boost::Asio 在版本 1.21.0 中添加了对文件操作的支持。其接口与 Asio 的其余部分类似。

首先,我们需要创建一个表示文件的对象。最常见的用例将使用 random_access_filestream_file 中的一个。在本示例代码中,流文件就足够了。

通过 async_read_some 进行读取,但通常可以使用 async_read 辅助函数一次性读取特定数量的字节。

如果操作系统支持,这些操作确实在后台运行并且使用很少的处理器时间。Windows 和 Linux 都支持此功能。


1

如果你想提高磁盘 I/O 的性能,就不要再考虑 C++ 流 I/O 了,因为早已证明它们是最慢的之一。相反,你可以尝试使用低级别的 C I/O,例如 FILE*(fopen、fread、fwrite)。你会立即注意到性能的提升。此外,正如其他人在这里建议的那样,尝试使用专用线程进行 I/O,并以块读写数据,理想情况下块大小应该等于扇区大小。在 SSD 的情况下,你需要找到最佳值来调整。如果这还不够,尝试使用低级别的操作系统特定调用,例如 Windows 中的 overlapped I/O 或完成端口,在 Linux 中则可能使用 epoll。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接