当然,磁盘上的文件缓冲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);
}
缓冲版本的结果:
$ 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
无缓冲版本的结果
$ 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
答案 0 :(得分:3)
缓冲版本的性能记录:
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
无缓冲版本的性能记录:
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
差异:
# 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
在深入研究源代码后,我发现fwrite
在iofwrite.c中是_IO_fwrite
,它只是实际写函数_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)
由于__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);
最后,进入fileops.c中的_IO_new_file_xsputn
函数,代码的相关部分如下:
/* 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);
在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)
我们可以看到,_IO_SYSWRITE
实际上是fmemopen_write
来电。
因此,性能差异是由fmemopen_write
调用引起的。
以前的表现记录证明了这一点。
最后,这个问题非常好,我对它非常感兴趣,它帮助我学习了表面下的IO功能。有关其他平台的详细信息,请参阅https://oxnz.github.io/2016/08/11/fwrite-perf-issue/。
答案 1 :(得分:1)
到目前为止,谢谢你们的帮助。
我检查了glibc 2.24的库源代码,似乎在每次刷新时添加0字节的附加逻辑负责时间开销。另见man-page:
刷新已打开以进行写入的流时(fflush(3)) 或者关闭(fclose(3)),在结尾处写入一个空字节 缓冲区,如果有空间。
在无缓冲模式下,在每个fwrite()之后添加此Null-Byte,只是用下一个fwrite()覆盖。
我复制了fmemopen_write()的库源代码,对那些也想知道这种奇怪行为的人来说......
static ssize_t
fmemopen_write (void *cookie, const char *b, size_t s)
{
fmemopen_cookie_t *c = (fmemopen_cookie_t *) cookie;;
_IO_off64_t pos = c->append ? c->maxpos : c->pos;
int addnullc = (s == 0 || b[s - 1] != '\0');
if (pos + s > c->size)
{
if ((size_t) (c->pos + addnullc) >= c->size)
{
__set_errno (ENOSPC);
return 0;
}
s = c->size - pos;
}
memcpy (&(c->buffer[pos]), b, s);
c->pos = pos + s;
if ((size_t) c->pos > c->maxpos)
{
c->maxpos = c->pos;
if (c->maxpos < c->size && addnullc)
c->buffer[c->maxpos] = '\0';
/* A null byte is written in a stream open for update iff it fits. */
else if (c->append == 0 && addnullc != 0)
c->buffer[c->size-1] = '\0';
}
return s;
}
答案 2 :(得分:0)
当调用库时,代码的优化级别不受代码的影响,并且会保持不变。
这就是为什么更改写入大小不会影响测试限制内的比率。 (如果写入大小倾向于您的数据大小,那么您的代码将占主导地位。)
调用fwrite的成本将决定是否刷新数据。
虽然我不确定内存流的fwrite实现,但如果调用接近内核,那么OS函数上的syscall
或安全门可能会导致成本占主导地位。这个成本是为什么写数据最适合底层商店的原因。
根据经验,我发现文件系统在8kb块上工作得相当好。我会考虑4kb的内存系统 - 因为这是处理器页面边界的大小。