Anima Mundi

Slow software is a design problem

For years, GTA Online took about six minutes to load on PC. In 2021 a player who goes by t0st, with no access to the source code, traced the delay to two bugs. The game read a 10 MB JSON file of about 63,000 shop items. Its parser re-measured the whole 10 MB text every time it read one value. Then, before storing each item, it compared it against every item already stored: roughly two billion comparisons, for a list that had no duplicates in it. They patched both. Loading dropped to 1 minute 50. Rockstar shipped an official fix in March 2021.

Nobody rewrote GTA in a faster language. The game was already in C++. It got 3x faster because it stopped doing useless work.

Sean Goedecke makes the general case in a recent post: software is slow because of how it is designed, much more than because of the language it is written in. With AI agents now able to rewrite whole codebases in Rust over a weekend, the point matters more than it did a year ago.

Why the language rarely matters

Most business software is I/O bound: it spends its time waiting on a database, a disk or another server, and the CPU sits idle meanwhile. Goedecke estimates that in a typical web app, the database takes around half of each request. That figure comes from his own experience at a couple of companies, so treat it as a ballpark.

Amdahl's law does the rest. If half the time is waiting, making the other half infinitely fast caps your gain at 2x. A real rewrite gets you less.

The heavy lifting is often already in fast code anyway. A Python machine learning script hands its maths to PyTorch, which runs C++ and GPU code. A Ruby app renders Markdown through a C library. Rewriting the Python around it changes almost nothing.

Even in CPU-heavy code, design tends to win. In 2018 Bryan Cantrill ported a carefully tuned C program to Rust, expecting it to be slower. A naive Rust version came out about 30% faster. The reason was the data structure: Rust's standard library gave him a B-tree, which plays much better with the CPU cache than the binary tree his C code used. Same story with a recent assembly rewrite of OpenAI's tokenizer: much of its 5x gain came from dropping regular expressions, a choice available in any language.

The redesigns that actually pay off

Goedecke says almost every big speed-up he has shipped came from a short list of changes. Here it is, extended with the cases above.

  1. Stop doing work twice. GTA's duplicate check grew with the square of the list size. 63,000 items meant billions of steps. A hash table checks for duplicates in one step per item. Loops inside loops over growing data are the first thing to look for.
  2. Batch your database calls. The classic trap is the "N+1 query": load 100 posts in one query, then load each post's author in its own query. That is 101 round trips to the database where 1 or 2 would do. ORMs (libraries that turn database rows into code objects) make this easy to write by accident, and it stays invisible on a laptop with 10 test rows.
  3. Cache slow answers. If an external API returns the same exchange rate for an hour, ask once and keep the answer. A cache turns a 300 ms call into a lookup in memory.
  4. Get slow work out of the user's way. Sending an email, resizing an image or generating a PDF does not need to happen while the user waits. Put it in a background job and answer right away.
  5. Run independent work in parallel. Three API calls that don't depend on each other can run at the same time. The request then takes as long as the slowest call, not the sum of the three. Large downloads can be split into ranges and fetched side by side.
  6. Only fetch what's on screen. A list of 5,000 orders shows 50 per page. Loading customer details for all 5,000 is wasted work for 4,950 of them.
  7. Pick the data structure that fits. Cantrill's B-tree, GTA's missing hash table. Same language, different shape of data, very different speed.

None of these needs a new language. Most of them are easier to spot in a high-level language, where the code is short enough to read.

When the language does matter

Discord is the honest counter-example. Its "Read States" service tracks which messages each user has read, billions of entries, hit on every message sent. In Go it was fast most of the time but spiked every two minutes. Go's garbage collector (the part of the runtime that frees unused memory) ran at least that often and had to scan a cache holding tens of millions of entries. Tuning did not help. Shrinking the cache helped the spikes but sent more requests to the database.

They rewrote it in Rust, which has no garbage collector, and the spikes disappeared. Even there, one of their three follow-up optimizations was a data structure swap, from a hash map to a B-tree.

So rewrites have a place: a small, hot, CPU-bound piece of code that has already been redesigned and still isn't fast enough. That is a function, rarely a whole app.

In practice

  1. Measure before changing anything. A profiler shows where time goes, and it is rarely where you guessed.
  2. Count database queries per page. Dozens for a single screen usually means an N+1.
  3. Look for loops inside loops over data that grows with users or time.
  4. Ask of every slow step: does the user need to wait for this?
  5. Consider a rewrite only for the one function the profiler still flags after all that.

The GTA fix came from someone who could not even see the source code.

The fastest code is the work you deleted.

#Tech #english