【问题标题】:fwrite() performance well below disk capacityfwrite() 性能远低于磁盘容量
【发布时间】:2014-03-23 18:04:57
【问题描述】:

我有一个动态分配的 struct 数组,包含 1700 万个元素。为了将它保存到磁盘,我写了

fwrite(StructList, sizeof(Struct), NumStructs, FilePointer)

在后面的步骤中,我使用等效的 fread 语句读取它,即使用 sizeof(Struct) 和计数 NumStructs。我预计生成的文件大约为 3.5 GB(这都是 x64)。

是否可以通过 sizeof(Struct) * NumStructs 作为大小和 1 作为计数来加快速度?我对为什么在具有 32 GB RAM(大量写入缓存)的快速计算机上写入操作可能需要 分钟 感到头疼。我已经运行了自制的基准测试,并且缓存足够激进,前 800 MB 到 1 GB 的速度通常为 400 MB/秒。 PerfMon 显示它在 fwrite 期间消耗了一个内核的 100%。

我看到了here 的问题,所以我要问的是,fwrite 中是否有一些循环可以通过告诉它写入 1 个大小为 n*s 而不是 n 的元素来“欺骗”它更快地运行大小为 s 的元素。

编辑

我在发布模式下运行了两次,两次都放弃了等待。然后我在调试模式下运行它,知道通常fwrite 操作需要更长的时间。要写入的数据的确切大小是 4,368,892,928 字节。在所有这三种情况下,PerfMon 都显示了两次磁盘写入活动的爆发,间隔大约 30 秒,之后 CPU 达到一个内核的 100%。该文件当时为 73,924,608 字节。我在fwrite 的两侧都有断点,所以我知道它所在的位置。确实似乎有什么东西卡住了,但我会让它运行一夜然后看看。

编辑

一夜之间,它肯定挂在fwrite,文件从未超过 70 MB。

【问题讨论】:

  • 你试过了吗?为了防止缓存妨碍您,您可以使用其他应用程序来占用内存的其余部分。
  • 这样改变参数不会影响性能;如果有一个简短的写入,它只会影响错误报告。 IMO,您无能为力来提高性能。通过只需要一次调用,您已将系统调用开销降至最低;从那里开始,问题是 o/s 可以多快分配 3.5 GiB 磁盘空间。您可以寻找系统调用来预配置文件大小并使分配尽可能接近顺序,但这是非常特定于平台的。我看到您的标签中有 Windows;我无能为力...
  • 如果您的程序正在燃烧 100% 核心,那么它不会被磁盘卡住。这使您认为它与 fwrite() 有任何关系的假设非常弱。不信任任何反恶意软件并使用分析器。
  • 1.您确实知道 fwrite 使用缓冲区 - 原始的 write 会更好。 2. 改变设计——为什么要从磁盘读取/写入大量数据。
  • 您没有提供编译器或运行时库。您的文件足够大,字节数足以溢出 32 位无符号 size_t。一个可怜的或旧的 crtl 可能会搞砸这个。

标签: c windows visual-studio-2012 file-io


【解决方案1】:

这绝对是fwrite 的问题(我试过VS2012 和2010)。

从一个标准 C++ 项目开始,我仅将设置更改为在静态链接中使用多字节字符集、x64 目标和标准库的多线程调试版本。

以下代码成功(为了简洁没有错误检查):

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>

int main()
{
    FILE *fp;
    long long n;
    unsigned char *data;

    n = 4LL * 1024 * 1024 * 1024 - 1;

    data = (unsigned char *)malloc(n * sizeof(unsigned char));

    fp = fopen("T:\\test.bin", "wb");

    fwrite(data, sizeof(unsigned char), n, fp);

    fclose(fp);
}

在我机器上的调试版本中,程序大约在 1 分钟内完成(malloc 只需要几秒钟,所以这主要是fwrite),平均消耗 30% 的 CPU。 PerfMon 显示写入完全发生在最后是一个 4 GB 的“闪存”(写入缓存)。

在 n 的分配中将 - 1 更改为 + 1 并重现问题:瞬时 100% 的 CPU 使用率并且没有写入任何内容。几分钟后,文件的大小仍然是 0 字节(回想一下,在我的实际代码中,它设法转储了 70 MB 左右)。

这肯定是fwrite的问题,因为下面的代码可以写文件就好了:

int main()
{
    FILE *fp;
    long long n;
    long long counter = 0;
    long long chunk;
    unsigned char *data;

    n = 4LL * 1024 * 1024 * 1024 + 1;

    data = (unsigned char *)malloc(n * sizeof(unsigned char));

    fp = fopen("T:\\test.bin", "wb");

    while (counter < n)
    {
        chunk = min(n - counter, 100*1000);
        fwrite(data+counter, sizeof(unsigned char), chunk, fp);
        counter += chunk;
    }

    fclose(fp);
}

在我的机器上,这需要 45 秒而不是 1 分钟。 CPU 使用率不是恒定的,它是突发的,报告的 IO 写入比“单块”方法更分散。

如果速度的提高是错误的(即由于缓存),我会感到非常惊讶,因为我在编写几个包含所有相同数据的文件与包含随机数据的文件和报告的写入速度(与缓存)是一样的。所以我敢打赌,至少fwrite 的这种实现不喜欢一次传递给它的大块。

我还测试了fread 在 4 GB+1 的情况下关闭文件进行写入后立即读取并及时返回 - 最多几秒钟(这里没有真实数据所以我没有检查它)。

编辑

我使用块写入方法和 4 GB-1 文件的单个 fwrite 调用运行了一些测试(这两种方法都可以做到的最大大小)。多次运行程序(使用这样的代码打开文件,用多个 fwrite 调用编写,关闭,然后再次打开,在单个调用中编写,然后关闭),毫无疑问,块写入方法返回得更快。在最坏的情况下,它会以 68% 的时间返回,而我最多只有 20%。

【讨论】:

  • 是的,它看起来像一个 32 位包装错误。关于性能,请记住malloc 如果它为大块保留 VM 空间但在触及内存之前不初始化交换,则可以向前支付成本。我不知道VC malloc 是否这样做。但如果是这样,这可能会导致 write 和 pager 之间发生奇怪的交互。在写入之前尝试初始化内存会很有趣。
【解决方案2】:

这不是fwrite问题,而是有意的(虽然不酷)行为:

fwrite() 函数应从ptr 指向的数组中,将大小由size 指定的nitems 元素写入stream 指向的流。 对于每个对象,size 应调用 fputc() 函数,从数组中获取值(按顺序)[...]

因此,基本上,通过正确使用fwrite不作弊,您将请求数十亿次调用fputc
考虑到上述要求,很明显您必须如何作弊才能使其正常工作。

【讨论】:

  • 我不明白你说的作弊是什么?并找到您的来源。
  • 如果您正在编写大小为 Y 的 X 结构,并且您告诉 C 标准库改为编写一个大小为 X*Y 的对象,这实际上是“作弊”。你没有说出你正在做的事情的真相。标准引用来自pubs.opengroup.org/onlinepubs/9699919799/functions/fwrite.html,但如果您愿意,可以参考 ISO 9899 (C99) 的 17.9.8.2 中的相同措辞。
  • 我不像你那样阅读它。对我来说,这说明 fwrite 有两个嵌套循环,其中是对 fputc 的调用。正如你所说,作弊除了返回值的粒度(即知道实际写入了多少项目)之外没有任何区别
  • 好吧,如果您不相信,请继续测量。我在我的系统(Win7 64,使用 MinGW-gcc 4.8.2)上运行了一个测试,执行 500k 写入,每个结构大小为 16 的 1M,如果你告诉 C 标准库你在做什么的真相,它需要 11.9 秒, 如果你告诉它写 16 倍的大小为 1 的对象,则需要 12.1 秒,但如果你告诉它你正在写一个巨大的对象,则只有 7.2 秒。每个测试重新运行 5 次,时间一致,相差 +/- 0.2 秒。 fwrite 内的循环运行方式肯定会有所不同。
  • 您应该发布一个答案here 并附上您的发现。我会用我的测试程序试一试,然后告诉你。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-03-23
  • 1970-01-01
  • 2015-06-27
  • 2011-02-19
  • 2016-01-26
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多