HN user

MaskRay

239 karma
Posts21
Comments45
View on HN
maskray.me 5mo ago

Long branches in compilers, assemblers, and linkers

MaskRay
9pts0
maskray.me 9mo ago

Remarks on SFrame

MaskRay
3pts0
discourse.llvm.org 10mo ago

LLVM's AI policy vs. code of confuct vs. reality

MaskRay
2pts0
maskray.me 11mo ago

Understanding alignment – from source to object file

MaskRay
5pts0
maskray.me 11mo ago

LLVM integrated assembler: Engineering better fragments

MaskRay
4pts0
github.com 1y ago

Apple is adding Mach-O's riscv32 support to LLVM

MaskRay
7pts1
maskray.me 1y ago

Simplifying Disassembly with LLVM Tools

MaskRay
4pts0
maskray.me 1y ago

Tinkering with Neovim

MaskRay
2pts1
maskray.me 2y ago

Mapping Symbols: Rethinking for Efficiency

MaskRay
16pts1
github.com 2y ago

polyfill-glibc

MaskRay
1pts0
maskray.me 2y ago

Integrated assembler improvements in LLVM 19

MaskRay
104pts27
beyondloom.com 2y ago

Another Year with Decker

MaskRay
1pts1
maskray.me 2y ago

Evolution of the ELF object file format

MaskRay
148pts51
maskray.me 2y ago

Reflections on LLVM's switch to GitHub pull requests

MaskRay
6pts0
maskray.me 3y ago

Relocatable linking

MaskRay
52pts3
maskray.me 3y ago

_FORTIFY_SOURCE

MaskRay
3pts0
maskray.me 5y ago

All about thread-local storage

MaskRay
50pts10
maskray.me 5y ago

C++ Exception Handling ABI

MaskRay
1pts0
github.com 6y ago

vscode-ccls 0.1.28: new configuration system of semantic highlight

MaskRay
2pts0
github.com 6y ago

C/C++/ObjC language server ccls 0.20190823.3 released

MaskRay
3pts0
news.ycombinator.com 7y ago

ccls: C/C++ language server supporting cross references, completion and more

MaskRay
3pts2

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); }

I hate compilers 1 month ago

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

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.

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__' ```

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...

Huge Binaries 7 months ago

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.

Huge Binaries 7 months ago

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.

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.

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.
Zig 0.15.1 11 months ago

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

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.)

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 ```

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.

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')

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); }

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 ? :)

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.

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.)