Building a respectable compiler requires basic blocks in order for liveness analysis to be performant. Consider my own Apple compiler:

Compilation was dominated by mkLiveness and moreover simple functions took tens of milliseconds to compile.

benchmarking pipeline/bytes (gamma)
time                 62.52 ms   (62.14 ms .. 62.86 ms)
                     1.000 R²   (1.000 R² .. 1.000 R²)
mean                 62.54 ms   (62.29 ms .. 62.80 ms)
std dev              470.3 μs   (308.3 μs .. 657.9 μs)

benchmarking pipeline/bytes (fcdf)
time                 108.3 ms   (106.6 ms .. 110.5 ms)
                     1.000 R²   (0.999 R² .. 1.000 R²)
mean                 106.1 ms   (105.4 ms .. 107.1 ms)
std dev              1.288 ms   (696.6 μs .. 2.067 ms)

After rewriting to use basic blocks, compilation times were less than 20ms, a far better user experience. Moreover, liveness analysis no longer dominates.

benchmarking pipeline/bytes (gamma)
time                 5.313 ms   (5.171 ms .. 5.485 ms)
                     0.994 R²   (0.991 R² .. 0.997 R²)
mean                 5.166 ms   (5.087 ms .. 5.262 ms)
std dev              267.0 μs   (218.5 μs .. 321.2 μs)
variance introduced by outliers: 29% (moderately inflated)

benchmarking pipeline/bytes (fcdf)
time                 10.42 ms   (9.838 ms .. 10.96 ms)
                     0.982 R²   (0.971 R² .. 0.991 R²)
mean                 12.13 ms   (11.71 ms .. 12.55 ms)
std dev              1.090 ms   (907.0 μs .. 1.409 ms)
variance introduced by outliers: 48% (moderately inflated)

Appel mentions basic blocks as an advanced optimization but does not fill in details; the precise implementation in Haskell is instructive and elegant.

First, we define uBB, dBB to simulate uses and defs on a basic block:

uBB, dBB :: E reg => [X86 reg freg a] -> IS.IntSet
uBB = foldr (\p n -> uses p \`IS.union\` (n IS.\\\ defs p)) IS.empty
dBB = foldMap defs

Note that foldr processes instructions in reverse execution order.

Then, to get liveness information for each instruction, we apply the liveness equations (each instruction has one successor in a basic block):

\( {\tt in}[n]={\tt uses}[n]\cup ({\tt out}[n]~\backslash~{\tt defs}[n])\)

\( {\tt out}[n]={\tt in}[succ(n)]\)

Using the live-out information for the basic block as an initial value, we proceed in reverse order:

data Liveness = Liveness { ins :: !IS.IntSet, out :: !IS.IntSet, fins :: !IS.IntSet, fout :: !IS.IntSet }
    deriving (Eq)

data BB arch reg freg a b = BB { unBB :: [arch reg freg a], caBB :: b }

expand :: (E reg, E freg) => BB X86 reg freg () Liveness -> [X86 reg freg Liveness]
expand (BB asms@(\_:\_) li) = scanr (\n p -> lN n (ann p)) lS iasms
    where lN a s =
            let ai=uses a <> (ao IS.\\\ defs a)
                ao=ins s
                aif=usesF a <> (aof IS.\\\ defsF a)
                aof=fins s
            in a $> Liveness ai ao aif aof
          lS = let ao=out li
                   aof=fout li
               in asm $> Liveness (uses asm \`IS.union\` (ao \`IS.difference\` defs asm)) ao (usesF asm \`IS.union\` (aof \`IS.difference\` defsF asm)) aof
          (iasms, asm) = (init asms, last asms)
expand _ = []