--- a/logscan.c 2026-09-23 13:38:26 +++ b/logscan.c 2026-09-23 13:38:26 @@ -1,21 +1,21 @@ -/* EXERCISE 05 — BROKEN ON PURPOSE. Do not copy this shape into real code. +/* EXERCISE 05 — REPAIRED. * * A crash-log scanner. It walks a log file line by line and tallies the lines - * that contain a marker. The author needed to split on newlines and could not - * find a "read a line" system call, so they wrote one: read a byte, test it, - * repeat. + * that contain a marker. * - * Symptom as reported by the field: "scanning a 64 MB log takes twenty - * seconds. The disk is idle the whole time and we are not even at 100% of one - * core. Is the SSD broken?" + * The repair: read in BUFFER-sized gulps and split lines in user space. The + * file is unchanged, the disk does the same work, and the answer is identical. + * What changes is the number of user-to-kernel transitions: one per 64 KiB + * instead of one per byte. * + * This is not "caching" and it does not make the storage faster. The page + * cache was already holding the data; the cost being removed is the system + * call itself. + * * Build and run: - * clang -O2 -g -Wall -Wextra logscan.c -o /tmp/logscan_broken - * /tmp/logscan_broken + * clang -O2 -g -Wall -Wextra logscan.c -o /tmp/logscan_fixed + * /tmp/logscan_fixed * - * The answer this program produces is CORRECT. Nothing here is a race and - * nothing here is a leak. - * * Every measurement is printed as key=value so a script can assert on it. */ #include @@ -89,26 +89,35 @@ long lines = 0, markers = 0, syscalls = 0; unsigned long long checksum = 0; + /* 64 KiB is comfortably past the knee of the size/throughput curve on + * every machine this has been run on, and it is small enough to stay off + * the heap's large-allocation path and out of cache-hostile territory. */ + enum { BUFFER = 64 * 1024 }; + char *buf = malloc(BUFFER); + if (!buf) { perror("malloc"); close(fd); unlink(path); return 1; } + double t0 = now_ms(); for (;;) { - char c; - /* One system call per byte. Each one is a full user-to-kernel - * transition to copy a single character that the kernel already had - * sitting in the page cache. */ - ssize_t n = read(fd, &c, 1); + /* One system call per BUFFER bytes. The line splitting that used to + * happen between system calls now happens between memory accesses. */ + ssize_t n = read(fd, buf, BUFFER); syscalls++; if (n <= 0) break; - if (c == '\n') { - line[len < sizeof line - 1 ? len : sizeof line - 1] = '\0'; - lines++; - if (strstr(line, "MARKER")) markers++; - checksum += (unsigned long long)len; - len = 0; - } else if (len < sizeof line - 1) { - line[len++] = c; + for (ssize_t i = 0; i < n; i++) { + char c = buf[i]; + if (c == '\n') { + line[len < sizeof line - 1 ? len : sizeof line - 1] = '\0'; + lines++; + if (strstr(line, "MARKER")) markers++; + checksum += (unsigned long long)len; + len = 0; + } else if (len < sizeof line - 1) { + line[len++] = c; + } } } double ms = now_ms() - t0; + free(buf); close(fd); unlink(path);