[RFC] Byte Type for LLVM IR
Authors: Juneyoung Lee (@aqjune), Pedro Lobo (@pedroclobo), Nuno Lopes (@nlopes), George Mitenkov (@george)
Summary
We propose adding a raw byte type (b<N>) to LLVM IR to correctly represent raw memory data in registers. This change:
- Fixes known correctness bugs in memcpy lowering and load merging/widening
- Enables the eventual removal of undef from LLVM IR
- Has minimal performance impact: 0.2% average slowdown with optimizations across 20 benchmarks
- Requires modest implementation effort: protoype has ~2.6k lines of code across LLVM & Clang
- Is backwards compatible
We also propose clarifying the semantics of load to allow type punning, which reduces overhead to near-zero.
Motivation
LLVM IR currently lacks a way to represent raw memory values in registers without prematurely interpreting them as integers or pointers. Today, such values are represented using integer types (i8, i16, i64, âŚ), even when they originate from bytewise memory operations such as memcpy.
This conflation causes both correctness issues and semantic ambiguity:
- Integer optimizations may be applied to values that are merely copies of memory.
- Type punning through loads exists in practice but is underspecified.
The byte type addresses this by making raw-memory values explicit in the IR, while leaving integer semantics and optimizations unchanged.
Concrete Problems This Solves
1. Unsound memcpy Lowering
LLVM currently lowers small memcpy calls into integer load/store pairs. This is incorrect when copying pointers:
; Current (incorrect) lowering
call void @llvm.memcpy(ptr %dst, ptr %src, i64 8, i1 false)
=>
%v = load i64, ptr %src ; loses pointer information!
store i64 %v, ptr %dst
This transformation loses pointer information, which breaks alias analysis and causes miscompilations (see bug 37469).
With the byte type, memcpy can be correctly lowered:
%v = load b64, ptr %src ; preserves all data correctly
store b64 %v, ptr %dst
This represents a pure memory copy with no implicit reinterpretation.
2. Unsound Load Merging/Widening
GVN and other optimizers merge loads but can spread poison incorrectly:
; Two 1-byte loads, one may be poison
%a = load i8, ptr %p
%b = load i8, ptr %q
; Current (incorrect) merging
%v = load i16, ptr %p
%a = trunc i16 %v to i8
%s = lshr i16 %v, 8
%b = trunc i16 %s to i8
Here, poison in one byte can incorrectly taint both results.
With the byte type:
%v = load b16, ptr %p
%c = trunc b16 %v to b8
%a = bytecast b8 %c to i8 ; poison only if %c is poison
%s = lshr b16 %v, 8
%d = trunc b16 %s to b8
%b = bytecast b8 %d to i8 ; poison only if %d is poison
Poison is tracked per byte, restoring correctness.
3. Enables Removing undef
The byte type is the final piece needed to eliminate undef values from LLVM IR. Currently, ~18% of Alive2-detected miscompilations occur solely because undef exists. Removing it would:
- Simplify LLVM IR semantics
- Make algebraic identities hold (e.g.,
x + xâĄ2 * x) - Reduce compiler bugs from misunderstanding undef
- Maintain backward compatibility via IR auto-upgrade
Proposal
1. Add Byte Type b<N>
A new IR type representing raw memory data, where each bit can be:
- An integer bit (0 or 1)
- Part of a pointer value
- Poison
Example: b8, b16, b32, b64
The byte type is:
- Allowed in alloca, load, and store
- Distinct from integer types for optimization purposes
2. New bytecast Instruction
Converts between bytes and other types:
; Exact variant: returns poison if type doesn't match exactly
%i = bytecast exact b8 %byte to i8
; Type-punning variant: forces conversion
%i = bytecast b8 %byte to i8 ; allows reading pointer bytes as int
%p = bytecast b64 %byte to ptr ; allows reading int bytes as ptr
The type-punning variant handles mixed-type data:
- Pointer bytes â integer: behaves like like
ptrtoaddr: it extracts the address value without escaping the pointer - Integer bytes â pointer: produces a pointer without provenance (cannot be dereferenced, but can be compared or used in
getelementptr) - When bits are consistent with the target type, the conversion is a no-op
3. Extend Existing Instructions
The following are extended to support byte types:
bitcast ty %v to b<N>: Convert any type to bytetrunc b<N> %v to b<M>: Truncate byte valueslshr b<N> %v, amt: Shift byte valuesfreeze b<N> %v: Freeze on per-bit basis
4. Clarify load Semantics (Important!)
LangRef does not define the semantics of a type punning load (i.e., load a region of memory with a type different than what it was stored).
We propose that regular load allows type punning, making load ty, ptr %p equivalent to:
%b = load b64, ptr %p
%v = bytecast b64 %b to ty ; type-punning bytecast
This is crucial for performance:
- Existing code continues to work unchanged
- No bytecast proliferation in normal code
- Byte types appear only where needed (char variables, memcpy lowering)
- Average overhead drops from ~0.8% to ~0.2% when comparing a version of LLVM without and with load type punning.
Implementation Experience
We have a working prototype:
- LLVM: 1.3k LoC
- Clang: 1.2k LoC (mostly lowering
chartob8and introducing bytecasts) - Alive2: Extended to support byte type for validation
Changes to LLVM
Core Lowerings
- memcpy/memmove: Lowered to byte load/store pairs
- Load merging, widening, and forwarding updated for byte semantics
- Clang:
charandstd::bytevariables âb8instead ofi8
Key Optimizations Implemented
- Bytecast constant folding
%v = bytecast <2 x b8> <b8 1, b8 2> to <2 x i8>
=> %v = <2 x i8> <i8 1, i8 2>
- Redundant cast elimination
%b = bitcast i32 %i to b32
%c = bytecast b32 %b to i32
=> %c = %i
- Store forwarding
store b32 %x, ptr %p
%v = load i32, ptr %p
=> %v = bytecast b32 %x to i32
- SLP vectorization
%b0 = load b8, ptr %p0
%b1 = load b8, ptr %p1
%x0 = bytecast b8 %b0 to i8
%x1 = bytecast b8 %b1 to i8
...
=> %l = load <2 x b8>, ptr %p0
%r = bytecast <2 x b8> %l to <2 x i8>
- Load combining
%b = load b8, ptr %p
%c = bytecast exact b8 %b to i8
=> %c = load i8, ptr %p
Performance Evaluation
Evaluated on 20 benchmarks (~6M LoC total, including LLVM itself):
| Configuration | Avg Slowdown | Max Slowdown | Binary Size | Compile Time |
|---|---|---|---|---|
| Byte type | 0.8% | 4.4% | +0.2% | +0.15% |
| Byte type + load type punning | 0.2% | 4.4% | +0.1% | +0.19% |
Remaining regressions are due to incomplete cost model and pattern updates, expected to be addressed incrementally (similar to freeze).
Correctness Validation
- 11 LLVM unit tests previously flagged as unsound by Alive2 are now fixed
- No new test failures introduced
- All known memcpy-related miscompilations are resolved
- Alive2 validation on Draco, eSpeak, FLAC, tjbench shows only bug fixes
Comparison to Alternatives
Why Not Use Metadata/Attributes?
Metadata would:
- Be difficult to preserve across transformations
- Complicate every pass
- Fail to represent raw values in SSA
- Prevent correct store/load forwarding
The byte type makes the property explicit and enforceable.
Why Not Just Make All Integer Types Behave Like Bytes?
This would pessimize all integer operations, as many optimizations (GVN, reassociation, value forwarding) would become unsound. The byte type allows us to:
- Keep aggressive optimizations for integers
- Use bytes only where raw memory semantics are needed
- Make the distinction clear in the IR
Why Not Just Make Memory Untyped?
The issue is not memory but SSA values. We need a type that can hold raw memory data in registers while preserving:
- Pointer information (for alias analysis)
- Per-bit poison propagation
- Compatibility with optimizations
Open Questions
- Load semantics: The performance data suggests making regular loads support type punning. Rust folks are supportive of this semantics. This has never been defined in LangRef; should we move on with this semantics?
- Bitwidth restrictions: Our prototype limits bitwidths to multiples of 8. Is this acceptable, or do we need arbitrary bitwidths?
- Bytecast variants: Should we support both exact and type-punning variants, or just one?
Proposed Timeline
We propose the following timeline:
- After LLVM 22 branches: Commit the implementation to
main - 6-month testing period: Allow broad community testing during LLVM 23 development
- Address feedback: Fix any issues discovered during testing
- LLVM 23 release: Ship with byte type fully integrated
This maximizes real-world testing while minimizing release risk.
Patches
They are available here:
- LLVM: GitHub - pedroclobo/llvm-project at byte-llvm
- Clang: GitHub - pedroclobo/llvm-project at byte-clang
- Clang Tests: GitHub - pedroclobo/llvm-project at byte-clang-tests
Acknowledgements
Thank you Nikita Popov & Ralf Jung for feedback on earlier drafts of this RFC.
Conclusion
The byte type addresses real correctness bugs in LLVM today, enables the long-term removal of undef, and does so with minimal performance and implementation cost. The prototype demonstrates feasibility, and the proposed timeline allows ample testing.
We look forward to feedback and discussion!