当然,磁盘上的文件缓冲I/O比无缓冲的速度快.但是为什么即使写入内存缓冲区也有好处?
以下基准代码示例使用gcc 5.40编译,使用优化选项-O3,与glibc 2.24链接.(请注意,常见的glibc 2.23有关于fmemopen()的错误.)
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <assert.h>
int main() {
size_t bufsz=65536;
char buf[bufsz];
FILE *f;
int r;
f=fmemopen(buf,bufsz,"w");
assert(f!=NULL);
// setbuf(f,NULL); // UNCOMMENT TO GET THE UNBUFFERED VERSION
for(int j=0; j<1024; ++j) {
for(uint32_t i=0; i<bufsz/sizeof(i); ++i) {
r=fwrite(&i,sizeof(i),1,f);
assert(r==1);
}
rewind(f);
}
r=fclose(f);
assert(r==0);
}
Run Code Online (Sandbox Code Playgroud)
缓冲版本的结果:
$ gcc -O3 -I glibc-2.24/include/ -L glibc-2.24/lib test-buffered.c
$ time LD_LIBRARY_PATH=glibc-2.24/lib ./a.out
real 0m1.137s
user 0m1.132s
sys 0m0.000s
Run Code Online (Sandbox Code Playgroud)
无缓冲版本的结果
$ gcc -O3 -I glibc-2.24/include/ -L glibc-2.24/lib test-unbuffered.c
$ time LD_LIBRARY_PATH=glibc-2.24/lib ./a.out
real 0m2.266s
user 0m2.256s
sys 0m0.000s
Run Code Online (Sandbox Code Playgroud)
缓冲版本性能记录:
Samples: 19K of event 'cycles', Event count (approx.): 14986217099
Overhead Command Shared Object Symbol
48.56% fwrite libc-2.17.so [.] _IO_fwrite
27.79% fwrite libc-2.17.so [.] _IO_file_xsputn@@GLIBC_2.2.5
11.80% fwrite fwrite [.] main
9.10% fwrite libc-2.17.so [.] __GI___mempcpy
1.56% fwrite libc-2.17.so [.] __memcpy_sse2
0.19% fwrite fwrite [.] fwrite@plt
0.19% fwrite [kernel.kallsyms] [k] native_write_msr_safe
0.10% fwrite [kernel.kallsyms] [k] apic_timer_interrupt
0.06% fwrite libc-2.17.so [.] fmemopen_write
0.04% fwrite libc-2.17.so [.] _IO_cookie_write
0.04% fwrite libc-2.17.so [.] _IO_file_overflow@@GLIBC_2.2.5
0.03% fwrite libc-2.17.so [.] _IO_do_write@@GLIBC_2.2.5
0.03% fwrite [kernel.kallsyms] [k] rb_next
0.03% fwrite libc-2.17.so [.] _IO_default_xsputn
0.03% fwrite [kernel.kallsyms] [k] rcu_check_callbacks
Run Code Online (Sandbox Code Playgroud)
无缓冲版本性能记录:
Samples: 35K of event 'cycles', Event count (approx.): 26769401637
Overhead Command Shared Object Symbol
33.36% fwrite libc-2.17.so [.] _IO_file_xsputn@@GLIBC_2.2.5
25.58% fwrite libc-2.17.so [.] _IO_fwrite
12.23% fwrite libc-2.17.so [.] fmemopen_write
6.09% fwrite libc-2.17.so [.] __memcpy_sse2
5.94% fwrite libc-2.17.so [.] _IO_file_overflow@@GLIBC_2.2.5
5.39% fwrite libc-2.17.so [.] _IO_cookie_write
5.08% fwrite fwrite [.] main
4.69% fwrite libc-2.17.so [.] _IO_do_write@@GLIBC_2.2.5
0.59% fwrite fwrite [.] fwrite@plt
0.33% fwrite [kernel.kallsyms] [k] native_write_msr_safe
0.18% fwrite [kernel.kallsyms] [k] apic_timer_interrupt
0.04% fwrite [kernel.kallsyms] [k] timerqueue_add
0.03% fwrite [kernel.kallsyms] [k] rcu_check_callbacks
0.03% fwrite [kernel.kallsyms] [k] ktime_get_update_offsets_now
0.03% fwrite [kernel.kallsyms] [k] trigger_load_balance
Run Code Online (Sandbox Code Playgroud)
差异:
# Baseline Delta Shared Object Symbol
# ........ ....... ................. ..................................
#
48.56% -22.98% libc-2.17.so [.] _IO_fwrite
27.79% +5.57% libc-2.17.so [.] _IO_file_xsputn@@GLIBC_2.2.5
11.80% -6.72% fwrite [.] main
9.10% libc-2.17.so [.] __GI___mempcpy
1.56% +4.54% libc-2.17.so [.] __memcpy_sse2
0.19% +0.40% fwrite [.] fwrite@plt
0.19% +0.14% [kernel.kallsyms] [k] native_write_msr_safe
0.10% +0.08% [kernel.kallsyms] [k] apic_timer_interrupt
0.06% +12.16% libc-2.17.so [.] fmemopen_write
0.04% +5.35% libc-2.17.so [.] _IO_cookie_write
0.04% +5.91% libc-2.17.so [.] _IO_file_overflow@@GLIBC_2.2.5
0.03% +4.65% libc-2.17.so [.] _IO_do_write@@GLIBC_2.2.5
0.03% -0.01% [kernel.kallsyms] [k] rb_next
0.03% libc-2.17.so [.] _IO_default_xsputn
0.03% +0.00% [kernel.kallsyms] [k] rcu_check_callbacks
0.02% -0.01% [kernel.kallsyms] [k] run_timer_softirq
0.02% -0.01% [kernel.kallsyms] [k] cpuacct_account_field
0.02% -0.00% [kernel.kallsyms] [k] __hrtimer_run_queues
0.02% +0.01% [kernel.kallsyms] [k] ktime_get_update_offsets_now
Run Code Online (Sandbox Code Playgroud)
在深入研究源代码后,我发现iofwrite.c 中fwrite的_IO_fwrite仅仅是实际 write 函数的包装函数_IO_sputn。并且还发现:
libioP.h:#define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N)
libioP.h:#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)
Run Code Online (Sandbox Code Playgroud)
由于该__xsputn函数是实际的_IO_file_xsputn,可以如下找到:
fileops.c: JUMP_INIT(xsputn, _IO_file_xsputn),
fileops.c:# define _IO_new_file_xsputn _IO_file_xsputn
fileops.c:versioned_symbol (libc, _IO_new_file_xsputn, _IO_file_xsputn, GLIBC_2_1);
Run Code Online (Sandbox Code Playgroud)
最后进入_IO_new_file_xsputnfileops.c中的函数,相关部分代码如下:
/* Try to maintain alignment: write a whole number of blocks. */
block_size = f->_IO_buf_end - f->_IO_buf_base;
do_write = to_do - (block_size >= 128 ? to_do % block_size : 0);
if (do_write)
{
count = new_do_write (f, s, do_write);
to_do -= count;
if (count < do_write)
return n - to_do;
}
/* Now write out the remainder. Normally, this will fit in the
buffer, but it's somewhat messier for line-buffered files,
so we let _IO_default_xsputn handle the general case. */
if (to_do)
to_do -= _IO_default_xsputn (f, s+do_write, to_do);
Run Code Online (Sandbox Code Playgroud)
在 RHEL 7.2 上,block_size如果启用了缓冲区,则等于 8192,否则等于 1。
所以有这样的情况:
情况 1:启用缓冲区
do_write = to_do - (to_do % block_size) = to_do - (to_do % 8192)
在我们的例子中,
,to_do = sizeof(uint32)
和do_write = 0将调用该_IO_default_xsputn函数。
new_do_write之后函数to_do为零。只是new_do_write一个包装调用_IO_SYSWRITE
libioP.h:#define _IO_SYSWRITE(FP, DATA, LEN) JUMP2 (__write, FP, DATA, LEN)
Run Code Online (Sandbox Code Playgroud)
正如我们所看到的,这_IO_SYSWRITE是实际的fmemopen_write调用。所以,性能差异是由fmemopen_write调用引起的。之前的表现记录也证明了这一点。
最后,这个问题很好,我对此很感兴趣,它帮助我了解了一些底层的IO功能。有关其他平台的更多信息,请参阅https://oxnz.github.io/2016/08/11/fwrite-perf-issue/ 。
当调用库时,代码的优化级别不受代码影响,并且是恒定的。
这就是为什么更改写入大小不会影响测试限制内的比率。(如果写入大小倾向于您的数据大小,那么您的代码将占主导地位)。
调用 fwrite 的成本将导致决定是否刷新数据。
虽然我不确定内存流的 fwrite 实现,但如果调用接近内核,那么syscall操作系统函数上的 或 安全门可能会导致成本占主导地位。这种成本就是写入数据最适合底层存储的原因。
根据经验,我发现文件系统在 8kb 块上工作得相当好。我会考虑将 4kb 用于内存系统 - 因为这是处理器页面边界的大小。