HNHacker News
TopNewBestAskShowJobs

MaskRay

257 karma · joined March 23, 2012

submissionscomments
MaskRay··on Mold: A Massively Parallel Linker
shameless plug: lld 23.x is significantly faster than 22.x! I wrote about the optimizations in https://maskray.me/blog/2026-04-12-recent-lld-elf-performanc... As a side note, parallel input file parsing and symbol resolution aren't a silver bullet. I have an experimental branch (net ~800 lines of code) adding this to lld/ELF, but it actually loses performance when restricted to --threads={1,2}

I read how mold implements parallel input file parsing a while ago. It is overly complicated. wild's model seems superior.

---

"Table 4." in the paper feels misleading to me. I have never measured such high speedups. In addition, on debug builds (especially --gdb-index builds), mold's edge over lld is smaller.

On ICF, some unknown fraction of mold's speedup is due to "doesn't do the symbol bootkeeping". The symbol tables are different in mold but identical in lld.

MaskRay··on The Art of 64-bit Assembly
Interesting to see that people are still spending much time on assembly languages :) I've had a lot of fun with it in recent years as well. (Shameless plug: I've written a few on LLVM integrated assembler regarding better fragments and improving expressions and relocations).

When comparing GNU Assembler and MASM, GAS is missing many features: while loop, string processing (.e.g strlen)

> page 3: "It’s important to understand that MASM converts macro invocation arguments to text values before doing the macro expansion"

Like MASM, GAS uses call-by-name evaluation by default. While GAS's altmacro mode does allow for expression evaluation using syntax like `%(1+2)`, it has strict limitations: it only supports absolute expressions and is restricted to argument positions.

In contrast, MASM's % operator (page 8) looks far more general.

MaskRay··on Zigzag Decoding with AVX-512
While we could utilize zigzag encoding (i>>31) ^ (i<<1) to convert SLEB128-encoded type/addend to use ULEB128 instead, the generate code is inferior to or on par with SLEB128 for one-byte encodings on x86, AArch64, and RISC-V. Haven't tried wider values - but zigzag encoding is likely slower as well

// One-byte case for SLEB128 int64_t from_signext(uint64_t v) { return v < 64 ? v - 128 : v; }

// One-byte case for ULEB128 with zig-zag encoding int64_t from_zigzag(uint64_t z) { return (z >> 1) ^ -(z & 1); }

MaskRay··on I hate compilers
GetProgramPath consulting PATH is a pretty standard behavior inspired by GCC. Clang driver may find an external program from its program paths (e.g. -ccc-install-dir, the GCC installation's bin directory, -B, ) and then PATH.

> Clang relies on address layout for ordering things

Every such instance is a bug. I have fixed many issues in 2023. There is even an upstream build bot https://discourse.llvm.org/t/reverse-iteration-bots/72224

MaskRay··on We tasked Opus 4.6 using agent teams to build a C Compiler
git switch v6.9

The riscv build succeeded. For the x86-64 build I ran into

    % make O=/tmp/linux/x86 ARCH=x86_64 CC=/tmp/p/claudes-c-compiler/target/release/ccc-x86 HOSTCC=/tmp/p/claudes-c-compiler/target/release/ccc-x86 LDFLAGS=-fuse-ld=bfd LD=ld.bfd -j30 vmlinux -k
    make[1]: Entering directory '/tmp/linux/x86'
    ...
      CC      arch/x86/platform/intel/iosf_mbi.o
    ccc: error: lgdtl requires memory operand
      AR      arch/x86/platform/intel-mid/built-in.a
    make[6]: *** [/home/ray/Dev/linux/scripts/Makefile.build:362: arch/x86/realmode/rm/wakeup_asm.o] Error 1
    ld.bfd: arch/x86/entry/vdso/vdso32/sigreturn.o: warning: relocation in read-only section `.eh_frame'
    ld.bfd: error in arch/x86/entry/vdso/vdso32/sigreturn.o(.eh_frame); no .eh_frame_hdr table will be created
    ld.bfd: warning: creating DT_TEXTREL in a shared object
    ccc: error: unsupported pushw operand
There are many other errors.

tinyconfig and allnoconfig have fewer errors.

    RELOCS  arch/x86/realmode/rm/realmode.relocs
    Invalid absolute R_386_32 relocation: real_mode_seg
Still very impressive.
MaskRay··on We tasked Opus 4.6 using agent teams to build a C Compiler
I want to verify the claim that it builds the Linux kernel. It quickly runs into errors, but yeah, still pretty cool!

make O=/tmp/linux/x86 ARCH=x86_64 CC=/tmp/p/claudes-c-compiler/target/release/ccc -j30 defconfig all

``` /home/ray/Dev/linux/arch/x86/include/asm/preempt.h:44:184: error: expected ';' after expression before 'pto_tmp__' do { u32 pto_val__ = ((u32)(((unsigned long) ~0x80000000) & 0xffffffff)); if (0) { __typeof_unqual__((__preempt_count)) pto_tmp__; pto_tmp__ = (~0x80000000); (void)pto_tmp__; } asm ("and" "l " "%[val], " "%" "[var]" : [var] "+m" (((__preempt_count))) : [val] "ri" (pto_val__)); } while (0); ^~~~~~~~~ fix-it hint: insert ';' /home/ray/Dev/linux/arch/x86/include/asm/preempt.h:49:183: error: expected ';' after expression before 'pto_tmp__' do { u32 pto_val__ = ((u32)(((unsigned long) 0x80000000) & 0xffffffff)); if (0) { __typeof_unqual__((__preempt_count)) pto_tmp__; pto_tmp__ = (0x80000000); (void)pto_tmp__; } asm ("or" "l " "%[val], " "%" "[var]" : [var] "+m" (((__preempt_count))) : [val] "ri" (pto_val__)); } while (0); ^~~~~~~~~ fix-it hint: insert ';' /home/ray/Dev/linux/arch/x86/include/asm/preempt.h:61:212: error: expected ';' after expression before 'pao_tmp__' ```

MaskRay··on Handling Long Branches
I accidentally published handling-long-branches.html . I actually removed it quickly, but Google Search already got it....

https://news.ycombinator.com/item?id=46759921 (Long branches in compilers, assemblers, and linkers), 2 days ago

MaskRay··on Maze Algorithms (2017)
Amazing! I read you article in 2012 when the link was https://homepages.cwi.nl/~tromp/maze.html I was learning Haskell and Ocaml and wrote my own article in Chinese then https://maskray.me/blog/2012-11-02-perfect-maze-generation

Now I should fix the link.

MaskRay··on Banned C++ features in Chromium
I recall that google3 switched to -funsigned-char for x86-64 a long time ago.
MaskRay··on Why senior engineers let bad projects fail
Open source project maintenance follows a similar model, but with a different set of stakes.

The "price tag" of voicing concerns is lower, yet raise them too often and you still earn a reputation as obstructionist. Meanwhile, the cost of accepting problematic changes can be higher—you may end up maintaining that code long after changing jobs. And unlike corporate politics, the "influence bank account" is public: communications are archived indefinitely.

There is a fascinating shift in how "withdrawals" are calculated: In a corporate hierarchy, the cost of dissent feels exponential: something like `cost = exp(their_level - your_level)`. Say, as a Google L3/L4/L5 engineer, opposing L6-L8 feels like trying to make a massive withdrawal with a tiny balance. In contrast, in OSS the cost almost stays constant despite the corporate level difference.

This created a paradox for me: leaving Google means less time for LLVM maintenance, but it also lets me voice objections more freely, without the shadow of internal performance ratings or hierarchical friction.

That said, I know I've been "withdrawing" heavily, including from a lot of previous colleagues. In a recent LLVM Project Council meeting:

> There is a pattern of behavior here of blocking contributions due to concerns about maintenance cost and design simplicity.

(I appreciate the transparency of making these meetings public, by the way.)

I had to respond at https://discourse.llvm.org/t/llvm-project-council-meeting-no...

MaskRay··on Huge Binaries
We are missing tooling to partition a huge binary into a few larger shared objects.

As my https://maskray.me/blog/2023-05-14-relocation-overflow-and-c... (linked by author, thanks! But I maintain lld/ELF instead of "wrote" it - it's engineer work of many folks)

Quoting the relevant paragraphs below:

## Static linking

In this section, we will deviate slightly from the main topic to discuss static linking. By including all dependencies within the executable itself, it can run without relying on external shared objects. This eliminates the potential risks associated with updating dependencies separately.

Certain users prefer static linking or mostly static linking for the sake of deployment convenience and performance aspects:

* Link-time optimization is more effective when all dependencies are known. Providing shared object information during executable optimization is possible, but it may not be a worthwhile engineering effort.

* Profiling techniques are more efficient dealing with one single executable.

* The traditional ELF dynamic linking approach incurs overhead to support [symbol interposition](https://maskray.me/blog/2021-05-16-elf-interposition-and-bsy...).

* Dynamic linking involves PLT and GOT, which can introduce additional overhead. Static linking eliminates the overhead.

* Loading libraries in the dynamic loader has a time complexity `O(|libs|^2*|libname|)`. The existing implementations are designed to handle tens of shared objects, rather than a thousand or more.

Furthermore, the current lack of techniques to partition an executable into a few larger shared objects, as opposed to numerous smaller shared objects, exacerbates the overhead issue.

In scenarios where the distributed program contains a significant amount of code (related: software bloat), employing full or mostly static linking can result in very large executable files. Consequently, certain relocations may be close to the distance limit, and even a minor disruption (e.g. add a function or introduce a dependency) can trigger relocation overflow linker errors.

MaskRay··on Huge Binaries
Note, sections without the SHF_ALLOC flag, such as `.debug_*` sections, do not contribute to the relocation distance pressure. Many 10+GiB binaries (likely due to not using split DWARF) might have much smaller code+data and not even close to the limit.

However, Google, Meta, and ByteDance have encountered x86-64 relocation distance issue with their huge C++ server binaries. To my knowledge industry users in other domains haven't run into this problem.

To address this, Google adopted the medium code model approximately two years ago for its sanitizer and PGO instrumentation builds. CUDA fat binaries also caused problems. I suggest that linker script `INSERT BEFORE/AFTER` for orphan sections (https://reviews.llvm.org/D74375 ) served as a key mitigation.

I hope that a range extension thunk ABI, similar to AArch64/Power, is defined for the x86-64 psABI. It is better than the current long branch pessimization we have with -mcmodel=large.

---

It seems that nobody has run into this .eh_frame_hdr implementation limitation yet

* `.eh_frame_hdr -> .text`: GNU ld and ld.lld only support 32-bit offsets (`table_enc = DW_EH_PE_datarel | DW_EH_PE_sdata4;`) as of Dec 2025.

MaskRay··on Ask HN: How would you lint natural-language specs before LLMs run them?
Rex
MaskRay··on A review of Nim 2: The good and bad with example code
The compiler team has never had more than three people at any time, but both the language and its feature set are impressive. I don't know whether they got any sponsorship. Now that the two most prolific developers are primarily working on Nimony, I wonder whether the stable Nim repository will get enough contributions or bug fixes.

The stable compiler repo has a fork https://github.com/nim-works/nimskull/ . It’s unfortunate that developers have different opinions.

MaskRay··on Benchmarking Compression Programs
Tested programs:

    lz4
    zstd
    brotli
    bzip3
    xz
    lzham: The build system is not well-polished for Linux. I have forked it, fixed build errors due to stdint.h, and installed lzhamtest. IMHO the command line program lzhamtest should be renamed to lzham.
    kanzi: There are a wide variety of transforms and entropy encoders, unusual for a compresion program. For the compression speed of enwik8, it's Pareto superior to xz, but decompression is slower.
MaskRay··on Zig 0.15.1
I don't follow Zig's development. https://ziglang.org/download/0.15.1/release-notes.html#Motiv... feels vague to me, and linking to a lengthy video isn't very clarifying. Still, I skimmed through it to get a sense of it.

Zig's approach seems to require users to explicitly call flush for any writes. This reminds me of a similar issue in LLVM's `raw_svector_ostream`. Before a 2015 commit https://github.com/llvm/llvm-project/commit/3d1173ba1a53cab0... , it only called flush at the end, which was efficient. That commit changed this, leading to unnecessarily slow performance. To not break existing users, the 2024 pull request https://github.com/llvm/llvm-project/pull/97704 tries to implement a new interface, but the changes would cause significant churn. @aengelke

MaskRay··on Jemalloc Postmortem
Thanks for sharing the insight!

As I observed when I was at Google: tcmalloc wasn't a dedicated team but a project driven by server performance optimization engineers aiming to improve performance of important internal servers. Extracting it to github.com/google/tcmalloc was complex due to intricate dependencies (https://abseil.io/blog/20200212-tcmalloc ). As internal performance priorities demanded more focus, less time was available for maintaining the CMake build system. Maintaining the repo could at best be described as a community contribution activity.

> Meta’s needs stopped aligning well with those of external uses some time ago, and they are better off doing their own thing.

I think Google's diverged from the external uses even long ago:) (For a long time google3 and gperftools's tcmalloc implementations were so different.)

MaskRay··on TPDE: A Fast Adaptable Compiler Back-End Framework
Build instructions

In the llvm/llvm-project repository

    git switch origin/release/19.x
    cmake -GNinja -S. -B/tmp/out/custom -DLLVM_TARGETS_TO_BUILD='X86;AArch64' -DLLVM_ENABLE_PROJECTS=clang -DLLVM_ENABLE_PLUGINS=off -DCMAKE_BUILD_TYPE=Release -DLLVM_LINK_LLVM_DYLIB=on
    # consider -DCLANG_ENABLE_OBJC_REWRITER=off -DCLANG_ENABLE_STATIC_ANALYZER=off -DCLANG_ENABLE_ARCMT=off -DCLANG_PLUGIN_SUPPORT=off
    ninja -C /tmp/out/custom clang LLVM FileCheck   # build clang and libLLVM.so and test utilities

In the tpde repository

    git submodule update --init
    cmake -GNinja -S. -Bout/debug -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=on -DCMAKE_PREFIX_PATH=/tmp/out/custom -DCMAKE_CXX_COMPILER=$HOME/Stable/bin/clang++ -DCMAKE_C_COMPILER=$HOME/Stable/bin/clang
/Stable/bin/clang

There are some failures:

``` % /tmp/out/custom/bin/llvm-lit out/debug/tpde/test/filetest ... Failed Tests (5): TPDE FileTests :: codegen/eh-frame-arm64.tir TPDE FileTests :: codegen/eh-frame-x64.tir TPDE FileTests :: codegen/simple_ret.tir TPDE FileTests :: codegen/tbz.tir TPDE FileTests :: tir/duplicate_funcs.tir ```

MaskRay··on 0+0 > 0: C++ thread-local storage performance
Regarding: `cmpb $0, %fs:__tls_guard@tpoff`, the per-function-call overhead is due to dynamic initialization on first use requirement:

> Block variables with static or thread(since C++11) storage duration are initialized the first time control passes through their declaration (unless their initialization is zero- or constant-initialization, which can be performed before the block is first entered). On all further calls, the declaration is skipped. --- https://en.cppreference.com/w/cpp/language/storage_duration

From https://maskray.me/blog/2021-02-14-all-about-thread-local-st...

> If you know x does not need dynamic initialization, C++20 constinit can make it as efficient as the plain old `__thread`. [[clang::require_constant_initialization]] can be used with older language standards.

Regarding `data16 lea tls_obj(%rip),%rdi` in the general-dynamic TLS model, yeah it's for linker optimization. The local-dynamic TLS model doesn't have data16 or rex prefixes.

Regarding "Why don’t we just use the same code as before — the movl instruction — with the dynamic linker substituting the right value for tls_obj@tpoff?"

Because -fpic/-fPIC was designed to support dlopen. The desired efficient GOTTPOFF code sequence is only feasible when the shared object is available at program start, in which case you can guarantee that "you would need the TLS areas of all the shared libraries to be allocated contiguously:"

    # x86-64
    movq ref@GOTTPOFF(%rip), %rax
    movl %fs:(%rax), %eax
With dlopen, the dynamic loader needs a different place for the TLS blocks of newly loaded shared libraries, which unfortunately requires one more indirection.

Regarding "... and I don’t say a word about GL_TLS_GENERATION_OFFSET, for example, and I could."

`GL_TLS_GENERATION_OFFSET` in glibc is for the lazy TLS allocation scheme. I don't want to spend my valuable time on its implementation... It is almost infeasible to fix on the glibc side.

MaskRay··on Tinkering with Neovim
I have placed a lot of focus on code navigation. Here is what I mentioned in my post:

  nmap('J', '<cmd>Telescope lsp_definitions<cr>', 'Definitions')
  nmap('<M-,>', '<cmd>Telescope lsp_references<CR>', 'References')

  nmap('H', '<cmd>pop<cr>', 'Tag stack backward')
  nmap('L', '<cmd>tag<cr>', 'Tag stack forward')

  nmap('xn', function() M.lsp.words.jump(vim.v.count1) end, 'Next reference')
  nmap('xp', function() M.lsp.words.jump(-vim.v.count1) end, 'Prev reference')
MaskRay··on Another variable-length integer encoding (2021)
The scheme proposed in this blog post is also called PrefixVarInt.

Signed integers can be represented with either zigzag encoding or sign extension. For the most common one-byte encoding, zigzag encoding is a worse scheme. https://maskray.me/blog/2024-03-09-a-compact-relocation-form...

My blog post is about a relocation format. I investigated a few schemes and concluded that LEB128 is the best for my use case. There are multiple reasons including super simple implementation:

    static uint64_t read_leb128(unsigned char **buf, uint64_t sleb_uleb) {
      uint64_t acc = 0, shift = 0, byte;
      do {
        byte = *(*buf)++;
        acc |= (byte - 128*(byte >= sleb_uleb)) << shift;
        shift += 7;
      } while (byte >= 128);
      return acc;
    }
    
    uint64_t read_uleb128(unsigned char **buf) { return read_leb128(buf, 128); }
    int64_t read_sleb128(unsigned char **buf) { return read_leb128(buf, 64); }
MaskRay··on Picking Glibc Versions at Runtime
Here is the glibc feature request: https://sourceware.org/bugzilla/show_bug.cgi?id=31959 ("Feature request: special static-pie capable of loading the interpreter from a relative path")
MaskRay··on rr – record and replay debugger for C/C++
I almost use rr every day, along with a gdb frontend: cgdb.

rr record /tmp/Debug/bin/llvm-mc a.s && rr replay -d cgdb

I've have success story with some bugs only reproducible with LTO. Without rr it would be a significant challenge.

It would be nice if Linux kernel could be debugged with rr. Does anyone have success with kernel under rr+qemu ? :)

MaskRay··on Mapping Symbols: Rethinking for Efficiency
tl;dr

- Mapping symbols describe data in code and instruction sets transition (e.g. A32<=>T32).

- Pending LLVM integrated assembler patch that will eliminate almost all mapping symbols without breaking disassemblers https://github.com/llvm/llvm-project/pull/99718

- RISC-V ISA extension `$x<ISA>` (unimplemented yet) would raise question where the relocatable files are more heterogeneous.

- Mach-O LC_DATA_IN_CODE describes ranges.

- Compressed .strtab and .symtab might be beneficial.

MaskRay··on Integrated assembler improvements in LLVM 19
Big thanks for the recent performance changes! The "many small inefficiencies" point resonates – it definitely shows how performance is hurt in many small areas.

(I aim to write blog posts every 2-3 weeks, but this latest one was postponed... I wrote this in relatively short time so that the gap would not be too long, and I really should take time to refine the post.)

MaskRay··on Integrated assembler improvements in LLVM 19
Thanks!
MaskRay··on Integrated assembler improvements in LLVM 19
https://mirrors.edge.kernel.org/pub/tools/llvm/ provides a PGO-optimized LLVM toolchain. It is likely much faster than Distro provided Clang.

You might also want to replace the malloc with mimalloc/snmalloc, which might yield ~10% performance boost.

MaskRay··on Integrated assembler improvements in LLVM 19
Thx. Fixed
MaskRay··on Why curl closes PRs on GitHub
For people who prefer rebase and force push, https://getcord.github.io/spr/ is a great tool resembling Phabricator's `arc diff`.

https://github.com/orgs/community/discussions/3478 ("Improve workflow when force-pushing during code reviews") could use more support.

I've also got lots of complaints in this section when LLVM switched to GitHub PR: https://maskray.me/blog/2023-09-09-reflections-on-llvm-switc...

MaskRay··on Evolution of the ELF object file format
https://www.sco.com/developers/gabi/latest/revision.html specifies

> Second draft published May 3, 1999. > ... > New dynamic section tags DT_RUNPATH and DT_FLAGS added. Dynamic section tag DT_RPATH moved to level 2.

In glibc, DT_RPATH and DT_RUNPATH have different semantics regarding precedence related to LD_LIBRARY_PATH. In FreeBSD rtld and musl, DT_RPATH and DT_RUNPATH are equivallent.

Page 1 of 2Next →