fwrite 比 Windows 中的 WriteFile 快吗?

Posted

技术标签:

【中文标题】fwrite 比 Windows 中的 WriteFile 快吗?【英文标题】:Is fwrite faster than WriteFile in windows? 【发布时间】:2012-12-26 18:05:27 【问题描述】:

我一直认为WriteFile比fwrite效率更高,因为fwrite在内部调用WriteFile,但是下面的测试代码告诉我fwrite明显比WriteFile快。

fwrite 花费 2 毫秒,而 WriteFile 需要 27000(FILE_ATTRIBUTE_NORMAL),每次写入调用后都会刷新。如果我用 FILE_FLAG_WRITE_THROUGH 调用 WriteFile,并注释 FlushFileBuffers(wfile) 行,WriteFile 会更快,它花费 800。

那么 fwrite 真的会调用 WriteFile 吗?是什么造成了如此巨大的差异? fwrite 在内部是如何工作的?如何使用 API 比 fwrite 更高效地将数据写入文件?(无缓冲,同步)。

   #include <Windows.h>
   #include <stdio.h>
   #include <iostream>

   int main() 
     FILE* cfile = fopen("file1.txt", "w");
     HANDLE wfile = CreateFile("file2.txt", GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, 
           /*FILE_ATTRIBUTE_NORMAL*/FILE_FLAG_WRITE_THROUGH, NULL);
     DWORD written = 0;

     DWORD start_time, end_time;
     char * text = "test message ha ha ha ha";
     int size = strlen(text);
     int times = 999;

     start_time = timeGetTime();
     for(int i = 0; i < times; ++i) 
       fwrite(text, 1, size, cfile);
       fflush(cfile);
     
     end_time = timeGetTime();
     std::cout << end_time - start_time << '\n';

     start_time = timeGetTime();
     for(int i = 0; i < times; ++i) 
         WriteFile(wfile, text, size, &written, NULL);
         //FlushFileBuffers(wfile);
     
     end_time = timeGetTime();
     std::cout << end_time - start_time << std::endl;

     system("pause");
     return 0;
   

更新: 感谢您的回答,这里是答案: 查看VS目录\VS\crt\src\fflush.c:

    //fflush.c
    int __cdecl _fflush_nolock (FILE *str) 
        //irrelevant codes
        if (str->_flag & _IOCOMMIT) 
                return (_commit(_fileno(str)) ? EOF : 0);
        
        return 0;
    

所以这里有一个 _IOCOMMIT 标志,然后查看 ...\src\fdopen.c

    FILE * __cdecl _tfdopen (int filedes, const _TSCHAR *mode) 
      //irrelevant codes
        while(*++mode && whileflag)
          switch(*mode) 
      //...
              case _T('c'):
                if (cnflag)
                    whileflag = 0;
                else 
                    cnflag = 1;
                    fileflag |= _IOCOMMIT;
                
               break;
     //...
    

_tfopen是fopen内部调用的,参考fopen的文档,发现是这样的:

" 模式: 'c'

启用关联文件名的提交标志,以便在调用 fflush 或 _flushall 时将文件缓冲区的内容直接写入磁盘。" 因此,只有在调用 fopen 时设置了 'c' 标志时才会调用 _commit。

_commit 函数最终会调用 FlushFileBuffers。

除此之外,我发现当我只向文件写入少量数据(不超过缓冲区大小)时,如果 fwrite 没有 fflush,则文本显然不会被写入,而对于 API,即使我没有写入文件,也可以在 WriteFile 之后' t调用FlushFileBuffers,当我打开文件(程序处于睡眠状态)时,内容会自动写入文件,这就是我对flush感到困惑的原因之一,这个操作可能是由操作系统完成的,WriteFile将数据复制到系统缓存,并且它的文件缓冲区由操作系统管理,因此 fflush() 仅在内部调用 WriteFile 而没有真正的刷新是合理的,系统知道何时刷新它们,可能是当文件句柄关闭或另一个 I/O 访问该文件时发生。 所以我将基准修改为:

      start_time = timeGetTime();
for(int i = 0; i < times; ++i) 
    fwrite(text, 1, size, cfile);
    fflush(cfile);

end_time = timeGetTime();
std::cout << end_time - start_time << '\n';

start_time = timeGetTime();
for(int i = 0; i < times; ++i) 
    WriteFile(wfile, text, size, &written, NULL);

end_time = timeGetTime();
std::cout << end_time - start_time << std::endl;

结果是 次:99999 写入:217 写文件:171

所以,总而言之,要加快 API 文件写入操作:

    不要显式调用FlushFileBuffers,系统缓存中的数据会在需要时刷新到磁盘。

    为 WriteFile 获取缓冲区,就像 fwrite 一样,因为 API 调用比简单的 memcpy 花费更多时间,当缓冲区填满时调用 WriteFile。

【问题讨论】:

效率是什么意思?应用程序响应速度更快,或者 API 调用和数据在介质上的时间尽可能有序? 您是否尝试过指定GENERIC_ALL @lego 我的意思是写调用的速度 @Mehrdad GENERIC_ALL 在这里无效,我试过 GENERIC_READ|GENERIC_WRITE,800+ 毫秒 尝试将共享模式FILE_SHARE_READ 更改为 0。这将独占打开文件,这可能会产生影响。 【参考方案1】:

如果设置正确,WriteFile() 可以fwrite() 更有效。 WriteFile() 允许您微调它在执行您发出的 IO 请求时使用的条件。

例如,您可以绕过中间缓冲 IO 子系统并直接从 您的 数据指针中提取,就好像它是中间 IO 缓冲区一样,因此消除了重要的中间人。但是,设置有些限制。您的数据指针必须位于与正在写入的卷的扇区大小相等的字节边界上。出于显而易见的原因,fwrite() 不存在这样的设施。 Windows API 爱好者(大约是 J. Richter 和他的兄弟们)非常喜欢使用 WriteFile() 的这种用法来挤压他们的 Windows 程序 IO 性能的最后一个下降。

如果您想知道为什么人们不喜欢WriteFile()love-children,我可以向您保证,很多人都喜欢,但没有他们对可移植代码最不感兴趣。那些(或只是不 关心它的那些(Knuth 说的过早优化是什么……?),选择像fwrite() 这样的标准设施。

如果您真的fwrite() 的 MSVCRT 实现以及它的工作原理感兴趣,请查看源代码。它随每个版本的 VC++ Standard 或更高版本一起提供(可能不是 Express;我从未检查过)。

【讨论】:

不错的答案,在这里 +1 - 虽然我质疑你使用“爱孩子”:) @HerrJoebob 哈哈。有时它只是来找我。 =P 问题是问为什么WriteFile() 似乎比fwrite() 效率要那么多。该示例几乎不是使用WriteFile() 来压榨每一滴 I/O 性能的广告。 我是一个WriteFile 爱孩子并且很自豪 ;) 它的使用可以在visual studio目录\VC\crt\src\write.c中找到(另见,fwrite.c, open.c 和 fopen.c)。 @Steve 我希望这不会被误解,如果我正在编写 Windows 代码,并且我知道它永远是终生的 Windows 代码,我们有相同的父母=P(或至少一个...)【参考方案2】:

使用类似 Sysinternals 的 Process Monitor (procmon) 之类的工具,您会发现对 fflush() 的调用与 FlushFileBuffers(wfile)(或 FILE_FLAG_WRITE_THROUGHCreateFile() 的标志)的调用不同。

fwrite() 会将数据写入缓冲区,直到缓冲区填满,这将导致它将缓冲区中的数据发送到 WriteFile() 调用。当您调用 fflush() 时,所发生的只是当前缓冲区中的数据被传递给对 WriteFile() 的调用 - fflush() 不会调用 FlushFileBuffers()

1:21:32.9391534 AM  test.exe    6132    WriteFile   C:\temp\file1.txt   SUCCESS Offset: 0, Length: 24
1:21:32.9392200 AM  test.exe    6132    WriteFile   C:\temp\file1.txt   SUCCESS Offset: 24, Length: 24
1:21:32.9392340 AM  test.exe    6132    WriteFile   C:\temp\file1.txt   SUCCESS Offset: 48, Length: 24
1:21:32.9392436 AM  test.exe    6132    WriteFile   C:\temp\file1.txt   SUCCESS Offset: 72, Length: 24
1:21:32.9392526 AM  test.exe    6132    WriteFile   C:\temp\file1.txt   SUCCESS Offset: 96, Length: 24
1:21:32.9392623 AM  test.exe    6132    WriteFile   C:\temp\file1.txt   SUCCESS Offset: 120, Length: 24

为了比较,下面是一个来自fwrite() 循环的跟踪示例,没有fflush() 调用:

1:27:28.5675034 AM  test.exe    3140    WriteFile   C:\temp\file1.txt   SUCCESS Offset: 0, Length: 1,024
1:27:28.5676098 AM  test.exe    3140    WriteFile   C:\temp\file1.txt   SUCCESS Offset: 1,024, Length: 1,024
1:27:28.5676399 AM  test.exe    3140    WriteFile   C:\temp\file1.txt   SUCCESS Offset: 2,048, Length: 1,024
1:27:28.5676651 AM  test.exe    3140    WriteFile   C:\temp\file1.txt   SUCCESS Offset: 3,072, Length: 1,024

这是来自WriteFile() 循环的跟踪的sn-p(带有FILE_ATTRIBUTE_NORMAL 标志和对FlushFileBuffers() 的显式调用 - 它只是使自FlushFileBuffers() 调用以来在跟踪中发生的事情更容易看到显示在跟踪中,而不是仅显示为第二个 4KB WriteFile() 调用)。

1:21:29.0068503 AM  test.exe    6132    WriteFile   C:\temp\file2.txt   SUCCESS Offset: 0, Length: 24, Priority: Normal
1:21:29.0069197 AM  test.exe    6132    FlushBuffersFile    C:\temp\file2.txt   SUCCESS 
1:21:29.0069517 AM  test.exe    6132    WriteFile   C:\temp\file2.txt   SUCCESS Offset: 0, Length: 4,096, I/O Flags: Non-cached, Paging I/O, Synchronous Paging I/O, Priority: Normal
1:21:29.0087574 AM  test.exe    6132    WriteFile   C:\temp\file2.txt   SUCCESS Offset: 24, Length: 24
1:21:29.0087798 AM  test.exe    6132    FlushBuffersFile    C:\temp\file2.txt   SUCCESS 
1:21:29.0088087 AM  test.exe    6132    WriteFile   C:\temp\file2.txt   SUCCESS Offset: 0, Length: 4,096, I/O Flags: Non-cached, Paging I/O, Synchronous Paging I/O, Priority: Normal
1:21:29.0102260 AM  test.exe    6132    WriteFile   C:\temp\file2.txt   SUCCESS Offset: 48, Length: 24
1:21:29.0102428 AM  test.exe    6132    FlushBuffersFile    C:\temp\file2.txt   SUCCESS 
1:21:29.0102701 AM  test.exe    6132    WriteFile   C:\temp\file2.txt   SUCCESS Offset: 0, Length: 4,096, I/O Flags: Non-cached, Paging I/O, Synchronous Paging I/O, Priority: Normal
1:21:29.0113444 AM  test.exe    6132    WriteFile   C:\temp\file2.txt   SUCCESS Offset: 72, Length: 24
1:21:29.0113602 AM  test.exe    6132    FlushBuffersFile    C:\temp\file2.txt   SUCCESS 
1:21:29.0113848 AM  test.exe    6132    WriteFile   C:\temp\file2.txt   SUCCESS Offset: 0, Length: 4,096, I/O Flags: Non-cached, Paging I/O, Synchronous Paging I/O, Priority: Normal

因此,您的基准测试显示WriteFile() 循环严重劣势的原因仅仅是因为您有大约一千次对FlushFileBuffers() 的调用不在fwrite() 循环中。

【讨论】:

你说得对,在 fdopen.c 中有一个 switch case 'c', fileflag |= _IOCOMMIT;这意味着“启用关联文件名的提交标志,以便在调用 fflush 或 _flushall 时将文件缓冲区的内容直接写入磁盘。”-来自 MSDN 缓冲加速了大量的小写。这就是它用于高级 I/O 库的原因。

以上是关于fwrite 比 Windows 中的 WriteFile 快吗?的主要内容,如果未能解决你的问题,请参考以下文章

缓冲文件系统(fopen/fread/fwrite)和非缓冲文件系统(open/read/write)

Linux read/write fread/fwrite两者区别

C:拷贝函数&write()&fwrite()

超出EOF的PHP fwrite模式“r +”不起作用,XAMPP,Windows 10 x64 [重复]

了解 fwrite() 的缓冲行为

[Linux]read/write和fread/fwrite有什么区别