--- a/tilecache.c 2026-09-23 13:34:03 +++ b/tilecache.c 2026-09-23 13:34:03 @@ -1,21 +1,21 @@ -/* EXERCISE 02 — BROKEN ON PURPOSE. Do not copy this shape into real code. +/* EXERCISE 02 — REPAIRED. * * A map tile cache. Tiles arrive, are decoded into 32 KiB buffers, and are * evicted when the viewport moves. Eviction is interleaved: the tiles that * leave the viewport are scattered through the allocation order, not grouped * at the end of it. * - * Symptom as reported by the field: "we evict half the cache and the process - * footprint does not move at all." + * The repair: a tile is a whole number of pages with an independent lifetime, + * so it does not belong on the general-purpose heap at all. Each tile gets its + * own anonymous mapping, and munmap(2) returns those pages to the OS the + * moment the tile is evicted — in any order, with no neighbours to strand. * * Build and run: - * clang -O2 -g -Wall -Wextra tilecache.c -o /tmp/tiles_broken - * /tmp/tiles_broken + * clang -O2 -g -Wall -Wextra tilecache.c -o /tmp/tiles_fixed + * /tmp/tiles_fixed * - * Nothing here leaks. Every byte is freed before exit. That is exactly what - * makes the symptom confusing, and it is the point of the exercise. - * - * Every measurement is printed as key=value so a script can assert on it. + * The decoded bytes and the checksum are unchanged. Only who owns the pages + * is different. Every measurement is printed as key=value. */ #include #include @@ -24,6 +24,7 @@ #include #include #include +#include #define TILES 20000 #define TILE_BYTES (32 * 1024) @@ -70,10 +71,13 @@ double base = footprint_mb(); printf("phase=start footprintMB=%.1f\n", base); - /* Fill the cache. Every tile is touched, so every page is real. */ + /* Fill the cache. Every tile is touched, so every page is real. + * TILE_BYTES is a multiple of the page size, so one mapping per tile + * wastes nothing to rounding. */ for (int i = 0; i < TILES; i++) { - tiles[i] = malloc(TILE_BYTES); - if (!tiles[i]) { fprintf(stderr, "out of memory\n"); return 1; } + tiles[i] = mmap(NULL, TILE_BYTES, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANON, -1, 0); + if (tiles[i] == MAP_FAILED) { perror("mmap"); return 1; } memset(tiles[i], (i & 0x7f) + 1, TILE_BYTES); } double full = footprint_mb(); @@ -82,20 +86,21 @@ full, (double)TILES * TILE_BYTES / 1048576.0); /* Evict half the cache — interleaved, the way a moving viewport does it. - * Every odd-indexed tile survives, so no large contiguous span of the - * heap becomes free. */ - for (int i = 0; i < TILES; i += 2) { free(tiles[i]); tiles[i] = NULL; } + * Every odd-indexed tile survives — which no longer matters, because a + * tile's pages are not shared with its neighbours. */ + for (int i = 0; i < TILES; i += 2) { munmap(tiles[i], TILE_BYTES); tiles[i] = NULL; } double half = footprint_mb(); printf("phase=evicted-half footprintMB=%.1f\n", half); - /* Ask the allocator, explicitly, to give memory back. */ + /* Still asked, so the two runs print the same fields. With the tiles out + * of the heap there is nothing here for it to reclaim. */ size_t relieved = malloc_zone_pressure_relief(NULL, 0); double relief = footprint_mb(); printf("phase=after-pressure-relief footprintMB=%.1f reliefBytes=%zu\n", relief, relieved); /* Free the rest. */ - for (int i = 1; i < TILES; i += 2) { free(tiles[i]); tiles[i] = NULL; } + for (int i = 1; i < TILES; i += 2) { munmap(tiles[i], TILE_BYTES); tiles[i] = NULL; } double empty = footprint_mb(); printf("phase=empty footprintMB=%.1f\n", empty);