我正在大力优化一些代码(幸运的是没有平台独立性 - 仅限 Linux)。我创建了一个非常简单的框架来测量经过的时间(以时钟周期为单位)。我早期的一个想法是抓住用 Linux 系统调用替换 libc 函数这一唾手可得的成果(重要的是:我一次最多打印一个字符,并且不进行任何格式设置)。然而,我的测试一致表明,
putchar
的速度大约是syscall
的两倍多(时钟周期是通过rdtsc
获得的,并且是64位十六进制整数;如果足够小,它们的差异显示为十进制):
注意: 测试不包括重复:以下代码仅通过执行一次来测量。
syscall
:# %rdi contains a string
movq $1 , %rax # system call 1: write
movq %rdi, %rsi # what: char pointer
movq $1 , %rdi # where: stdout
movq $1 , %rdx # how many bytes: 1
syscall
__________________________________
RDTSC Post: 0x0002c22fbac1c7b5
RDTSC Pre: 0x0002c22fbabf4d1f
Clock cycles: 0x0000000000027a96
Clock cycles: 162454
putchar
:# %rdi contains a string
mov %rdi, %rsi
xor %edi, %edi
movb (%rsi), %dil
call putchar
__________________________________
RDTSC Post: 0x0002c221713726cd
RDTSC Pre: 0x0002c2217136469f
Clock cycles: 0x000000000000e02e
Clock cycles: 57390
注意: 即使我只包含一个结果,对于
syscall
方法来说,结果始终慢约 2.5 倍。
putchar
的所有内部工作原理,这尤其奇怪,可以通过使用 GDB 逐步执行该函数来看到:IO_validate_vtable (vtable=0x7ffff7e16600 <_IO_file_jumps>) at ./libio/libioP.h:943
943 ./libio/libioP.h: No such file or directory.
(gdb)
__GI__IO_file_doallocate (fp=0x7ffff7e1a780 <_IO_2_1_stdout_>) at ./libio/libioP.h:947
947 in ./libio/libioP.h
(gdb)
__GI__IO_file_stat (fp=0x7ffff7e1a780 <_IO_2_1_stdout_>, st=0x7fffffffde90) at ./libio/fileops.c:1146
1146 ./libio/fileops.c: No such file or directory.
(gdb)
1147 in ./libio/fileops.c
(gdb)
__GI___fstat64 (fd=1, buf=0x7fffffffde90) at ../sysdeps/unix/sysv/linux/fstat64.c:29
29 ../sysdeps/unix/sysv/linux/fstat64.c: No such file or directory.
(gdb)
30 in ../sysdeps/unix/sysv/linux/fstat64.c
(gdb)
35 in ../sysdeps/unix/sysv/linux/fstat64.c
(gdb)
__GI___fstatat64 (fd=1, file=0x7ffff7dd846f "", buf=0x7fffffffde90, flag=4096) at ../sysdeps/unix/sysv/linux/fstatat64.c:153
153 ../sysdeps/unix/sysv/linux/fstatat64.c: No such file or directory.
(gdb)
163 in ../sysdeps/unix/sysv/linux/fstatat64.c
(gdb)
fstatat64_time64_stat (flag=4096, buf=0x7fffffffde90, file=0x7ffff7dd846f "", fd=1) at ../sysdeps/unix/sysv/linux/fstatat64.c:98
98 in ../sysdeps/unix/sysv/linux/fstatat64.c
(gdb)
__GI___fstatat64 (fd=1, file=0x7ffff7dd846f "", buf=0x7fffffffde90, flag=4096) at ../sysdeps/unix/sysv/linux/fstatat64.c:166
166 in ../sysdeps/unix/sysv/linux/fstatat64.c
(gdb)
__GI__IO_file_doallocate (fp=0x7ffff7e1a780 <_IO_2_1_stdout_>) at ./libio/filedoalloc.c:86
86 ./libio/filedoalloc.c: No such file or directory.
(gdb)
91 in ./libio/filedoalloc.c
(gdb)
__gnu_dev_major (__dev=34817) at ../include/sys/sysmacros.h:47
47 ../include/sys/sysmacros.h: No such file or directory.
(gdb)
91 ./libio/filedoalloc.c: No such file or directory.
(gdb)
94 in ./libio/filedoalloc.c
(gdb)
97 in ./libio/filedoalloc.c
(gdb)
101 in ./libio/filedoalloc.c
(gdb)
__GI___libc_malloc (bytes=bytes@entry=1024) at ./malloc/malloc.c:3287
3287 ./malloc/malloc.c: No such file or directory.
(gdb)
3294 in ./malloc/malloc.c
(gdb)
3295 in ./malloc/malloc.c
(gdb)
ptmalloc_init () at ./malloc/arena.c:315
315 ./malloc/arena.c: No such file or directory.
(gdb)
ptmalloc_init () at ./malloc/arena.c:313
313 in ./malloc/arena.c
(gdb)
321 in ./malloc/arena.c
(gdb)
0x00007ffff7ca1a31 in tcache_key_initialize () at ./malloc/malloc.c:3162
3162 ./malloc/malloc.c: No such file or directory.
(gdb)
318 ./malloc/arena.c: No such file or directory.
(gdb)
321 in ./malloc/arena.c
(gdb)
tcache_key_initialize () at ./malloc/malloc.c:3162
3162 ./malloc/malloc.c: No such file or directory.
(gdb)
__GI___getrandom (buffer=buffer@entry=0x7ffff7e204d8 <tcache_key>, length=length@entry=8, flags=flags@entry=1) at ../sysdeps/unix/sysv/linux/getrandom.c:28
libc 可以比 syscall 更快吗?
相当于
syscall
的是 call write
,而不是 call putchar
。
当然:直接
syscall
会(稍微)比 call write
快。
不完全是,因为我操作的是 1 个字符的长字符串 - 缓冲在这里不应该发挥作用。
情况恰恰相反:
写入少量数据时(在
putchar()
的情况下:正好一个字节!),通过缓冲可以节省大量时间;
在每次调用中写入大量数据时(例如
fwrite(x,1,10000,y)
),您几乎不会保存任何内容。
write(x,y,1000)
需要的时间远少于 write(x,y,1)
的 1000 倍。
因此,与调用
write(x,y,1000)
1000 次相比,将数据写入缓冲区并在 1000 个字符后调用 write(x,y,1)
可以节省大量时间。
这正是
putchar()
的作用:它将每个字符保存在缓冲区中,当缓冲区已满时,调用 write()
。
原因是Linux内核的很多代码行在调用
write()
(或对应的syscall
)时都会被执行一次。当使用单个 write()
调用写入 1000 个字节时,这些行将执行一次;当调用 write()
1000 次时,这些行会执行 1000 次。