<!-- mobian-agent-page publisher="dailydev" canonical="https://daily.dev/posts/writing-a-fast-compiler-5hnqshf7f" -->

---
title: Writing a Fast Compiler | daily.dev
description: A deep dive into techniques for building fast programming language compilers, targeting 500,000+ lines per second on a single core. Covers language design...
canonical: https://daily.dev/posts/writing-a-fast-compiler-5hnqshf7f
twitter:card: summary_large_image
twitter:site: @dailydotdev
og:type: website
og:site_name: daily.dev
og:title: Writing a Fast Compiler | daily.dev
og:description: A deep dive into techniques for building fast programming language compilers, targeting 500,000+ lines per second on a single core. Covers language design...
og:url: https://daily.dev/posts/writing-a-fast-compiler-5hnqshf7f
og:image: https://api.daily.dev/og/posts/5Hnqshf7f.png
og:image:alt: Writing a Fast Compiler
og:image:width: 1200
og:image:height: 630
og:locale: en
---

> ## Documentation Index
> Fetch the complete documentation index at: https://daily.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Writing a Fast Compiler

**[Lobsters](https://daily.dev/sources/lobsters)** · 11 min read · 0 upvotes · 0 comments

## Summary

A deep dive into techniques for building fast programming language compilers, targeting 500,000+ lines per second on a single core. Covers language design choices favoring easy parsing, memory region allocation instead of malloc/free, representing identifiers as integers instead of strings, optimizing the common (error-free) path, lazy compilation of unused code, and building a custom x64 code generator that is 30x faster than LLVM. Includes concrete data structure and algorithm choices like linear scan register allocation and two-pass assembly.

## Full article

daily.dev links to this article rather than hosting it. Read it at the original source: <https://tibleiz.net/blog/2024-02-04-writing-a-fast-compiler.html>

## Questions this post answers

### Why is a custom x64 code generator faster than using LLVM as a compiler backend?

A custom x64 backend can be roughly 30 times faster than LLVM for code generation, though at a large cost in implementation complexity. One example LLVM backend used about 2,000 lines of straightforward code, while the equivalent custom x64 backend required over 10,000 lines of complex code and still lacked floating point support.

_Comparing backend trade-offs like this is easier when you follow compiler engineering discussions on daily.dev._

### Why should compilers store identifiers as integers instead of strings?

Storing identifiers as integers avoids slow string comparisons and hashing, since CPUs work efficiently with fixed-size integers but not variable-length strings. During lexical analysis, each distinct identifier is assigned a unique integer the first time it's seen, reusing the lookup already required for keyword matching, so the added cost is minimal.

_Developers optimizing lexer performance can find more compiler internals writeups on daily.dev._

### What is a memory region allocator and why is it useful for compilers?

A memory region is a contiguous block of memory where individual allocations just advance a pointer and cannot be freed individually; the entire region is freed at once. This makes allocation and deallocation extremely fast, fits well with compiler compilation units, and works best paired with fixed-size arrays or linked lists rather than resizable arrays or open-addressing hash tables.

_daily.dev surfaces memory management deep dives for engineers designing performance-critical systems._

## Community take

How the wider developer community reacted, aggregated from 3 discussions and 19 comments across hackernews, lobsters (as of 2026-08-18).

**TL;DR:** Discussion centers on the claim that a custom x64 backend is 30x faster than LLVM, with several experienced compiler authors arguing the article's emphasis on lexing/parsing is misplaced since those stages are rarely the bottleneck compared to type checking, inlining, and LLVM lowering/codegen.

**Sentiment:** 15% positive · 35% mixed · 50% skeptical

**The case for**

- Interning identifiers as integers and optimizing the fast/error-free path are seen as generally applicable advice that helps across compiler phases, not just parsing.
- The 30x speedup from replacing LLVM with a custom backend is viewed as the most compelling and noteworthy claim in the piece.
- One commenter notes the author's other projects are impressively executed.

**The pushback**

- Multiple commenters argue lexing and parsing are usually a small fraction of compile time, so devoting so much of the article to them is misguided.
- Several say the real hard parts of a fast compiler—type checking, inlining, and lowering—are glossed over despite being where most time and difficulty actually lie.
- Some feel the piece reads like generic 'draw the rest of the owl' advice without enough concrete substance on the hardest stages.
- A commenter questions the 500K LOC/s benchmark as arbitrary and potentially meaningless without specifying what kind of code is being compiled.
- Detailed pushback that LLVM's slowness is often due to how it's used (IR constructs, pass configuration, instruction selector choice) rather than being inherently and uniformly slow.

**By community**

- hackernews (mixed): No substantive comments were provided for either Hacker News thread.
- lobsters (heated): A detailed back-and-forth debate emerged over whether the article's focus on lexing/parsing is misguided, alongside deep technical disagreement about why and how much LLVM's compile times are slow.

**Hottest debate:** Whether lexing/parsing optimization matters at all given that type checking, inlining, and LLVM codegen dominate compile time for most real-world compilers.

**Open questions**

- How exactly was the custom x64 backend made 30x faster than LLVM, and would similar techniques transfer to other compilers?
- Is LLVM's slowness fundamentally unavoidable, or mostly a result of common misuse of its IR constructs and pass pipelines?
- What does the 500,000 LOC/second benchmark actually represent given wide variance in code complexity (e.g., generic-heavy vs. plain code)?

**Highlights**

> I was hoping for a bit more substance, rather than just a list of somewhat vague things to do. For example, there's a decent portion dedicated to lexical analysis and parsing and yet that's usually the least time consuming process of a compiler (unless you're implementing a dump source-to-source compiler). It's also a stage that's pretty easy to speed up dramatically by performing the work in parallel. To illustrate, a non-incremental debug build of Inko's entire test suite (which effectively also means it includes the entire standard library, and clocks in at somewhere between 50 000 and 100 000 LOC) takes about 4.2 seconds in total on my desktop, of which only 2% is spent in lexing and parsing while 73% is spent in lowering to and compiling the LLVM IR to machine code.  I did recently spend some time looking into optimizing the lexer and parser by doing fewer string allocations in a bunch of places where they are technically redundant. My findings were that the resulting code wasn't something I was happy with, and there was no measurable difference in performance. The areas where things are more interesting to optimize (and to discuss on how to do that) would be type checking. This stage typically involves a lot of mutations of shared data structures such that you can't just slap a bunch of threads on it and call it a day. You'll likely also end up producing a lot of garbage that needs to be cleaned up, such as when you specialize generic types and functions as the original version has to be kept around until this stage is done. Another time consuming stage is inlining, which suffers from the same issues: lots of mutations, difficult to parallelize, and lots of copying (and thus an increase in memory usage). In fact, the inliner is probably the most time consuming part of Inko's compiler apart from the LLVM stage. It's also the most memory hungry because of all the copying it needs to do. I'd love to read more about those stages, yet there's sadly not a lot available on it. My best guess is that most writing their own compiler either give up before reaching a point where they need to implement this themselves, or they just never do and defer to some underlying library/compiler (e.g. LLVM) to do all the work for them.
> — [yorickpeterse on lobsters · 7 points, 2 comments](https://lobste.rs/s/6i1at9/writing_fast_compiler#c_afhoxv)

> Yeah, my experience has been that the hard parts of building a compiler intended for serious use (as opposed to e.g. a hobby project created primarily for fun and/or the author's learning) get glossed over by learning materials about compilers. For example, the article includes this diagram: ```   Lexical Analysis ---> Syntax Analysis ---> Build ---> Code Generation ``` Everything I would categorize as "the hard parts" of building a compiler all live in that step labeled "Build" - type-checking, optimizing, and lowering to an A-normal form (or something similarly primitive) representation. Those are the hard parts, and I'd guess maybe 90% of the time spent on a production compiler is spent on them. Everything up through and including parsing is so much easier it's closer to "trivial" than "comparable." I'd say code generation, including to x64, ARM, wasm, and LLVM IR, is only somewhat harder than parsing. Those hard parts also account for ~all of the time spent in compilation, with the two exceptions of LLVM and linking. LLVM is famously slow to do anything, including to emit completely unoptimized code, and even modern linkers like mold and lld can be a big source of build time. This has been not only my personal experience, but also what I've heard from other authors of production compilers that emit machine code. So unless you're doing your own low-level optimizations and/or linking, making compilation faster also usually means spending ~all your time on things in that "Build" step. That said, I'm not surprised the article focuses on the easy parts - the harder stuff is not only hard, it takes a lot more time to explain! When I taught a 1-day workshop on implementing a type checker with type inference (https://master.dev/courses/type-compiler/) the challenge was fitting all the necessary background info into the time slot of a single day. Compilers are a deep topic!
> — [rtfeldman on lobsters · 3 points](https://lobste.rs/s/6i1at9/writing_fast_compiler#c_5r2epm)

> > LLVM is great; it is very easy to use, it generates highly optimized code and supports many platforms. But it is slow, I've observed a factor of 30 speed up by developing my own x64 backend.  I think everything else in this article is made kind of irrelevant by this bit. In most cases, a compiler targeting LLVM will have LLVM as its performance bottleneck. In my own compiler, Clang represents 88.2% of the runtime, and I haven't particularly done much optimization to the other parts of my compiler (other than the choice to implement it in C). Even if I made the non-Clang code 1,000,000x faster, it would only make the overall compilation time 13% faster. OP mentions replacing the LLVM backend with a handwritten x86 backend (not yet feature complete), but that should be the point of the article if the article is about making a fast compiler. The other optimizations are chump change unless the initial implementation was somehow slower than the LLVM bottleneck.
> — [spillybones on lobsters · 2 points, 2 comments](https://lobste.rs/s/6i1at9/writing_fast_compiler#c_hgrnx3)

> LLVM compile-time performance depends on a variety of factors -- while never truly fast, it has various degrees of slowness. I get tired of people complaining about LLVM being slow without providing details on how they use LLVM -- because it's not unlikely that they were "using it wrong" in some way (LLVM is hard to use correctly). The tuning knobs are: - Used IR constructs. LLVM supports a fair amount of IR constructs, but LLVM isn't optimized for many of them, so they will cause trips to slow code paths. Examples of avoidable constructs are values of aggregate type (e.g. structs, arrays), integers larger than i64, and unusual calling conventions. One can often achieve a substantial compile-time speed up by moving away from badly supported constructs (I've measured a >2x c-t reduction in one application). (I've proposed banning aggregate values some time ago, because this is a common trap for newcomers, but couldn't get a consensus on that due to front-end author convenience...) - IR structure. LLVM is bad at handling large functions, several algorithms have super-linear runtime. (E.g., compile-time grows O(n^2) by the number of calls per function, O(n^3) for loop nesting depth (becomes problematic at a few hunders levels).) Dense control-flow graphs are also bad (the general assumption is that the number of nodes and edges is roughly similar), as are blocks with many predecessors (O(n^2)). Not generating huge functions can substantially improve compile times. - Optimization passes. These are configurable, or completely avoidable if the IR doesn't need any transformations. The existing O1/O2/3 pipelines are somewhat slow, but they also support a lot -- but for common code, most of the passes do nothing. Tuning the pass pipeline for the specific needs can greatly reduce costs here. When building optimized code, it also helps to generate SSA IR from the start and save on some initial simplification passes (e.g. for SSA construction). - Back-end optimization level (this is independent!). LLVM has two back-end pipelines: the O0 pipeline (no optimizations, simple ISel, fast regalloc) and the optimizing pipeline. Both can be used with unoptimized and optimized IR. The optimizing pipeline is ~7-10x slower than the unoptimizing one. Using the optimizing pipeline with unoptimized (alloca/load/store) IR is pointless. Using the unoptimizing pipeline with optimized IR also works, but has an impact on code quality; from my observations, the slowdown can range from <20% (call-heavy code) to 3x (hot loops that depend on good regalloc). - Instruction selector. This is independently configurable from the back-end optimization level. LLVM has SelectionDAG (for optimized code), FastISel (for unoptimized code), and GlobalISel (intending to, maybe eventually, replace the former two). FastISel is reasonably fast, but only supports constructs typically seen in unoptimized IR; for unsupported constructs it falls back to SelectionDAG per basic block -- and SelectionDAG is *very* slow (although also very powerful). It is possible to use FastISel with the optimizing pipeline and I've also observed substantial compile-time improvements with comparably low performance impact from doing so. GlobalISel is a bit faster than SelectionDAG but much (2-3x) slower than FastISel. Unfortunately, it's the default for AArch64 -O0, but you can get FastISel back through configuration options.
> — [aengelke on lobsters · 1 points](https://lobste.rs/s/6i1at9/writing_fast_compiler#c_bb8l3c)

> 1. A goal like "500K SLOC/second" seems like a arbitrary and unreasonable. Not the 500K in particular, you can replace that with any number. But it reveals to me that this new language provides no *serious* capabilities at the type-system and compile-time level. Compiling 500K SLOC of traits and highly-generic Rust code would take a much longer time than compiling 500K SLOC of plain, linear, macro-less C code. Now it could be an *average* of 500K SLOC, but I think an average in this context would be meaningless. 2. As the top comment says, most of these optimizations are kind-of pre-mature. Parsing and lexing take up not a lot of time. But I would like to link [Logos](https://docs.rs/logos) here, which is a lexer generator for Rust and generates one of the fastest lexers. I would've been more interested in reading up on how their LLVM x86_64 backend is 30x faster than the native one, tbh. LLVM is slow, but 30x is a brow-raising claim in this context.
> — [vforvaline on lobsters · 1 points, 1 comments](https://lobste.rs/s/6i1at9/writing_fast_compiler#c_i69fdc)

**Source threads**

- [hackernews](https://news.ycombinator.com/item?id=39541135) · 3 points · 0 comments
- [lobsters](https://lobste.rs/s/6i1at9/writing_fast_compiler) · 25 points · 19 comments
- [hackernews](https://news.ycombinator.com/item?id=49329603) · 2 points · 0 comments

## Similar posts on daily.dev

- [Build a Compiler in Five Projects](https://daily.dev/posts/build-a-compiler-in-five-projects-4vyq4uhvw) · Lobsters · 3 upvotes · 0 comments
- [A Basic Just-In-Time Compiler](https://daily.dev/posts/a-basic-just-in-time-compiler-mrdvhvck5) · Hacker News · 1 upvotes · 0 comments
- [How To Make a Fast Dynamic Language Interpreter](https://daily.dev/posts/how-to-make-a-fast-dynamic-language-interpreter-ax510brkl) · Hacker News · 0 upvotes · 0 comments
- [Static Allocation For Compilers](https://daily.dev/posts/static-allocation-for-compilers-jjuf4dofn) · matklad · 1 upvotes · 0 comments

---

Tags: [#general-programming](https://daily.dev/tags/general-programming), [#performance](https://daily.dev/tags/performance), [#compiler](https://daily.dev/tags/compiler)

[View this post on daily.dev](https://daily.dev/posts/writing-a-fast-compiler-5hnqshf7f)

```json
{"@context":"https://schema.org","@graph":[{"@type":"Organization","@id":"https://daily.dev/#organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180},"sameAs":["https://twitter.com/dailydotdev","https://github.com/dailydotdev","https://www.linkedin.com/company/daily-dev-ltd"]},{"@type":"WebSite","@id":"https://daily.dev/#website","url":"https://daily.dev","name":"daily.dev","publisher":{"@id":"https://daily.dev/#organization"},"potentialAction":{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https://daily.dev/search?q={search_term_string}"},"query-input":"required name=search_term_string"}}]}
{"@context":"https://schema.org","@type":"TechArticle","headline":"Writing a Fast Compiler","url":"https://daily.dev/posts/writing-a-fast-compiler-5hnqshf7f","mainEntityOfPage":{"@type":"WebPage","@id":"https://daily.dev/posts/writing-a-fast-compiler-5hnqshf7f"},"datePublished":"2026-08-17T22:53:38.530Z","dateModified":"2026-08-18T08:39:43.684Z","description":"A deep dive into techniques for building fast programming language compilers, targeting 500,000+ lines per second on a single core. Covers language design...","image":"https://media.daily.dev/image/upload/s--HRgLpUt6--/f_auto/v1722860399/public/Placeholder%2003","thumbnailUrl":"https://media.daily.dev/image/upload/s--HRgLpUt6--/f_auto/v1722860399/public/Placeholder%2003","isAccessibleForFree":true,"articleSection":"Lobsters","inLanguage":"en","publisher":{"@type":"Organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180}},"author":{"@type":"Organization","name":"Lobsters","logo":"https://media.daily.dev/image/upload/s--tl8v_Fku--/f_auto,t_logo/v1698841318/logos/lobste.jpg","url":"https://daily.dev/sources/lobsters"},"commentCount":0,"discussionUrl":"https://daily.dev/posts/writing-a-fast-compiler-5hnqshf7f","interactionStatistic":[{"@type":"InteractionCounter","interactionType":{"@type":"LikeAction"},"userInteractionCount":0},{"@type":"InteractionCounter","interactionType":{"@type":"CommentAction"},"userInteractionCount":0}],"keywords":"general-programming,performance,compiler","timeRequired":"PT11M"}
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://daily.dev"},{"@type":"ListItem","position":2,"name":"Lobsters","item":"https://daily.dev/sources/lobsters"},{"@type":"ListItem","position":3,"name":"Writing a Fast Compiler"}]}
{"@context":"https://schema.org","@type":"FAQPage","@id":"https://daily.dev/posts/writing-a-fast-compiler-5hnqshf7f#faq","mainEntity":[{"@type":"Question","name":"Why is a custom x64 code generator faster than using LLVM as a compiler backend?","acceptedAnswer":{"@type":"Answer","text":"A custom x64 backend can be roughly 30 times faster than LLVM for code generation, though at a large cost in implementation complexity. One example LLVM backend used about 2,000 lines of straightforward code, while the equivalent custom x64 backend required over 10,000 lines of complex code and still lacked floating point support. Comparing backend trade-offs like this is easier when you follow compiler engineering discussions on daily.dev."}},{"@type":"Question","name":"Why should compilers store identifiers as integers instead of strings?","acceptedAnswer":{"@type":"Answer","text":"Storing identifiers as integers avoids slow string comparisons and hashing, since CPUs work efficiently with fixed-size integers but not variable-length strings. During lexical analysis, each distinct identifier is assigned a unique integer the first time it's seen, reusing the lookup already required for keyword matching, so the added cost is minimal. Developers optimizing lexer performance can find more compiler internals writeups on daily.dev."}},{"@type":"Question","name":"What is a memory region allocator and why is it useful for compilers?","acceptedAnswer":{"@type":"Answer","text":"A memory region is a contiguous block of memory where individual allocations just advance a pointer and cannot be freed individually; the entire region is freed at once. This makes allocation and deallocation extremely fast, fits well with compiler compilation units, and works best paired with fixed-size arrays or linked lists rather than resizable arrays or open-addressing hash tables. daily.dev surfaces memory management deep dives for engineers designing performance-critical systems."}}]}
```

