I’ve rewritten this in SSA form:
phi, which takes as input all
of its possible dominating nodes and explicitly graphs to data flow of
functions:entry:
%x = call double @random()
%cond = fcmp olt double %x, 5.000000e-01
br i1 %cond, label %then, label %else
then:
%heads.ptr = getelementptr inbounds [6 x i8], ptr @.heads, i64 0, i64 0
br label %merge
else:
%tails.ptr = getelementptr inbounds [6 x i8], ptr @.tails, i64 0, i64 0
br label %merge
merge:
%result = phi ptr [ %heads.ptr, %then ], [ %tails.ptr, %else ]
call void @print_string(ptr %result)
ret voidThat gets us this asm:
Let’s take this example:
The LLVM IR emitted is the same:
entry:
%x = call double @random()
%cond = fcmp olt double %x, 5.000000e-01
br i1 %cond, label %then, label %else
then:
%heads.ptr = getelementptr inbounds [6 x i8], ptr @.heads, i64 0, i64 0
br label %merge
else:
%tails.ptr = getelementptr inbounds [6 x i8], ptr @.tails, i64 0, i64 0
br label %merge
merge:
%result = phi ptr [ %heads.ptr, %then ], [ %tails.ptr, %else ]
call void @print_string(ptr %result)But let’s see how we’d delete it:
entry:
%x = call double @random() # we can label %x as [0.0, 1.0]
%cond = fcmp ole double %x, 1.0 # thus this is trueentry:
%x = call double @random() # we can label %x as [0.0, 1.0]
%cond = true # transform to true
br i1 %cond, label %then, label %else # now else is deadSo we can delete the else and set %result equal to %heads and emit this program:
#include <stdio.h>
void flip_assume(double x) {
__builtin_assume(x >= 0.0 && x <= 1.0); // prove x has to be [0, 1]
const char *result;
if (x <= 1.0) {
result = "heads";
} else {
result = "tails";
}
puts(result);
}Emits this code:
Hoisting:
Switching to a lookup table:
opt).llc).
opt and llc in optimizations:pub fn base128_length(n: u128) -> usize {
let bits = if n != 0 { 128 - n.leading_zeros() } else { 1 };
let bytes = bits.div_ceil(7);
bytes as usize
}define noundef range(i64 0, 6) i64 @src(i32 noundef %n) unnamed_addr #0 {
start:
%0 = or i32 %n, 1
%self.i = zext i32 %0 to i128
; calls llvm's count leading zeroes intrinsic
; with the information that the result fits in the range [0, 128]
%1 = tail call range(i128 0, 128) i128 @llvm.ctlz.i128(i128 %self.i, i1 true)
; now truncate the result to an i8 from a i128, there is no
; signed or unsigned wrapping
%2 = trunc nuw nsw i128 %1 to i8
; sub cannot unsigned wrap
%bits.i = sub nuw i8 -128, %2
; div here
%d1.i = udiv i8 %bits.i, 7
; zero extend the i8 to i64
%d.zext.i = zext nneg i8 %d1.i to i64
; call remainder
%r2.i = urem i8 %bits.i, 7
%_8.not.i = icmp ne i8 %r2.i, 0
%3 = zext i1 %_8.not.i to i64
%bytes.sroa.0.0.i = add nuw nsw i64 %3, %d.zext.i
ret i64 %bytes.sroa.0.0.i
}Range analysis involves passing the lower and upper bound of integer variables during program execution. This is useful for code optimization.
This emits something like this:
define i32 @g(i32 %x) {
entry:
%r = urem i32 %x, 7
%cmp = icmp ult i32 %r, 7
br i1 %cmp, label %then, label %else
then:
br label %merge
else:
br label %merge
merge:
%ret = phi i32 [ 0, %then ], [ 1, %else ]
ret i32 %ret
}Range analysis helps simplify to this:
define i32 @g(i32 %x) {
entry:
br i1 true, label %then, label %else
then:
br label %merge
else:
br label %merge
merge:
ret i32 0
}SimplifyCFG will delete all the code and turn it to this:
alive) to prove that algebraic
transformations are valid.Take this program:
%cmp = icmp slt i32 %a, %b
br i1 %cmp, label %then, label %else
then:
br label %merge
else:
br label %merge
merge:
%r = phi i32 [ %a, %then ], [ %b, %else ]
ret i32 %rdefine dso_local noundef i32 @min_branch(i32 noundef %0, i32 noundef %1) local_unnamed_addr {
%3 = tail call i32 @llvm.smin.i32(i32 %0, i32 %1)
ret i32 %3
}And fold to a cmov:
Backend passes tend to be target dependent:
For example this LLVM IR:
can be optimized to a scaled mov in x86_64:
Take this print function:
void print_array(const int *restrict a, size_t n) {
for (size_t i = 0; i < n; ++i)
printf("%d\n", a[i]);
}In pseudocode, this is:
copy the ptr var
for all the items in the array:
var += i * 4
print(a[var])
Note that this multiplication is expensive and this is the same thing, replace the pointer fiddling with an addition instead:
copy the ptr var
for all the items in the array:
var += 4
print(arr[var])
Or even:
copy the ptr var
for all the items in the array:
print(arr[var *= 4])
Which is expressed in the code:
C++26 constexpr structured bindings generate bad code
#include <array>
#include <cstdint>
using u8 = std::uint8_t;
template<u8 N>
struct Range
{
template<std::size_t I>
consteval friend u8 get(Range) noexcept { return I; }
};
namespace std
{
template<u8 N>
struct tuple_size<Range<N>> {static constexpr std::size_t value = N;};
template<std::size_t I, u8 N>
struct tuple_element<I, Range<N>> {using type = u8;};
}
template<u8 x>constexpr u8 constant{x};
template<std::size_t L, std::size_t R>
__attribute__((always_inline)) inline constexpr std::array<u8, L+R> concat(const std::array<u8, L>& l, const std::array<u8, R>& r)
{
static constexpr auto [...I] = Range<L>{};
static constexpr auto [...J] = Range<R>{};
return { l[I]..., r[J]... };
}
auto test(const std::array<u8, 16>& l, const std::array<u8, 16>& r)
{
return concat(l, r);
}test(std::array<unsigned char, 16ul> const&, std::array<unsigned char, 16ul> const&):
movzx ecx, byte ptr [rip + reference temporary for std::array<unsigned char, 16ul + 16ul> concat<16ul, 16ul>(std::array<unsigned char, 16ul> const&, std::array<unsigned char, 16ul> const&)::I]
mov rax, rdi
movzx ecx, byte ptr [rsi + rcx]
mov byte ptr [rdi], cl
movzx ecx, byte ptr [rip + reference temporary for std::array<unsigned char, 16ul + 16ul> concat<16ul, 16ul>(std::array<unsigned char, 16ul> const&, std::array<unsigned char, 16ul> const&)::I (.2)]
movzx ecx, byte ptr [rsi + rcx]
mov byte ptr [rdi + 1], cl
movzx ecx, byte ptr [rip + reference temporary for std::array<unsigned char, 16ul + 16ul> concat<16ul, 16ul>(std::array<unsigned char, 16ul> const&, std::array<unsigned char, 16ul> const&)::I (.4)]
movzx ecx, byte ptr [rsi + rcx]
mov byte ptr [rdi + 2], cl
... (30 more times)
reference temporary for std::array<unsigned char, 16ul + 16ul> concat<16ul, 16ul>(std::array<unsigned char, 16ul> const&, std::array<unsigned char, 16ul> const&)::J (.52):
.byte 0
reference temporary for std::array<unsigned char, 16ul + 16ul> concat<16ul, 16ul>(std::array<unsigned char, 16ul> const&, std::array<unsigned char, 16ul> const&)::J (.54):
.byte 1
reference temporary for std::array<unsigned char, 16ul + 16ul> concat<16ul, 16ul>(std::array<unsigned char, 16ul> const&, std::array<unsigned char, 16ul> const&)::J (.56):
.byte 2
... (30 more times)When the correct codegen is this:
Strength Reduce lock xadd to lock sub in Instcombine
bool fn_add(std::atomic<uint64_t> &a, uint64_t n) noexcept {
return a.fetch_add(-n, std::memory_order_relaxed) == n;
}Lowered to this:
But the ideal is this, which is far better:
Remove two unnecessary movs when %rdx is an input to mulx
void mul64_u128(uint64_t *hi, uint64_t *lo, uint64_t a, uint64_t b) {
unsigned __int128 r = (unsigned __int128)a * b;
*lo = (uint64_t)r;
*hi = (uint64_t)(r >> 64);
}Generates this:
mul64_u128:
movq %rdx, %rax
movq %rcx, %rdx
mulxq %rax, %rcx, %rax
movq %rcx, (%rsi)
movq %rax, (%rdi)
retqWhen this is the correct codegen (2 less movs):
Strength Reduce Constant Division
Emits this currently (note there’s no division here)
udiv_16_7: # @udiv_16_7
movzwl %di, %eax
imull $9363, %eax, %ecx
shrl $16, %ecx
subl %ecx, %edi
movzwl %di, %eax
shrl %eax
addl %ecx, %eax
shrl $2, %eax
retqThe ideal is this:
You might be expecting something like this (this is how a non-optimizing compiler would compile this code)
udiv_16_7:
push %rbp
mov %rsp, %rbp
sub $16, %rsp
mov %rsp, -8(%rbp)
mov %di, -10(%rbp)
mov $7, %rax
push %rax
lea -10(%rbp), %rax
movzwl (%rax), %eax
pop %rdi
cdq
idiv %edi
jmp .L.return.udiv_7
.L.return.udiv_7:
mov %rbp, %rsp
pop %rbp
retThe reason why is division is extremely expensive. On my computer, this non-optimized version is about 5x slower than the ideal version. The ideal is about 2x as fast as the current clang version.
This is the division by constant strength reduction: For an unsigned division where you don’t know \(X\) but you know \(C\): We compute a magic reciprocal \(m\) and then compute a quotient \(q\):
We want to replace:
\[q = \left\lfloor \frac{x}{d} \right\rfloor\]
With:
\[q \approx \left\lfloor \frac{x \cdot m}{2^k} \right\rfloor\]
We choose \(m\) with the formula:
\[\frac{2^k}{d}\]
So that:
\[\frac{x}{d} \approx \frac{x \cdot m}{2^k}\]
If this equation is approximate, not exact, then a fixup phase is done. If the equation is exact, then we can replace the instruction sequence with this formula, one multiplication and one right shift.
The reason why clang currently emits a fixup sequence is because the magic, \(m\) does not have enough precision to precisely express the quotient in a 32-bit space. However, on most 32-bit and 64-bit architectures, 64-bit multiply is not that bad.
We can thus change the formula a bit:
We still calculate:
\[q \approx \left\lfloor \frac{x \cdot m}{2^k} \right\rfloor\]
We choose \(m\) with the formula:
\[\frac{2^k}{d}\]
However we calculate three formulas to prove that
\[\frac{x}{d} = \frac{x \cdot m}{2^k}\]
Error Check:
\[Error = Magic \cdot C - 2^{Shift}\]
No overflow (fits in the space we alloted):
\[MaxX \cdot Magic < 2^{DestWidth}\]
And exactness (the equality is true):
\[MaxX \cdot Error < 2^{Shift}\]
Let’s do another optimization:
How would you implement ceiling division (instead of floor division)
If there is no remainder, e.g. \(\mathrm{div\_ceil}(21, 7) = 3\) ceiling division acts like normal division.
If there is a remainder, you add one to the quotient: \(\mathrm{div\_ceil}(22, 7) = 4\)
How do we translate this to code?
There’s a formula to compute ceiling division with only one division (instead of two, which we stated before):
The general formula is:
\[ \mathrm{div\_ceil}(X, Y) = \left\lfloor \frac{X}{Y} \right\rfloor + \begin{cases} 1 & \text{if } X \bmod Y > 0 \\ 0 & \text{otherwise} \end{cases} \]
However we can exploit the fact that we have cheap floor divide. Instead of doing a remainder calculation, we can fold the remainder into the division.
\[ \mathrm{div\_ceil}(X, Y) = \left\lceil \frac{X}{Y} \right\rceil = \left\lfloor \frac{X + Y - 1}{Y} \right\rfloor \] \[ \qquad \text{for } Y > 0 \]
This actually fails in alive:
Thankfully alive tells us the reasoning (we have made a variable have undefined behavior).
The counter case is that if x is 250, we add 6 to it and we overflow an i8 (0-255).
So we need to find a way to either extend (zero-extend) or constrain \(X + Y - 1 < 255\)
The proof works and our reward is better code:
range handling doesn’t propagate range information through udiv, sdiv, urem, srem, mul, add, sub, trunc.
This change propagates information recursively for integers that are created that use these operations.
In this example, the icmp is deleted because %x + 4 < -6
turns into:
This changes the following (poison/undefined behavior)
%a = add nsw <2 x i8> %o, <i8 -1, i8 -1>
%b = zext <2 x i8> %a to <2 x i32>
%c = add nuw nsw <2 x i32> %b, <i32 1, i32 poison>And just deletes it:
Popcount detection:
Clang currently emits:
This code isn’t properly optimized to the best:
std::popcount, C has had
__builtin_popcount for 2+ decades, rust has
.count_ones(). These all lower automatically to
popcount without llvm needing to handle detecting infinite
programs.int f(int a, int b, int c) {
int x = a + b;
if (c > 0) {
return (a + b) * x;
}
return x - (a + b);
}