【问题标题】:efficiency of fwrite for massive numbers of small writesfwrite 处理大量小写的效率
【发布时间】:2012-11-15 07:27:37
【问题描述】:

我有一个使用fwrite 保存许多大于1GB 的大文件的程序它工作正常,但不幸的是,由于数据的性质,每次调用fwrite 只写入1-4 字节。结果写入可能需要一个多小时,其中大部分时间似乎是由于系统调用开销(或至少在 fwrite 的库函数中)。 fread 也有类似的问题。

有谁知道任何现有的/库函数将使用内联函数缓冲这些写入和读取,或者这是您自己的另一个滚动?

【问题讨论】:

  • 根据这个问题,fwrite 无论如何都会被缓冲:stackoverflow.com/questions/2806104/…
  • 您是否考虑过只写入标准输出并让 bash 等写入文件?
  • @PhilH:Bash 和这个有什么关系?
  • @larsmans:它是显式写入文件的替代方案;从 bash 运行文件并让它处理缓冲和磁盘 IO。
  • @PhilH:这不会改变任何事情。使用> 重定向只会导致shell 打开文件并将文件描述符传递给进程。它不会导致所有 I/O 都通过 shell,感谢上帝。

标签: c++ unix fwrite system-calls


【解决方案1】:

您的问题不是fwrite() 的缓冲,而是使用少量数据进行库调用的总开销。如果你只写 1MB 的数据,你会进行 250000 次函数调用。您最好尝试在内存中收集数据,然后通过一次调用fwrite() 将数据写入磁盘。

更新:如果您需要证据:

$ dd if=/dev/zero of=/dev/null count=50000000 bs=2
50000000+0 records in
50000000+0 records out
100000000 bytes (100 MB) copied, 55.3583 s, 1.8 MB/s
$ dd if=/dev/zero of=/dev/null count=50 bs=2000000
50+0 records in
50+0 records out
100000000 bytes (100 MB) copied, 0.0122651 s, 8.2 GB/s

【讨论】:

  • 所以不要调用 fwrite,而是使用内存缓冲区和当前的写/读指针,在满/空时刷新/填充缓冲区并重新开始。
  • @Skizz 请告诉我们你是如何生成数据的,然后你可能会得到建议。但通常std::vector<your_stuff> 应该通过指针、写入、刷新来解决问题,最后您只需要一个fwrite()。或者更多,不时。
  • 关于计时,GNU dd 不使用 fwrite()。假设您的dd 相同,则时间与问题无关。
  • 这个答案是完全错误的。查看 NPE 答案和 cmets(或我的 c++ 解决方案)以节省您的时间。
  • 这个答案极具误导性。带有 bs=2 的 dd 实际上每两个字节向内核发出一个写入系统调用。启用默认缓冲的 fwrite 将是每两个字节调用一次本地库函数,然后在每次缓冲区满时进行一次写入系统调用。主要开销是内核调用,因此 dd bs=2 不是对 2 字节块的 fwrite 的准确模拟。
【解决方案2】:

首先,fwrite() 是一个库而不是系统调用。其次,它已经缓冲了数据。

您可能想尝试增加缓冲区的大小。这是通过使用setvbuf() 完成的。在我的系统上,这只是一点点帮助,但 YMMV。

如果setvbuf() 没有帮助,您可以进行自己的缓冲,只有在积累了足够的数据后才调用fwrite()。这涉及更多工作,但几乎可以肯定会加快写入速度,因为您自己的缓冲可以比fwrite() 的更轻量级。

编辑:如果有人告诉你问题出在fwrite() 电话的绝对数量上,请要求查看证据。更好的是,做你自己的性能测试。在我的计算机上,使用fwrite() 进行 500,000,000 次双字节写入需要 11 秒。这相当于大约 90MB/s 的吞吐量。

最后但并非最不重要的一点是,我的测试中的 11 秒与您问题中提到的一小时之间的巨大差异暗示您的代码中可能还有其他原因导致性能非常差。

【讨论】:

  • 问题不在于缓冲,而在于调用 fwrite 的剪切次数。
  • @Skizz:是什么让你这么认为?如果你有任何证据,我很乐意看到。
  • 好吧,以 1-4 字节的块写入 1 GB 的数据是非常多的 fwrite 调用。
  • @Skizz:这不完全是证据,是吗?
  • 我同意 NPE。 fwrite 不是系统调用!!多次调用它是免费的。说相反的人需要回到学校。您可以设置一个足够大的缓冲区来减少底层系统调用,即“write(fd,void*,int)”函数。
【解决方案3】:

滚动您自己的缓冲区应该很容易。但幸运的是,标准 C++ 有你所要求的。 只需使用 std::ofstream :

//open and init
char mybuffer [1024];
std::ofstream filestr("yourfile");
filestr.rdbuf()->pubsetbuf(mybuffer,1024);
// write your data
filestr.write(data,datasize);

已编辑:错误,使用 ofstream 而不是 fstream,因为从标准的女巫缓冲区中不清楚是它(输入还是输出?)

【讨论】:

    【解决方案4】:

    stdio 中的 FILE * 层的意义在于它为你做缓冲。这使您免于系统调用开销。正如其他人所指出的,可能仍然存在问题的一件事是库调用开销,它要小得多。另一件可能会让您感到困扰的事情是同时写入磁盘上的许多不同位置。 (磁盘旋转,磁头需要 8 毫秒才能到达正确的位置进行随机写入。)

    如果您确定库调用开销是问题所在,我建议您使用向量滚动您自己的微不足道的缓冲,并定期将向量刷新到文件中。

    如果问题是您有大量写入分散在整个磁盘上,请尝试使用 setvbuf() 增加缓冲区大小。如果可以,请尝试每个文件 4MB 左右的数字。

    【讨论】:

      【解决方案5】:

      首先:小的 fwrites() 比较慢,因为每个 fwrite 都必须测试其参数的有效性,做相当于flockfile(),可能是fflush(),追加数据,返回成功:这个开销加起来 - 与其说是对 write(2) 的微小调用,但它仍然很明显。

      证明:

      #include <stdio.h>
      #include <stdlib.h>
      
      static void w(const void *buf, size_t nbytes)
      {
          size_t n;
          if(!nbytes)
              return;
          n = fwrite(buf, 1, nbytes, stdout);
          if(n >= nbytes)
              return;
          if(!n) {
              perror("stdout");
              exit(111);
          }
          w(buf+n, nbytes-n);
      }
      
      /* Usage: time $0 <$bigfile >/dev/null */
      int main(int argc, char *argv[])
      {
          char buf[32*1024];
          size_t sz;
      
          sz = atoi(argv[1]);
          if(sz > sizeof(buf))
              return 111;
          if(sz == 0)
              sz = sizeof(buf);
          for(;;) {
              size_t r = fread(buf, 1, sz, stdin);
              if(r < 1)
                  break;
              w(buf, r);
          }
          return 0;
      }
      

      话虽如此,您可以执行许多评论者建议的操作,即在 fwrite 之前添加您自己的缓冲:这是非常简单的代码,但您应该测试它是否真的给您带来任何好处。

      如果你不想自己动手,你可以使用例如skalibs 中的缓冲区接口,但你可能会花更长的时间来阅读文档而不是自己编写文档(恕我直言)。

      【讨论】:

        【解决方案6】:

        好吧,那很有趣。我想我会写一些实际的代码来看看速度是多少。就在这里。使用 C++ DevStudio 2010 Express 编译。这里有相当多的代码。它计算了 5 种写入数据的方式:-

        • 天真地调用 fwrite
        • 使用缓冲区并使用更大的缓冲区减少对 fwrite 的调用
        • 天真地使用 Win32 API
        • 使用缓冲区并使用更大的缓冲区减少对 Win32 的调用
        • 使用 Win32 但双缓冲输出并使用异步写入

        请检查我没有对上述任何一项做过一些愚蠢的事情。

        程序使用 QueryPerformanceCounter 对代码进行计时,并在文件关闭后结束计时以尝试包含任何待处理的内部缓冲数据。

        我机器上的结果(一个旧的 WinXP SP3 盒子):-

        • fwrite 本身通常是最快的,但如果大小和迭代恰到好处,缓冲版本有时可以击败它。
        • Naive Win32 速度明显变慢
        • 缓冲的 Win32 速度翻倍,但仍被 fwrite 轻松击败
        • 异步写入并不比缓冲版本好很多。也许有人可以检查我的代码并确保我没有做过愚蠢的事情,因为我以前从未真正使用过异步 IO。

        根据您的设置,您可能会得到不同的结果。

        随意编辑和改进代码。

            #define _CRT_SECURE_NO_WARNINGS
        
            #include <stdio.h>
            #include <memory.h>
            #include <Windows.h>
        
            const int
                // how many times fwrite/my_fwrite is called
                c_iterations = 10000000,
                // the size of the buffer used by my_fwrite
                c_buffer_size = 100000;
        
            char 
                buffer1 [c_buffer_size],
                buffer2 [c_buffer_size],
                *current_buffer = buffer1;
        
            int
                write_ptr = 0;
        
            __int64
                write_offset = 0;
        
            OVERLAPPED
                overlapped = {0};
        
            // write to a buffer, when buffer full, write the buffer to the file using fwrite
            void my_fwrite (void *ptr, int size, int count, FILE *fp)
            {
                const int
                    c = size * count;
        
                if (write_ptr + c > c_buffer_size)
                {
                    fwrite (buffer1, write_ptr, 1, fp);
                    write_ptr = 0;
                }
        
                memcpy (&buffer1 [write_ptr], ptr, c);
                write_ptr += c;
            }
        
            // write to a buffer, when buffer full, write the buffer to the file using Win32 WriteFile
            void my_fwrite (void *ptr, int size, int count, HANDLE fp)
            {
                const int
                    c = size * count;
        
                if (write_ptr + c > c_buffer_size)
                {
                    DWORD
                        written;
        
                    WriteFile (fp, buffer1, write_ptr, &written, 0);
                    write_ptr = 0;
                }
        
                memcpy (&buffer1 [write_ptr], ptr, c);
                write_ptr += c;
            }
        
            // write to a double buffer, when buffer full, write the buffer to the file using 
            // asynchronous WriteFile (waiting for previous write to complete)
            void my_fwrite (void *ptr, int size, int count, HANDLE fp, HANDLE wait)
            {
                const int
                    c = size * count;
        
                if (write_ptr + c > c_buffer_size)
                {
                    WaitForSingleObject (wait, INFINITE);
        
                    overlapped.Offset = write_offset & 0xffffffff;
                    overlapped.OffsetHigh = write_offset >> 32;
                    overlapped.hEvent = wait;
        
                    WriteFile (fp, current_buffer, write_ptr, 0, &overlapped);
                    write_offset += write_ptr;
                    write_ptr = 0;
                    current_buffer = current_buffer == buffer1 ? buffer2 : buffer1;
                }
        
                memcpy (current_buffer + write_ptr, ptr, c);
                write_ptr += c;
            }
        
            int main ()
            {
                // do lots of little writes
                FILE
                    *f1 = fopen ("f1.bin", "wb");
        
                LARGE_INTEGER
                    f1_start,
                    f1_end;
        
                QueryPerformanceCounter (&f1_start);
        
                for (int i = 0 ; i < c_iterations ; ++i)
                {
                    fwrite (&i, sizeof i, 1, f1);
                }
        
                fclose (f1);
        
                QueryPerformanceCounter (&f1_end);
        
                // do a few big writes
                FILE
                    *f2 = fopen ("f2.bin", "wb");
        
                LARGE_INTEGER
                    f2_start,
                    f2_end;
        
                QueryPerformanceCounter (&f2_start);
        
                for (int i = 0 ; i < c_iterations ; ++i)
                {
                    my_fwrite (&i, sizeof i, 1, f2);
                }
        
                if (write_ptr)
                {
                    fwrite (buffer1, write_ptr, 1, f2);
                    write_ptr = 0;
                }
        
                fclose (f2);
        
                QueryPerformanceCounter (&f2_end);
        
                // use Win32 API, without buffer
                HANDLE
                    f3 = CreateFile (TEXT ("f3.bin"), GENERIC_WRITE, 0, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);
        
                LARGE_INTEGER
                    f3_start,
                    f3_end;
        
                QueryPerformanceCounter (&f3_start);
        
                for (int i = 0 ; i < c_iterations ; ++i)
                {
                    DWORD
                        written;
        
                    WriteFile (f3, &i, sizeof i, &written, 0);
                }
        
                CloseHandle (f3);
        
                QueryPerformanceCounter (&f3_end);
        
                // use Win32 API, with buffer
                HANDLE
                    f4 = CreateFile (TEXT ("f4.bin"), GENERIC_WRITE, 0, 0, CREATE_ALWAYS, FILE_FLAG_WRITE_THROUGH, 0);
        
                LARGE_INTEGER
                    f4_start,
                    f4_end;
        
                QueryPerformanceCounter (&f4_start);
        
                for (int i = 0 ; i < c_iterations ; ++i)
                {
                    my_fwrite (&i, sizeof i, 1, f4);
                }
        
                if (write_ptr)
                {
                    DWORD
                        written;
        
                    WriteFile (f4, buffer1, write_ptr, &written, 0);
                    write_ptr = 0;
                }
        
                CloseHandle (f4);
        
                QueryPerformanceCounter (&f4_end);
        
                // use Win32 API, with double buffering
                HANDLE
                    f5 = CreateFile (TEXT ("f5.bin"), GENERIC_WRITE, 0, 0, CREATE_ALWAYS, FILE_FLAG_OVERLAPPED | FILE_FLAG_WRITE_THROUGH, 0),
                    wait = CreateEvent (0, false, true, 0);
        
                LARGE_INTEGER
                    f5_start,
                    f5_end;
        
                QueryPerformanceCounter (&f5_start);
        
                for (int i = 0 ; i < c_iterations ; ++i)
                {
                    my_fwrite (&i, sizeof i, 1, f5, wait);
                }
        
                if (write_ptr)
                {
                    WaitForSingleObject (wait, INFINITE);
        
                    overlapped.Offset = write_offset & 0xffffffff;
                    overlapped.OffsetHigh = write_offset >> 32;
                    overlapped.hEvent = wait;
        
                    WriteFile (f5, current_buffer, write_ptr, 0, &overlapped);
                    WaitForSingleObject (wait, INFINITE);
                    write_ptr = 0;
                }
        
                CloseHandle (f5);
        
                QueryPerformanceCounter (&f5_end);
        
                CloseHandle (wait);
        
                LARGE_INTEGER
                    freq;
        
                QueryPerformanceFrequency (&freq);
        
                printf ("  fwrites without buffering = %dms\n", (1000 * (f1_end.QuadPart - f1_start.QuadPart)) / freq.QuadPart);
                printf ("     fwrites with buffering = %dms\n", (1000 * (f2_end.QuadPart - f2_start.QuadPart)) / freq.QuadPart);
                printf ("    Win32 without buffering = %dms\n", (1000 * (f3_end.QuadPart - f3_start.QuadPart)) / freq.QuadPart);
                printf ("       Win32 with buffering = %dms\n", (1000 * (f4_end.QuadPart - f4_start.QuadPart)) / freq.QuadPart);
                printf ("Win32 with double buffering = %dms\n", (1000 * (f5_end.QuadPart - f5_start.QuadPart)) / freq.QuadPart);
            }
        

        【讨论】:

        • 我应该补充一点,我将程序构建为 Windows 控制台应用程序。
        • 酷!你得到了什么结果?
        【解决方案7】:

        这是nim 中的一个测试,表明fwrite 引入了函数调用开销,并且您端的批处理减少了时钟时间。

        batchPow 从 0 增加到 10,时钟时间从 36 秒减少到 4 秒 nim r -d:case1 -d:danger --gc:arc main.nim | wc -l 36 秒

        nim r -d:case2 -d:danger --gc:arc -d:batchPow:10 main.nim | wc -l 4 秒

        正如您在-d:case1 --passc:-flto --passl:-flto 中看到的那样,即使是 LTO 也无助于 fwrite 的函数调用开销

        var buf: string
        let n = 1000_000_000
        for i in 0..<n:
          let c = cast[char](i)
          when defined case1: # 36 seconds
            stdout.write c
          when defined case2: # 4 seconds
            const batchPow {.intdefine.} = 10
            buf.add c
            if ((i and (2 shl batchPow - 1)) == 0) or (i == n-1):
              stdout.write buf
              buf.setLen 0
        

        【讨论】:

          猜你喜欢
          • 2011-05-20
          • 2023-03-12
          • 1970-01-01
          • 1970-01-01
          • 2019-12-31
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多