8、标准输入输出的效率
这一节,将使用标准输入输出和直接使用系统调用进行输入输出的时间进行了对比,主要是对: char
的 std I/O
, line
的 std I/O
,系统调用设置最优缓存的 I/O
,以及系统调用没有设置缓存的 =I/O = 进行了对比,对比的表格如下:
使用标准输入输出库的时间
+------------------------------------------------------------------------------------------------------------------------------+
| Function | User CPU (seconds) | System CPU (seconds) | Clock time (seconds) | Bytes of program text |
|-----------------------------------+--------------------+----------------------+----------------------+-----------------------|
| best time from Figure 3.5 | 0.01 | 0.18 | 6.67 | |
|-----------------------------------+--------------------+----------------------+----------------------+-----------------------|
| fgets, fputs | 2.59 | 0.19 | 7.15 | 139 |
|-----------------------------------+--------------------+----------------------+----------------------+-----------------------|
| getc, putc | 10.84 | 0.27 | 12.07 | 120 |
|-----------------------------------+--------------------+----------------------+----------------------+-----------------------|
| fgetc, fputc | 10.44 | 0.27 | 11.42 | 120 |
|-----------------------------------+--------------------+----------------------+----------------------+-----------------------|
| single byte time from Figure 3.5 | 124.89 | 161.65 | 288.64 | |
+------------------------------------------------------------------------------------------------------------------------------+
这里,第1行的 best time from Figure3.5
对应原书中的相应图形,其实就是之前第3章8节中的 I/O
效率对比表格中,直接使用系统调用,传入最优缓存大小所用的时间(使得系统调用次数最少从而消耗时间最少)。我们通过操作一个 98.5MB
大小的文件(大约 300
万行),来显示这些数据,通过上表我们发现:
直接使用库函数,并不比最优缓存的系统调用
I/O
差很多。使用库函数,我们不用考虑系统最优缓存了,有时候只是考虑buffer
大小就行了,这比系统调用考虑最优缓存方便多了。如果line
的std I/O
使用char
的std I/O
实现的话,其消耗的时间要比char
的std I/O
要大,但是由于line
的std I/O
是用高效率的memccopy
实现的,所以快。使用
char
的std I/O
比没有缓存的系统调用I/O
要快,尽管循环次数差不多,而且char
的还额外增加了一点sys
的循环,但是一次系统调用的代价,比一次函数调用的代价大很多,所以,char
的std I/O
比纯粹没有缓存系统调用的I/O
代价要小。使用
std I/O
需要增加一些system time
用来拷贝。一般重要的程序,I/O
应该占用user time
更多。
译者注
原文参考
9、二进制 I/O
前面函数都是以字节或者行的方式进行操作。如果对于二进制 I/O
,我们更愿意一次读或写整个结构变量;虽然前面那样也能实现读写整个结构变量,但是不够方便,因此,提供了下列两个函数以执行二进制 I/O
操作。
#include <stdio.h>
size_t fread(void *restrict ptr, size_t size, size_t nobj,FILE *restrict fp);
size_t fwrite(const void *restrict ptr, size_t size, size_t nobj,FILE *restrict fp);
两者返回:读取或者写入的对象数目。
fread
函数从 fp
中读取 nobj
个内存块,每个内存块大小是 size
,把读取的数据存放在 ptr
指向的位置。如果成功了, fread
会返回读取的内存块数目。如果到达文件结尾或者出错了,会返回一个小于 nobj
的数或者0, fread
是无法区分的,这时候需要使用 feof()
和 ferror()
来进行区分。
fwrite
向 stream
中写 nobj
个内存块,每个内存块大小是 size
,数据来源是 ptr
。如果到达文件结尾或者出错了,会返回一个小于 nobj
的数或者0。
这函数有两个常用的用途:
a)读写一个二进制数组。例如将一个数组中的第2到5个元素写入,那么如下:
float data[10];
if (fwrite(&data[2], sizeof(float), 4, fp) != 4)
printf("fwrite error");
这里,我们指定数组每个元素(浮点类型)的大小,以及元素的数目。
b)读写一个结构,例如:
struct {
short count;
long total;
char name[NAMESIZE];
}item;
if (fwrite(&item, sizeof(item), 1, fp) != 1)
printf("fwrite error");
这里,我们指定结构大小,以及要写入的结构对象的数目(1个)。
注意: fread
和 fwrite
的一个问题是: fread
的数据必须是 fwrite
从同一个系统上面写的。大致因为:
- 不同系统上面,编译环境有可能不同,结构的字节对齐因素等也不同。
- 不同的系统,二进制文件格式可能不同,可能还包含了平台相关的信息。
译者注
原文参考
10、流的定位
有三组方法在 I/O
流中进行定位:
-
ftell
和fseek:
在大约version7
时开始,它们把位置存放在一个长整型中去。 -
ftello
和fseeko:
在Single UNIX Specification
是里面被引入,允许文件的偏移是off_t
类型的。 -
fgetpos
和fsetpos:
由ISO C
引入,它使用一个fpos_t
类型来存放文件的位置,这个类型可以记载需要的文件大小。
需要移植到非Unix的程序,最好用 fgetpos
和 fsetpos
。
#include <stdio.h>
long ftell(FILE *fp);
返回:如果成功返回当前文件位置标识, 如果错误返回 1L
(应该一般是 -1
并且设置 errno
)。
int fseek(FILE *fp, long offset, int whence);
返回:如果成功返回0,如果错误返回非0(应该一般是-1)。
void rewind(FILE *fp);
对于一个二进制文件,其位置标识是从文件起始位置开始,并以字节为计量单位。 ftell
用于二进制文件时,返回值就是字节位置。为了用 fseek
定位一个二进制文件,必须指定参数 offset
,以及解释这个参数含义的参数 whence
。 whence
的值与 lseek
函数的相同, SEEK_SET
表示从文件的起始位置开始, SEEK_CUR
表示从当前文件位置计算, SEEK_END
表示从文件的尾端计算。
对于文本文件,文件当前位置可能不以简单的字节位移量来度量。这主要是因为在非UNIX系统中,可能以不同的格式存放文本文件。为了定位一个文本文件, whence
一定要是 SEEK_SET
,而且 offset
只能有两种值:0(表到文件起始位置),或对该文件的 ftell
所返回的值。
使用 rewind
函数也可将一个流设置到文件的起始位置。
下面的函数和前面的 ftell
与 fseek
一样,只是参数类型由 long
变成了 off_t
:
#include <stdio.h>
off_t ftello(FILE *fp);
返回:如果成功返回当前文件位置标识, 如果错误返回1( off_t
类型,其值一般为-1)。
int fseeko(FILE *fp, off_t offset, int whence);
返回:如果成功返回0,如果错误返回非0(其值一般为-1)。
因为有些系统实现将 off_t
定义成大于32位的了,所以有了这两个函数。
fgetpos
和 fsetpos
是 ISO C
中引入的,声明如下:
#include <stdio.h>
int fgetpos(FILE *restrict fp, fpos_t *restrict pos);
int fsetpos(FILE *fp, const fpos_t *pos);
两者返回:如果成功返回0, 如果错误返回非0(其值一般为-1)。
fgetpos
将文件位置标识的当前值存入 pos
指向的对象中。在之后调用 fsetpos
时,可以使用此值将流重新定位至该位置。