fwrite对于大量小写的效率

时间:2012-11-27 16:07:13

标签: c++ unix fwrite system-calls

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

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

7 个答案:

答案 0 :(得分:9)

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

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

如果setvbuf()无效,您可以自行缓冲,只有在积累了足够的数据后才能拨打fwrite()。这涉及更多的工作,但几乎肯定会加快写作速度,因为你自己的缓冲可以变得更加轻量级fwrite()

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

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

答案 1 :(得分:6)

你的问题不是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

答案 2 :(得分:2)

好的,好吧,这很有意思。我以为我会写一些实际的代码来看看速度是多少。在这里。使用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);
    }

答案 3 :(得分:0)

首先:小fwrites() 慢,因为每个fwrite必须测试其参数的有效性,相当于flockfile(),可能是fflush(),附加数据,返回成功:这种开销加起来 - 与写入(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中的缓冲界面,但是您可能需要更长的时间来阅读文档而不是自己编写(imho)。

答案 4 :(得分: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秒

就像您在-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

答案 5 :(得分:-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,因为从标准的女巫缓冲区中不清楚它(输入还是输出?)

答案 6 :(得分:-1)

stdio中FILE *图层的意思是它为你做缓冲。这可以节省系统调用开销。正如其他人所指出的那样,一件可能仍然存在问题的问题是库调用开销,它要小得多。可能会咬你的另一件事是同时写入磁盘上的许多不同位置。 (磁盘旋转,头部需要8ms才能到达正确的位置进行随机写入。)

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

如果问题是您在整个磁盘上分散了大量写入内容,请尝试使用setvbuf()升级缓冲区大小。如果可以,请尝试每个文件大约4MB的数字。