I've created pr-shadow with vibe coding, a tool that maintains a shadow branch for GitHub pull requests (PR) that never requires force-pushing. This addresses pain points I described in Reflections on LLVM's switch to GitHub pull requests#Patch evolution.
2025年總結
TODO
一如既往,主要在工具鏈領域耕耘。但由于工作忙碌在open source社区投入的时间减少了。
Blogging
不包括这篇总结,一共写了18篇文章。
- Understanding and improving Clang -ftime-report
- Natural loops
- lld 20 ELF changes
- Migrating comments to giscus
- Compiling C++ with the Clang API
- Relocation generation in assemblers
- LLVM integrated assembler: Improving MCExpr and MCValue
- LLVM integrated assembler: Improving expressions and relocations
- GCC 13.3.0 miscompiles LLVM
- LLVM integrated assembler: Engineering better fragments
- LLVM integrated assembler: Improving sections and symbols
- Understanding alignment - from source to object file
- Benchmarking compression programs
- lld 21 ELF changes
- Remarks on SFrame
- Stack walking: space and time trade-offs
- Sacramento游记
- Weak AVL Tree
llvm-project
翻新了integrated assembler,写了4篇相关的blog posts: https://maskray.me/blog/tags/assembler/
Reviewed numerous patches. query
is:pr created:>2025-01-01 reviewed-by:MaskRay=> "989 Closed"
Linux kernel
贡献了两个commits,被引用了一次。
ccls
clang.prependArgs- 支持了LLVM 21和22
ELF specification
尝试推进compact
section header table,没有取得共识。 一些成员希望采用general
compression (like
zstd)的方式,像SHF_COMPRESSED那样压缩section header
table。包括我在内的另一些人不喜欢采用general compression。
Misc
Reported 6 feature requests or bugs to binutils.
ld --build-id does not use symtab/strtab contentgas: monolithic .sframe violates COMDAT group rulegas: Clarify whitespace between a label's symbol and its colonld: Add --print-gc-sections=fileld riscv: Relocatable linking challenge with R_RISCV_ALIGNld: add --why-live
旅行
- 第一次去:台南、西安、兰州、天水、Sacramento、Puerto Vallarta, Jalisco, Mexico、Mazatlán, Sinaloa, Mexico
- 曾经去过:台北(上一次是近11年前)、北京
Weak AVL Tree
tl;dr: Weak AVL trees are replacements for AVL trees and red-black trees.
The 2014 paper Rank-Balanced Trees (Haeupler, Sen, Tarjan) presents a framework using ranks and rank differences to define binary search trees.
- Each node has a non-negative integer rank
r(x). Null nodes have rank -1. - The rank difference of a node
xwith parentp(x)isr(p(x)) − r(x). - A node is
i,jif its children have rank differencesiandj(unordered), e.g., a 1,2 node has children with rank differences 1 and 2. - A node is called 1-node if its rank difference is 1.
Several balanced trees fit this framework:
- AVL tree: Ranks are defined as heights. Every node is 1,1 or 1,2 (rank differences of children)
- Red-Black tree: All rank differences are 0 or 1, and no parent of a 0-child is a 0-child. (red: 0-child; black: 1-child; null nodes are black)
- Weak AVL tree (new tree described by this paper): All rank
differences are 1 or 2, and every leaf has rank 0.
- A weak AVL tree without 2,2 nodes is an AVL tree.
1 | AVL trees ⫋ weak AVL trees ⫋ red-black trees |
Weak AVL Tree
Weak AVL trees are replacements for AVL trees and red-black trees. A single insertion or deletion operation requires at most two rotations (forming a double rotation when two are needed). In contrast, AVL deletion requires O(log n) rotations, and red-black deletion requires up to three.
Without deletions, a weak AVL tree is exactly an AVL tree. With deletions, its height remains at most that of an AVL tree with the same number of insertions but no deletions.
The rank rules imply:
- Null nodes have rank -1, leaves have rank 0, unary nodes have rank 1.
Insertion
The new node x has a rank of 0, changed from the null
node of rank -1. There are three cases.
- If the tree was previously empty, the new node becomes the root.
- If the parent of the new node was previously a unary node (1,2 node), it is now a 1,1 binary node.
- If the parent of the new node was previously a leaf (1,1 node), it is now a 0,1 binary node, leading to a rank violation.
When the tree was previously non-empty, x has a parent
node. We call the following subroutine with x indicating
the new node to handle the second and third cases.
The following subroutine handles the rank increase of x.
We call break if there is no more rank violation, i.e. we
are done.
The 2014 paper isn't very clear about the conditions.
1 | // Assume that x's rank has just increased by 1 and rank_diff(x) has been updated. |
Insertion never introduces a 2,2 node, so insertion-only sequences produce AVL trees.
Deletion
TODO: Describe deletion
1 | if (!was_2 && !x && !p->ch[0] && !p->ch[1] && p->rp()) { |
Implementation
Since valid rank differences can only be 1 or 2, ranks can be encoded efficiently using bit flags. There are three approaches:
- Store two bits representing the rank differences to each child. Bit 0: rank difference to left child (1 = diff is 2, 0 = diff is 1). Bit 1: rank difference to right child
- Store a single bit representing the parity (even/odd) of the node's absolute rank. The rank difference to a child is computed by comparing parities. Same parity → rank difference of 2. Different parity → rank difference of 1
- Store a 1-bit rank difference parity in each node.
FreeBSD's sys/tree.h (https://reviews.freebsd.org/D25480, 2020) uses the first
approach. The rb_ prefix remains as it can also indicate
Rank-Balanced:) Note: its insertion operation can be futher
optimized as the following code demonstrates.
https://github.com/pvachon/wavl_tree and https://crates.io/crates/wavltree use the second approach.
The third approach is less efficient because a null node can be
either a 1-child (parent is binary) or a 2-child (parent is unary),
requiring the sibling node to be probed to determine the rank
difference:
int rank_diff(Node *p, int d) { return p->ch[d] ? p->ch[d]->par_and_flg & 1 : p->ch[!d] ? 2 : 1; }
https://maskray.me/blog/2025-12-14-weak-avl-tree is a C++ implementation covering both approaches, supporting the following operations:
insert: insert a noderemove: remove a noderank: count elements less than a keyselect: find the k-th smallest element (0-indexed)prev: find the largest element less than a keynext: find the smallest element greater than a key
Node structure:
ch[2]: left and right child pointers.par_and_flg: packs the parent pointer with 2 flag bits in the low bits. Bit 0 indicates whether the left child has rank difference 2; bit 1 indicates whether the right child has rank difference 2. A cleared bit means rank difference 1.i: the key value.sum,size: augmented data maintained bymconcatfor order statistics operations.
Helper methods:
rd2(d): returns true if childdhas rank difference 2.flip(d): toggles the rank difference of childdbetween 1 and 2.clr_flags(): sets both children to rank difference 1 (used after rotations to reset a node to 1,1).
Invariants:
- Leaves always have
flags() == 0, meaning both null children are 1-children (null nodes have rank -1, leaf has rank 0). - After each insertion or deletion,
mconcatis called along the path to the root to update augmented data.
Rotations:
The rotate(x, d) function rotates node x in
direction d. It lifts x->ch[d] to replace
x, and updates the augmented data for x. The
caller is responsible for updating rank differences.
Misc
Visualization: https://tjkendev.github.io/bst-visualization/avl-tree/bu-weak.html
Sacramento游记
周末从旧金山湾南部去Sacramento参观。
Stack walking: space and time trade-offs
On most Linux platforms (except AArch32, which uses
.ARM.exidx), DWARF .eh_frame is required for
C++ exception
handling and stack
unwinding to restore callee-saved registers. While
.eh_frame can be used for call trace recording, it is often
criticized for its runtime overhead. As an alternative, developers can
enable frame pointers, or adopt SFrame, a newer format designed
specifically for profiling. This article examines the size overhead of
enabling non-DWARF stack walking mechanisms when building several LLVM
executables.
Runtime performance analysis will be added in a future update.
Remarks on SFrame
SFrame is a new stack
walking format for userspace profiling, inspired by Linux's
in-kernel ORC unwind
format. While SFrame eliminates some .eh_frame CIE/FDE
overhead, it sacrifices functionality (e.g., personality, LSDA,
callee-saved registers) and flexibility, and its stack offsets are less
compact than .eh_frame's bytecode-style CFI instructions.
In llvm-project executables I've tested on x86-64, .sframe
section is 20% larger than .eh_frame. It also remains
significantly larger than highly compact schemes like Windows
ARM64 unwind codes.
SFrame describes three elements for each function:
- Canonical Frame Address (CFA): The base address for stack frame calculations
- Return address
- Frame pointer
An .sframe section follows a straightforward layout:
- Header: Contains metadata and offset information
- Auxiliary header (optional): Reserved for future extensions
- Function Descriptor Entries (FDEs): Array describing each function
- Frame Row Entries (FREs): Arrays of unwinding information per function
lld 21 ELF changes
LLVM 21.1 have been released. As usual, I maintain lld/ELF and have added some notes to https://github.com/llvm/llvm-project/blob/release/21.x/lld/docs/ReleaseNotes.rst. I've meticulously reviewed nearly all the patches that are not authored by me. I'll delve into some of the key changes.
Benchmarking compression programs
tl;dr https://gist.github.com/MaskRay/74cdaa83c1f44ee105fcebcdff0ba9a7 is a single-file Ruby program that downloads and compiles multiple compression utilities, then benchmarks their compression and decompression performance on a specified input file, finally generates a HTML file with scatter charts. Scroll to the end to view example HTML pages.
Compression algorithms can be broadly categorized into three groups based on their typical compression ratio and decompression speed:
- Low ratio, high speed: lz4, snappy, Oodle Selkie.
- Medium ratio, medium speed: zlib, zstd, brotli, Oodle Kraken.
- High ratio, low speed: LZMA, bzip2, bzip3, bsc, zpaq, kanzi, Oodle Leviathan.
Low ratio Codecs in this category prioritize speed above all else. The compression and compression speeds are comparable. They are designed to decompress so quickly that they don't introduce a noticeable delay when reading data from storage like solid-state drives. These codecs typically producing byte-aligned output and often skip the final step of entropy encoding, which, while crucial for high compression, is computationally intensive. They are excellent choices for applications where latency is critical, such as kernel features like zswap.
Medium ratio This is the sweet spot for many tasks. The codecs achieve better compression ratio by employing entropy encoding, usually Huffman coding.
zstd has emerged as a clear leader, gaining popularity and effectively supplanting older codecs like the venerable DEFLATE (zlib).
High ratio They are designed to squeeze every last bit of redundancy out of the data, often at the cost of significantly longer compression and decompression times, and large memory usage. They are perfect for archival purposes or data distribution where the files are compressed once and decompressed infrequently. Codecs typically have 3 important components:
- Transforms: Codecs typically implement strong transforms to increase redundancy, even very specific ones like branch/call/jump filters for machine code.
- Predication model: This model anticipates the next piece of data based on what has already been processed.
- Entropy encoding: Traditional codecs use arithmetic encoder, which is replaced by the more efficient Range variant of Asymmetric Numeral Systems (rANS).
Some projects apply neural network models, such as Recurrent Neural Network, Long Short-Term Memory, and Transformer, to the predication model. They are usually very slow.
This categorization is loose. Many modern programs offer a wide range of compression levels that allow them to essentially span multiple categories. For example, a high-level zstd compression can achieve a ratio comparable to xz (a high-compression codec) by using more RAM and CPU. While zstd's compression speed or ratio is generally lower, its decompression speed is often much faster than that of xz.
Benchmarking
I want to benchmark the single worker performance of a few compression programs:
- lz4: Focuses on speed over compression ratio. Memory usage is extremely low. It seems Pareto superior to Google's Snappy.
- zstd: Gained significant traction and obsoleted many
existing codecs. Its LZ77 variant uses three recent match offsets like
LZX. For entropy encoding, it employs Huffman coding for literals and
2-way interleaved Finite State Entropy for Huffman weights, literal
lengths, match lengths, and offset codes. The large alphabet of literals
makes Huffman a good choice, as compressing them with FSE provides
little gain for a speed cost. However, other symbols have a small range,
making them a sweet spot for FSE. zstd works on multiple streams at the
same time to utilize instruction-level parallelism. zstd is supported by
the
Accept-Encoding: zstdHTTP header. Decompression memory usage is very low. - brotli: Uses a combination of LZ77, 2nd order context
model, Huffman coding, and static dictionary. The decompression speed is
similar to gzip with a higher ratio. At lower levels, its performance is
overshadowed by zstd. Compared with DEFLATE, it employs a
larger sliding window (from 16KiB-16B to 16MiB-16B) and a smaller
minimum match length (2 instead of 3). It has a predefined dictionary
that works well for web content (but feels less elegant) and supports
120 transforms. brotli is supported by the
Accept-Encoding: brHTTP header. Decompression memory usage is quite low. - bzip3: Combines BWT, RLE, and LZP and uses arithmetic encoder. Memory usage is large.
- xz: LZMA2 with a few filters. The filters must be enabled explicitly.
- lzham: Provides a compression ratio similar to LZMA but with faster
decompression. Compression is slightly slower while memory usage is
larger. The build system is not well-polished for Linux. I have forked
it, fixed
stdint.hbuild errors, and installedlzhamtest. The command line programlzhamtestshould really be renamed tolzham. - zpaq: Functions as a command-line archiver supporting multiple files. It combines context mixing with arithmetic encoder but operates very slowly.
- 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. Levels 8 and 9 belong to the PAQ8 family and consume substantial memory.
I'd like to test lzham (not updated for a few years), but I'm having
trouble getting it to compile due to a cstdio header
issue.
Many modern compressors are parallel by default. I have to disable
this behavior by using options like -T1. Still,
zstd uses a worker thread for I/O overlap, but I don't bother
with --single-thread.
To ensure fairness, each program is built with consistent compiler
optimizations, such as -O3 -march=native.
Below is a Ruby program that downloads and compiles multiple compression utilities, compresses then decompress a specified input file. It collects performance metrics including execution time, memory usage, and compression ratio, and finally generates an HTML file with scatter charts visualizing the results. The program has several notable features:
- Adding new compressors is easy: just modify
COMPRESSORS. - Benchmark results are cached in files named
cache_$basename_$digest.json, allowing reuse of previous runs for the same input file. - Adding a new compression level does not invalidate existing benchmark results for other levels.
- The script generates an HTML file with interactive scatter charts.
Each compressor is assigned a unique, deterministic color based on a
hash of its name (using the
hslfunction in CSS).
The single file Ruby program is available at https://gist.github.com/MaskRay/74cdaa83c1f44ee105fcebcdff0ba9a7
Limitation
A single run might not be representative.
Running the executable incurs initialization overhead, which would be amortized in a library setup. However, library setup would make updating libraries more difficult.
Demo
1 | ruby bench.rb enwik8 |
Many programs exhibit a stable decompression speed (uncompressed size / decompression time). There is typically a slightly higher decompression speed at higher compression levels. If you think of the compressed content as a form of "byte code", a more highly compressed file means there are fewer bytes for the decompression algorithm to process, resulting in faster decompression. Some programs, like zpaq and kanzi, use different algorithms that can result in significantly different decompression speeds.
xz -9 doesn't use parallelism on the two files under
~100 MiB because their uncompressed size is smaller than the default
block size for level 9.
From
install/include/lzma/container.hFor each thread, about 3 * block_size bytes of memory will be allocated. This may change in later liblzma versions. If so, the memory usage will probably be reduced, not increased.
Understanding alignment - from source to object file
Updated in 2026-04.
Alignment refers to the practice of placing data or code at memory addresses that are multiples of a specific value, typically a power of 2. This is typically done to meet the requirements of the programming language, ABI, or the underlying hardware. Misaligned memory accesses might be expensive or will cause traps on certain architectures.
This blog post explores how alignment is represented and managed as C++ code is transformed through the compilation pipeline: from source code to LLVM IR, assembly, and finally the object file. We'll focus on alignment for both variables and functions.
LLVM integrated assembler: Improving sections and symbols
In my previous post, LLVM integrated assembler: Improving expressions and relocations delved into enhancements made to LLVM's expression resolving and relocation generation. This post covers recent refinements to MC, focusing on sections and symbols.