unix 大量小写入的fwrite效率

yzckvree  于 2023-08-04  发布在  Unix
关注(0)|答案(8)|浏览(194)

我有一个程序,可以使用fwrite保存许多>1GB的大文件它工作正常,但不幸的是,由于数据的性质,每次调用fwrite只写1- 4字节。结果,写入可能花费超过一个小时,其中大部分时间似乎是由于系统调用开销(或至少在fwrite的库函数中)。fread也有类似的问题。
有人知道任何现有的/库函数,将缓冲这些写和读与内联函数,或者这是另一个滚你自己的?

wd2eg0qa

wd2eg0qa1#

首先,fwrite()是一个库,而不是一个系统调用。其次,它已经缓冲了数据。
您可能需要尝试增加缓冲区的大小。这是通过使用setvbuf()来完成的。在我的系统上,这只有一点点帮助,但YMMV。
如果setvbuf()不起作用,您可以自己进行缓冲,只有在积累了足够的数据后才调用fwrite()。这涉及到更多的工作,但几乎肯定会加快编写速度,因为您自己的缓冲可以做得比fwrite()的更轻量级。

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

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

v7pvogib

v7pvogib2#

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

字符串

dphi5xsq

dphi5xsq3#

好吧,那很有趣。我想我应该写一些实际的代码来看看速度是多少。这就是了使用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);
    }

字符串

ghhkc1vu

ghhkc1vu4#

如果你只从一个线程写入,请尝试使用fwrite_unlocked。在这种情况下,相对于直fwrite来说,它确实是奇迹。

uqcuzwp8

uqcuzwp85#

首先也是最重要的:小的fwrites()* 慢,因为每个fwrites都要测试其参数的有效性,做等价于flockfile()的操作,可能是fflush(),追加数据,返回success:这个开销加起来--不像对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中的buffer接口,但是你可能会花更长的时间来阅读文档,而不是自己写。

pqwbnv8z

pqwbnv8z6#

stdio中FILE * 层的意义在于它为您完成了缓冲。这可以节省系统调用开销。正如其他人所指出的,有一件事可能仍然是一个问题,那就是库调用开销,这个开销要小得多。另一件可能会困扰你的事情是同时写入磁盘上的许多不同位置。(磁盘旋转,磁头大约需要8毫秒才能到达正确的位置进行随机写入。)
如果您确定库调用开销是问题所在,我建议您使用vector滚动您自己的琐碎缓冲,并定期将vector刷新到文件中。
如果问题是有大量的写操作分散在磁盘上,请尝试使用setvbuf()提升缓冲区大小。如果可以的话,尝试每个文件4MB左右的数字。

rryofs0p

rryofs0p7#

下面是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

字符串

bis0qfac

bis0qfac8#

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

相关问题