K&R2 Exercise 8-2

2026-10-21

#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