#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>
//compile with: gcc -Wall -Wextra cc-c.c -o cc-c
//run with: ./cc-c
// gcc -Wall -Wextra -O2 cc-c.c -o cc-c && ./cc-c # optimized
// gcc -Wall -Wextra -O2 -falign-functions=64 cc-c.c -o cc-c && ./cc-c
// gcc -Wall -Wextra cc-c.c -o cc-c # back to normal
//K&R2 Exercise 8-2. Rewrite fopen and _fillbuf with fields instead of
//explicit bit operations. Compare code size and execution speed.
//
//In plain words: in 8.5 the five flags (READ, WRITE, UNBUF, EOF, ERR)
//are bits of one int, set and tested by hand with |, & and masks.
//Section 6.9 showed the other way: bit-fields, where each flag has a
//name and the compiler writes the masks. Write fopen and _fillbuf that
//way, then measure: are they bigger or smaller, faster or slower?
//
//Both versions are here, side by side:
// my_ the 8.5 code (bit operations), as in the block below this one
// f_ the same code with bit-fields; every changed line has a
// "was:" comment with the 8.5 line it replaces
//main:
// 1. the two structs: same size, and the fields sit on the same bits
// as the enum -- fields ARE bit operations, written by the compiler
// 2. both read zhouyi.txt: same bytes, same read() calls, same EOF/ERR
// 3. code size: the program runs nm on itself
// 4. speed: a whole-file read, then the flag test alone
//
//The three run lines: plain gcc, as always. -O2 lets the compiler
//optimize -- watch section 3 change. The third line explains a puzzle
//in section 4 at -O2: on this machine (i7-8700) the flag test with
//fields came out ~25% slower, in both orders, although objdump shows
//the same three instructions. -falign-functions=64 starts every
//function on a 64-byte boundary, and the gap is gone: it came from
//WHERE the code sat in memory, not from the bit-fields
#define MY_OPEN_MAX 20
#define MY_BUFSIZ 1024
#define PERMS 0666
int nreads = 0; //how many times read() was called, both versions
//=== the 8.5 version: flags are bits of an int =========================
typedef struct my_iobuf {
int cnt;
char *ptr;
char *base;
int flag;
int fd;
} MYFILE;
enum my_flags {
M_READ = 01,
M_WRITE = 02,
M_UNBUF = 04,
M_EOF = 010,
M_ERR = 020
};
MYFILE my_iob[MY_OPEN_MAX] = {
{ 0, (char *) 0, (char *) 0, M_READ, 0 },
{ 0, (char *) 0, (char *) 0, M_WRITE, 1 },
{ 0, (char *) 0, (char *) 0, M_WRITE | M_UNBUF, 2 }
};
int my_fillbuf(MYFILE *);
#define my_feof(p) (((p)->flag & M_EOF) != 0)
#define my_ferror(p) (((p)->flag & M_ERR) != 0)
#define my_getc(p) (--(p)->cnt >= 0 \
? (unsigned char) *(p)->ptr++ : my_fillbuf(p))
MYFILE *my_fopen(char *name, char *mode)
{
int fd;
MYFILE *fp;
if (*mode != 'r' && *mode != 'w' && *mode != 'a')
return NULL;
for (fp = my_iob; fp < my_iob + MY_OPEN_MAX; fp++)
if ((fp->flag & (M_READ | M_WRITE)) == 0)
break;
if (fp >= my_iob + MY_OPEN_MAX)
return NULL;
if (*mode == 'w')
fd = creat(name, PERMS);
else if (*mode == 'a') {
if ((fd = open(name, O_WRONLY, 0)) == -1)
fd = creat(name, PERMS);
lseek(fd, 0L, 2);
} else
fd = open(name, O_RDONLY, 0);
if (fd == -1)
return NULL;
fp->fd = fd;
fp->cnt = 0;
fp->base = NULL;
fp->flag = (*mode == 'r') ? M_READ : M_WRITE;
return fp;
}
int my_fillbuf(MYFILE *fp)
{
int bufsize;
if ((fp->flag & (M_READ | M_EOF | M_ERR)) != M_READ)
return EOF;
bufsize = (fp->flag & M_UNBUF) ? 1 : MY_BUFSIZ;
if (fp->base == NULL)
if ((fp->base = (char *) malloc(bufsize)) == NULL)
return EOF;
fp->ptr = fp->base;
nreads++;
fp->cnt = read(fp->fd, fp->ptr, bufsize);
if (--fp->cnt < 0) {
if (fp->cnt == -1)
fp->flag |= M_EOF;
else
fp->flag |= M_ERR;
fp->cnt = 0;
return EOF;
}
return (unsigned char) *fp->ptr++;
}
//=== the exercise: flags are bit-fields ================================
typedef struct f_iobuf {
int cnt;
char *ptr;
char *base;
//IDIOM: unsigned int name : 1 -- a one-bit field: holds 0 or 1, and
//the compiler packs all five into one unsigned int
struct {
unsigned int is_read : 1;
unsigned int is_write : 1;
unsigned int is_unbuf : 1;
unsigned int is_eof : 1;
unsigned int is_err : 1;
} flag; //was: int flag; (and the enum is gone)
int fd;
} FFILE;
//IDIOM: nested braces -- the inner { } fills the inner struct, field
//by field in declaration order: read, write, unbuf, eof, err
FFILE f_iob[MY_OPEN_MAX] = {
{ 0, (char *) 0, (char *) 0, { 1, 0, 0, 0, 0 }, 0 },
{ 0, (char *) 0, (char *) 0, { 0, 1, 0, 0, 0 }, 1 },
{ 0, (char *) 0, (char *) 0, { 0, 1, 1, 0, 0 }, 2 }
};
int f_fillbuf(FFILE *);
//was: (((p)->flag & _EOF) != 0) -- a 1-bit field is already 0 or 1
#define f_feof(p) ((p)->flag.is_eof)
#define f_ferror(p) ((p)->flag.is_err)
//getc never touches flag: unchanged except the name it calls
#define f_getc(p) (--(p)->cnt >= 0 \
? (unsigned char) *(p)->ptr++ : f_fillbuf(p))
FFILE *f_fopen(char *name, char *mode)
{
int fd;
FFILE *fp;
if (*mode != 'r' && *mode != 'w' && *mode != 'a')
return NULL;
for (fp = f_iob; fp < f_iob + MY_OPEN_MAX; fp++)
//was: if ((fp->flag & (_READ | _WRITE)) == 0)
if (fp->flag.is_read == 0 && fp->flag.is_write == 0)
break;
if (fp >= f_iob + MY_OPEN_MAX)
return NULL;
if (*mode == 'w')
fd = creat(name, PERMS);
else if (*mode == 'a') {
if ((fd = open(name, O_WRONLY, 0)) == -1)
fd = creat(name, PERMS);
lseek(fd, 0L, 2);
} else
fd = open(name, O_RDONLY, 0);
if (fd == -1)
return NULL;
fp->fd = fd;
fp->cnt = 0;
fp->base = NULL;
//was: fp->flag = (*mode == 'r') ? _READ : _WRITE;
//that one assignment set one bit AND cleared the other four. Fields
//are assigned one at a time, so all five have to be written
fp->flag.is_read = (*mode == 'r');
fp->flag.is_write = (*mode != 'r');
fp->flag.is_unbuf = 0;
fp->flag.is_eof = 0;
fp->flag.is_err = 0;
return fp;
}
int f_fillbuf(FFILE *fp)
{
int bufsize;
//was: if ((fp->flag & (_READ | _EOF | _ERR)) != _READ)
//the mask-and-compare said "READ set, EOF and ERR clear" in one
//test; with fields it is three tests, spelled out
if (!fp->flag.is_read || fp->flag.is_eof || fp->flag.is_err)
return EOF;
//was: bufsize = (fp->flag & _UNBUF) ? 1 : BUFSIZ;
bufsize = fp->flag.is_unbuf ? 1 : MY_BUFSIZ;
if (fp->base == NULL)
if ((fp->base = (char *) malloc(bufsize)) == NULL)
return EOF;
fp->ptr = fp->base;
nreads++;
fp->cnt = read(fp->fd, fp->ptr, bufsize);
if (--fp->cnt < 0) {
if (fp->cnt == -1)
fp->flag.is_eof = 1; //was: fp->flag |= _EOF;
else
fp->flag.is_err = 1; //was: fp->flag |= _ERR;
fp->cnt = 0;
return EOF;
}
return (unsigned char) *fp->ptr++;
}
//=== not in the book: helpers for main =================================
//put a file back to its start, so a timing loop can read it again
//without using up the table (there is no fclose until exercise 8-3)
void my_rewind(MYFILE *fp)
{
lseek(fp->fd, 0L, 0);
fp->cnt = 0;
//IDIOM: flag &= ~(A | B) -- clear two bits, leave the rest alone
fp->flag &= ~(M_EOF | M_ERR);
}
void f_rewind(FFILE *fp)
{
lseek(fp->fd, 0L, 0);
fp->cnt = 0;
fp->flag.is_eof = 0;
fp->flag.is_err = 0;
}
//set field k alone, return the raw bits of the whole flag struct
unsigned int fieldbits(int k)
{
FFILE t;
unsigned int bits;
memset(&t.flag, 0, sizeof(t.flag));
switch (k) {
case 0: t.flag.is_read = 1; break;
case 1: t.flag.is_write = 1; break;
case 2: t.flag.is_unbuf = 1; break;
case 3: t.flag.is_eof = 1; break;
case 4: t.flag.is_err = 1; break;
}
//IDIOM: memcpy to look at raw bits -- copies the bytes as they are,
//no conversion; the legal way to see one type's bits as another's
memcpy(&bits, &t.flag, sizeof(bits));
return bits;
}
double ms(clock_t t0)
{
return 1000.0 * (clock() - t0) / CLOCKS_PER_SEC;
}
#define PASSES 300 //whole-file reads per timing
#define CALLS 100000000 //flag tests per timing
//read the whole file PASSES times; return ms
double time_my_file(MYFILE *fp)
{
int k;
clock_t t0 = clock();
for (k = 0; k < PASSES; k++) {
my_rewind(fp);
while (my_getc(fp) != EOF)
;
}
return ms(t0);
}
double time_f_file(FFILE *fp)
{
int k;
clock_t t0 = clock();
for (k = 0; k < PASSES; k++) {
f_rewind(fp);
while (f_getc(fp) != EOF)
;
}
return ms(t0);
}
//call fillbuf CALLS times on a file already at EOF; return ms
double time_my_flag(MYFILE *fp)
{
int i;
//IDIOM: volatile -- every access really happens, so -O2 can't
//decide the answer never changes and delete the loop
MYFILE *volatile vfp = fp;
clock_t t0 = clock();
for (i = 0; i < CALLS; i++)
my_fillbuf(vfp);
return ms(t0);
}
double time_f_flag(FFILE *fp)
{
int i;
FFILE *volatile vfp = fp;
clock_t t0 = clock();
for (i = 0; i < CALLS; i++)
f_fillbuf(vfp);
return ms(t0);
}
int main(int argc, char *argv[])
{
MYFILE *mp;
FFILE *fp;
int c, i, k, mn, fn, mr, fr;
unsigned long msum, fsum;
double mt, ft, mt2, ft2;
char cmd[300];
char *names[] = { "is_read", "is_write", "is_unbuf", "is_eof", "is_err" };
int enums[] = { M_READ, M_WRITE, M_UNBUF, M_EOF, M_ERR };
(void) argc;
//--- 1. the structs ------------------------------------------------
printf("1. sizeof(MYFILE) %zu, sizeof(FFILE) %zu; int flag %zu bytes, the 5 fields %zu bytes\n",
sizeof(MYFILE), sizeof(FFILE), sizeof(int), sizeof(f_iob[0].flag));
for (k = 0; k < 5; k++)
printf(" %-8s alone -> raw bits %02o enum: %02o\n",
names[k], fieldbits(k), enums[k]);
printf(" same bits: the fields are the enum, named. (gcc on x86-64 puts the\n"
" first field in the lowest bit; C itself doesn't promise that order)\n\n");
//--- 2. same behavior ------------------------------------------------
mp = my_fopen("zhouyi.txt", "r");
fp = f_fopen("zhouyi.txt", "r");
if (mp == NULL || fp == NULL) {
fprintf(stderr, "can't open zhouyi.txt\n");
return 1;
}
mn = fn = 0;
msum = fsum = 0;
nreads = 0;
while ((c = my_getc(mp)) != EOF) {
mn++;
msum = msum * 31 + c;
}
mr = nreads;
nreads = 0;
while ((c = f_getc(fp)) != EOF) {
fn++;
fsum = fsum * 31 + c;
}
fr = nreads;
printf("2. bits: %d chars, checksum %lu, %d read() calls, feof %d\n",
mn, msum, mr, my_feof(mp));
printf(" fields: %d chars, checksum %lu, %d read() calls, feof %d\n",
fn, fsum, fr, f_feof(fp));
MYFILE *mdir = my_fopen(".", "r");
FFILE *fdir = f_fopen(".", "r");
my_getc(mdir);
f_getc(fdir);
printf(" a directory: bits ferror %d, fields ferror %d\n", my_ferror(mdir), f_ferror(fdir));
for (k = 0; my_fopen("zhouyi.txt", "r") != NULL; k++)
;
for (i = 0; f_fopen("zhouyi.txt", "r") != NULL; i++)
;
printf(" table full after %d more (bits), %d more (fields)\n\n", k, i);
//--- 3. code size ----------------------------------------------------
//nm lists a program's symbols; -S adds each one's size, -t d in
//decimal. argv[0] is how this program was started: ./cc-c
printf("3. code size, from nm -t d -S %s:\n", argv[0]);
fflush(stdout); //our printf output first, then nm's
snprintf(cmd, sizeof(cmd),
"nm -t d -S %s | grep -E ' (my|f)_(fopen|fillbuf)$' | sort -k4"
" | awk '{ printf \" %%-11s %%4d bytes\\n\", $4, $2 }'", argv[0]);
if (system(cmd) != 0)
printf(" (nm failed)\n");
//objdump -d turns the machine code back into instructions. flag is
//at offset 0x18 (24) in both structs: cnt 4 + 4 padding + ptr 8 +
//base 8. Print just the opening flag test of each fillbuf
printf(" the flag test that opens each fillbuf, from objdump -d:\n");
fflush(stdout);
for (k = 0; k < 2; k++) {
printf(" %s\n", k == 0 ? "my_fillbuf:" : "f_fillbuf:");
fflush(stdout);
snprintf(cmd, sizeof(cmd),
"objdump -d --no-show-raw-insn %s"
" | awk -F'\\t' '/<%s>:/ { p = 1 } p && /ffffffff|push +%%rbx/ { exit }"
" p && /[^-]0x18\\(%%r|and +\\$|cmp +\\$|test / { print \" \" $2 }'",
argv[0], k == 0 ? "my_fillbuf" : "f_fillbuf");
if (system(cmd) != 0)
printf(" (objdump failed)\n");
}
printf("\n");
//--- 4. speed --------------------------------------------------------
//each timing runs twice, in both orders: whatever runs first or
//second can be faster for reasons that have nothing to do with the
//code (caches, CPU clock speed), and swapping shows that
mt = time_my_file(mp);
ft = time_f_file(fp);
mt2 = time_my_file(mp);
ft2 = time_f_file(fp);
printf("4. %d whole-file reads, ms: bits %4.0f fields %4.0f | bits %4.0f fields %4.0f\n",
PASSES, mt, ft, mt2, ft2);
//the files are at EOF now, so every fillbuf call is just the flag
//test and return EOF: time that alone
ft = time_f_flag(fp);
mt = time_my_flag(mp);
ft2 = time_f_flag(fp);
mt2 = time_my_flag(mp);
printf(" flag test alone, %d calls: fields %4.0f bits %4.0f | fields %4.0f bits %4.0f\n",
CALLS, ft, mt, ft2, mt2);
printf(" a whole file is %d my_getc but only %d my_fillbuf calls: the flags\n"
" are looked at once per 1024 bytes, and one read() costs far more\n",
mn, mr);
return 0;
}
win@DESKTOP-MEIH88T:~$ cd webdev-projects
win@DESKTOP-MEIH88T:~/webdev-projects$ gcc -Wall -Wextra cc-c.c -o cc-c
win@DESKTOP-MEIH88T:~/webdev-projects$ ./cc-c
1. sizeof(MYFILE) 32, sizeof(FFILE) 32; int flag 4 bytes, the 5 fields 4 bytes
is_read alone -> raw bits 01 enum: 01
is_write alone -> raw bits 02 enum: 02
is_unbuf alone -> raw bits 04 enum: 04
is_eof alone -> raw bits 10 enum: 10
is_err alone -> raw bits 20 enum: 20
same bits: the fields are the enum, named. (gcc on x86-64 puts the
first field in the lowest bit; C itself doesn't promise that order)
2. bits: 264389 chars, checksum 5235079687469029511, 260 read() calls, feof 1
fields: 264389 chars, checksum 5235079687469029511, 260 read() calls, feof 1
a directory: bits ferror 1, fields ferror 1
table full after 15 more (bits), 15 more (fields)
3. code size, from nm -t d -S ./cc-c:
f_fillbuf 343 bytes
f_fopen 465 bytes
my_fillbuf 322 bytes
my_fopen 367 bytes
the flag test that opens each fillbuf, from objdump -d:
my_fillbuf:
mov 0x18(%rax),%eax
and $0x19,%eax
cmp $0x1,%eax
f_fillbuf:
movzbl 0x18(%rax),%eax
and $0x1,%eax
test %al,%al
movzbl 0x18(%rax),%eax
and $0x8,%eax
test %al,%al
movzbl 0x18(%rax),%eax
and $0x10,%eax
test %al,%al
4. 300 whole-file reads, ms: bits 188 fields 189 | bits 187 fields 191
flag test alone, 100000000 calls: fields 189 bits 167 | fields 189 bits 168
a whole file is 264389 my_getc but only 260 my_fillbuf calls: the flags
are looked at once per 1024 bytes, and one read() costs far more
win@DESKTOP-MEIH88T:~/webdev-projects$ gcc -Wall -Wextra -O2 cc-c.c -o cc-c && ./cc-c
1. sizeof(MYFILE) 32, sizeof(FFILE) 32; int flag 4 bytes, the 5 fields 4 bytes
is_read alone -> raw bits 01 enum: 01
is_write alone -> raw bits 02 enum: 02
is_unbuf alone -> raw bits 04 enum: 04
is_eof alone -> raw bits 10 enum: 10
is_err alone -> raw bits 20 enum: 20
same bits: the fields are the enum, named. (gcc on x86-64 puts the
first field in the lowest bit; C itself doesn't promise that order)
2. bits: 264389 chars, checksum 5235079687469029511, 260 read() calls, feof 1
fields: 264389 chars, checksum 5235079687469029511, 260 read() calls, feof 1
a directory: bits ferror 1, fields ferror 1
table full after 15 more (bits), 15 more (fields)
3. code size, from nm -t d -S ./cc-c:
f_fillbuf 178 bytes
f_fopen 302 bytes
my_fillbuf 185 bytes
my_fopen 283 bytes
the flag test that opens each fillbuf, from objdump -d:
my_fillbuf:
mov 0x18(%rdi),%eax
and $0x19,%edx
cmp $0x1,%edx
f_fillbuf:
movzbl 0x18(%rdi),%eax
and $0x19,%edx
cmp $0x1,%dl
4. 300 whole-file reads, ms: bits 160 fields 159 | bits 159 fields 159
flag test alone, 100000000 calls: fields 178 bits 141 | fields 178 bits 141
a whole file is 264389 my_getc but only 260 my_fillbuf calls: the flags
are looked at once per 1024 bytes, and one read() costs far more
win@DESKTOP-MEIH88T:~/webdev-projects$ gcc -Wall -Wextra -O2 -falign-functions=64 cc-c.c -o cc-c && ./cc-c
1. sizeof(MYFILE) 32, sizeof(FFILE) 32; int flag 4 bytes, the 5 fields 4 bytes
is_read alone -> raw bits 01 enum: 01
is_write alone -> raw bits 02 enum: 02
is_unbuf alone -> raw bits 04 enum: 04
is_eof alone -> raw bits 10 enum: 10
is_err alone -> raw bits 20 enum: 20
same bits: the fields are the enum, named. (gcc on x86-64 puts the
first field in the lowest bit; C itself doesn't promise that order)
2. bits: 264389 chars, checksum 5235079687469029511, 260 read() calls, feof 1
fields: 264389 chars, checksum 5235079687469029511, 260 read() calls, feof 1
a directory: bits ferror 1, fields ferror 1
table full after 15 more (bits), 15 more (fields)
3. code size, from nm -t d -S ./cc-c:
f_fillbuf 178 bytes
f_fopen 302 bytes
my_fillbuf 185 bytes
my_fopen 283 bytes
the flag test that opens each fillbuf, from objdump -d:
my_fillbuf:
mov 0x18(%rdi),%eax
and $0x19,%edx
cmp $0x1,%edx
f_fillbuf:
movzbl 0x18(%rdi),%eax
and $0x19,%edx
cmp $0x1,%dl
4. 300 whole-file reads, ms: bits 160 fields 159 | bits 159 fields 160
flag test alone, 100000000 calls: fields 118 bits 118 | fields 118 bits 118
a whole file is 264389 my_getc but only 260 my_fillbuf calls: the flags
are looked at once per 1024 bytes, and one read() costs far more