Notes, scribbles, and the occasional longer post.


Notes, scribbles, and the occasional longer post.
Part 1 made each tree smaller. This part makes fewer objects instead. You only need this when two things are true: you have a lot of objects, and a big part of each one is the same as in many others. A million trees with three species qualify. A thousand trees don’t, and neither do a million trees that are all different. The problem In a game, a tree also needs to look like something. Say the forest now comes from a file, and every tree has a species with a colour and a texture: ...
Say you’re simulating a forest: a million trees, each with a position and an age. class Tree: def __init__(self, x, y, age): self.x = x self.y = y self.age = age Three small ints per tree. In C that’s 12–24 bytes a tree, so a million trees would take about 24 MB. In Python 3.10 the same million trees take 152 MB. In 3.13 they take 96 MB, without changing a line. There are two ways to make a million objects cheap: make each object smaller, or make fewer objects. This post is about the first one. It covers where those bytes go, what __slots__ removes, and how much of that CPython 3.11+ now does for you. ...
The Makefile for a one-binary C project, explained piece by piece. CC = gcc CFLAGS = -std=gnu23 -Og -g3 -Wall -Werror -Wextra -Wwrite-strings OUT = out BIN = $(OUT)/tinybox all: $(BIN) # Any out/<name> from <name>.c, e.g. `make out/scratch` $(OUT)/%: %.c Makefile | $(OUT) $(CC) $(CFLAGS) -o $@ $< $(OUT): mkdir -p $@ tinybox: $(BIN) scratch: $(OUT)/scratch run: $(BIN) ./$(BIN) clean: rm -rf $(OUT) .PHONY: all run clean tinybox scratch Rules target: prerequisites recipe make rebuilds target if it doesn’t exist or any prerequisite is newer (by file modification time). That’s the whole algorithm. The recipe lines must start with a tab. Spaces give *** missing separator. Stop. A rule with no recipe (tinybox: $(BIN)) just says “to make this, make that”. Variables CC, CFLAGS are conventional names (make’s built-in rules use them too). Use with $(NAME). Override from the command line: make CFLAGS=-O2. ...
Every GCC diagnostic has the same shape: scratch.c:3:13: error: initialization discards 'const' qualifier from pointer target type [-Werror=discarded-qualifiers] 3 | char *s = "scratching surface"; | ^~~~~~~~~~~~~~~~~~~~ cc1: all warnings being treated as errors Line 1: file:line:col: severity: message [flag] scratch.c:3:13: where. Columns start at 1. Most terminals/editors make this clickable. severity: error stops the build; warning doesn’t; note: adds context to the diagnostic above it. [-Werror=discarded-qualifiers]: the flag that controls this check. -Werror= means it’s really the warning -Wdiscarded-qualifiers, promoted to an error by -Werror. Search man gcc for /Wdiscarded-qualifiers. -Wno-<name> disables it, which is rarely the right fix. The message has a fixed vocabulary The first words say where in the code the problem is: ...
I wrote a cast that compiled, ran fine, and was still wrong. Figuring out why finally made me learn how to read C pointer types properly. The mistake execv wants its argv as char *const argv[]. I wanted mine fully const: const char *const argv[] = {"/bin/sh", nullptr}; execv(argv[0], argv); error: passing argument 2 of 'execv' from incompatible pointer type [-Wincompatible-pointer-types] note: expected 'char * const*' but argument is of type 'const char * const*' So I cast it to what I thought execv wants: ...
A string literal like "/bin/sh" is stored in .rodata, which is mapped read-only (see where things live in a C program). But its C type is plain char[N], not const char[N] (C++ fixed this, C kept it for historical reasons). So the compiler happily lets you do this: char *p = "/bin/sh"; // compiles without a warning by default p[0] = 'X'; // undefined behavior; in practice SIGSEGV $ ./ub Segmentation fault (exit status 139 = 128 + SIGSEGV) -Wwrite-strings: make the compiler tell you -Wwrite-strings gives literals the type const char[N], so pointing a plain char * at one becomes a warning (an error with -Werror): ...
A compiled C program is an ELF file split into sections. The loader maps them into memory with different permissions, and those permissions are what the CPU enforces at runtime. Section Holds Permissions Example .text machine code read + execute main .rodata read-only data read (no write) string literals, static const tables .data initialized globals / statics read + write static char buf[] = "hi"; .bss zero-initialized globals / statics read + write static int counter; stack locals, return addresses read + write char buf[8]; inside a function .data and .rodata are both in the binary; “in the binary” doesn’t mean read-only. The section decides that. .bss takes no space in the file (type NOBITS): the loader just hands out zeroed memory. Writing to .rodata (e.g. through a pointer to a string literal) → the page isn’t writable → SIGSEGV. See it yourself Sections and their flags (A = alloc, W = write, X = exec): ...