9

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

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

4

7 回答 7

18

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

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

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

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

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

于 2012-11-27T16:21:16.067 回答
4

您的问题不是缓冲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
于 2012-11-27T16:20:07.523 回答
4

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

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

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

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

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

  • fwrite 本身通常是最快的,尽管如果您获得恰到好处的大小和迭代,缓冲版本有时可以击败它。
  • 天真的 Win32 明显慢
  • Buffered 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);
    }
于 2012-11-28T10:04:55.910 回答
1

首先最重要的是:小的 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中的缓冲区接口,但你可能需要更长的时间来阅读文档而不是自己编写它(恕我直言)。

于 2012-11-27T22:50:07.100 回答
0

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

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

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

于 2012-11-27T16:59:31.930 回答
0

这是一个测试,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 秒

如您所见,即使 LTO 也无助于 fwrite 的函数调用开销-d:case1 --passc:-flto --passl:-flto

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
于 2020-10-23T00:53:39.117 回答
-1

滚动自己的缓冲区应该很容易。但幸运的是,标准 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,因为从标准女巫缓冲区中不清楚是它(输入还是输出?)

于 2012-11-27T16:30:29.717 回答