Home · ASPLOS 2023 · Compilers & Programming Languages

SPLENDID: Supporting Parallel LLVM-IR Enhanced Natural Decompilation for Interactive Development

Zujun Tan, Yebin Chon, Michael Kruse, Johannes Doerfert, Ziyang Xu, Brian Homerding and 2 others

Conference
ASPLOS 2023
Published
2023
Pages
679-693
Citations
2

Writing fast parallel code by hand is hard, and automatic parallelizing compilers only get you part way because they lack facts only the programmer knows — so the best results come from the two working together, but a programmer cannot build on the compiler's work if it is buried in unreadable low-level form; this paper (Princeton, Northwestern, Argonne) builds SPLENDID, a decompiler that turns a compiler's parallelized LLVM internal code back into clean, portable, human-readable C with OpenMP pragmas and real variable names, so a programmer can pick up where the compiler left off and, with about three lines of edits, double the speed of either working alone.

The argument in one glance

  1. Problem

    The best parallel code needs both compiler and programmer, but they cannot see each other's work

    Writing parallel programs by hand is difficult and error-prone, so automatic parallelizing compilers are attractive. But a compiler can only go so far: it cannot make choices that depend on facts only the programmer knows — that out-of-order diagnostic messages are acceptable, that lower floating-point precision is fine, that a particular loop really has no hidden dependence — because it cannot expand the set of valid outputs a program is allowed to produce. So the strongest parallelization is a collaboration: let the compiler do what it can automatically, then let the programmer refine it with knowledge the compiler lacks. The obstacle is visibility. The compiler's parallelized result lives in its low-level internal representation, not in source the programmer can read and edit. To collaborate, the programmer needs to see what the compiler parallelized, and how, in a form they can build on.

  2. What people did

    Show the programmer the compiler's work with existing tools

    There are ways to surface the compiler's parallelization. You can show assembly. You can run analysis tools that report program properties or suggest actions — dependences to remove, loops to consider. Or you can use a decompiler to turn the compiled form back into source-like code, of which many exist for reverse engineering, security, and software maintenance.

  3. Why it fell short

    Assembly is unreadable, suggestions are extra work, and decompiled parallel code is unusable

    Assembly is far too obfuscated to find parallelism in. Analysis tools that only suggest changes do not reduce the programmer's work — the programmer must still make each change by hand, and asking for improvements to only some regions signals that other regions are done, limiting impact. Decompilers are the promising path, but existing ones fail on parallel code in three specific ways: they hard-code the parallel runtime library's function calls into the output, so it only recompiles against that one runtime (not portable); the loop-rotation the compiler applied leaves them emitting do-while loops, which cannot legally carry an OpenMP for-loop pragma (not natural, not even syntactically valid to parallelize); and single-static-assignment form has shattered each source variable into register-named fragments, so the code is full of meaningless names. The programmer cannot practically build on any of it.

  4. This paper's idea

    Decompile the parallel internal code back into portable, natural OpenMP source

    SPLENDID is the first decompiler that translates parallel LLVM internal code into portable, natural C with OpenMP. It strips out the runtime-specific parallelization setup and re-expresses the parallelism as standard OpenMP pragmas, so the code recompiles with any compiler. It undoes the loop-rotation that produced do-while loops, reconstructing the canonical for-loops that OpenMP pragmas require. And it reverses single-static-assignment, collapsing register fragments back into single variables and recovering the majority of their original source names — even inferring names across an inlined parallel region from debug information. The programmer receives a clean, compiler-parallelized starting point and, with about three lines of edits, can double the performance of either the compiler or hand-parallelization alone.

Why this problem is worth solving

Parallelization is inherently a partnership, and the value is lost if the two partners cannot exchange work in a usable form.

Getting good parallel performance is, to some degree, always a collaboration between the programmer and the compiler. The programmer expresses parallelism, or code properties that enable it, and the compiler maps that to hardware; or the compiler parallelizes automatically and the programmer refines. The reason it cannot be fully automated is fundamental: a compiler is not allowed to change what outputs a program may produce, even when a different output would be both far faster and perfectly acceptable to the programmer. Printing diagnostic messages out of order, tolerating slightly lower floating-point precision, or knowing that a loop carries no real dependence are facts only the programmer holds, and only they can authorize the compiler to exploit. So the highest-performing, lowest-effort path is for the compiler to parallelize what it safely can and then hand the result to the programmer to improve with knowledge the compiler lacks. But a handoff requires a shared, readable artifact. If the compiler’s work is locked in low-level internal form, the programmer cannot see what was parallelized or how, cannot build on it, and the collaboration never happens. Making that handoff practical is what this paper is about.

The natural bridge is decompilation, but a decompiler only enables collaboration if its output is both portable and natural.

For decompiled code to be a usable starting point, it needs two properties. Portability means the recovered source recompiles with any compiler of the source language, not just the one that produced it — otherwise the programmer is locked to a single toolchain and cannot take the compiler’s parallelization elsewhere. Naturalness means the code follows contemporary coding practice — canonical loops, a standard parallel programming model, meaningful variable names — so that reading it is informative about what and how the compiler parallelized, and editing it fits any normal workflow. Existing decompilers were built for other goals — reverse engineering, security analysis, software validation — where the output need not be recompilable or human-friendly, only informative about behavior. For parallel programs they therefore fall short on both counts, because parallelism in the internal code is expressed through a specific runtime library’s calls and through canonicalizing transforms like loop rotation, and faithfully echoing those into source produces code that is neither portable nor natural. Delivering both at once, for parallel code, is the gap SPLENDID sets out to fill.

What people did before, and where it broke

  • Show the programmer assembly. Present the compiled machine code directly. Where it broke. Assembly is far too obfuscated for a programmer to find and reason about parallelism in; it is not a usable collaboration surface.
  • Analysis tools that suggest changes. Report program properties or recommend actions, such as dependences to remove. Where it broke. Suggestions do not reduce work — the programmer must still make each change by hand — and asking for improvements in only some regions implies the rest are finished, capping the programmer’s impact.
  • Existing decompilers (Ghidra, Rellic, LLVM C backend, and others). Turn compiled or internal code back into source-like code for reverse engineering, security, or maintenance. Where it broke. Their goals never required recompilable or natural output, so on parallel code they hard-code the parallel runtime’s calls (not portable), emit do-while loops from rotated loops that cannot carry an OpenMP pragma (not natural), and name variables after registers (meaningless).
  • Fully automatic parallelizing compilers (Polly, and similar). Parallelize the program without the programmer. Where it broke. They cannot use facts only the programmer knows, so on programs where a small human insight unlocks large gains they leave performance on the table, and their result stays trapped in internal form.
  • The gap. No decompiler produced parallel source that is both portable — recompilable with any compiler — and natural — canonical loops, standard OpenMP, real variable names — which is exactly what a programmer needs to build on the compiler’s parallelization. Producing that is what SPLENDID contributes.

How SPLENDID works

From runtime-laden parallel internal code to clean, portable OpenMP source

  1. new

  2. new

  3. new

  4. new

  5. new

The idea

Treat the compiler’s parallelized internal code as a first draft to be handed back, not a black box. The reframing is that automatic parallelization and manual parallelization are not competitors but stages of one workflow, provided the compiler’s output can be returned to the programmer in a form they can read and edit. SPLENDID makes that return practical by decompiling the parallel internal representation into real source, so the programmer sees precisely what the compiler did — which loops it parallelized and how — and can maintain that decompiled source in place of the original, then push further where only they have the knowledge to do so. Recovering readable, structured, source-level meaning from a low-level representation that was never meant to be read again is the same reconstruction challenge the work on accurate disassembly of complex binaries covered elsewhere on this site takes on, rebuilding the high-level shape of code from a form that has been flattened and stripped of its original structure.

Undo exactly the transformations that block portability and naturalness, and no others. SPLENDID’s technical core is a set of careful de-transformations. It removes the runtime-library setup and re-expresses parallelism as OpenMP pragmas, so the source recompiles anywhere; it de-transforms loop rotation back into canonical for-loops, which are both natural and the only form an OpenMP loop pragma will legally accept; and it reverses single-static-assignment, collapsing the register fragments a variable was split into and recovering its source name from debug information — inferring the name across an inlined parallel region when the fragment itself has none. Crucially it de-transforms only what harms the goal: loop rotation is undone because it blocks pragmas and readability, but performance-critical optimizations are deliberately kept, since they do not hurt portability and are what make the code fast. Leaning on the debug metadata that ties low-level instructions back to source variables to reconstruct meaning through layers of optimization is the same reliance the work on compilation consistency modulo debug information covered elsewhere on this site examines, treating that metadata as the thread connecting optimized code to its source.

Aim for the sweet spot where the compiler’s strength and the programmer’s knowledge compound. The payoff is not that the decompiled code is perfect, but that it is a strong, honest starting point from which a tiny amount of human effort yields outsized gains. On programs simple enough that either the compiler or a programmer should have reached top performance but neither did, SPLENDID’s portable OpenMP output plus about three lines of edits doubles the speedup over either approach alone — the collaboration beats both soloists. The whole enterprise rests on the automatic parallelizer’s ability to find and express parallelism in the first place, the strength SPLENDID makes reusable. Building compiler machinery that discovers and encodes parallelism in loops is the same foundational capability the work on supporting dynamic task parallelism beyond static parallel loops covered elsewhere on this site advances, extending what a compiler can automatically parallelize.

Following a loop through SPLENDID

Turning one rotated, runtime-driven parallel loop into clean OpenMP

A trace, following the paper's motivating PolyBench example, of how SPLENDID recovers a natural for-loop from parallelized internal code.

Step 1 of 5

The parallel internal code arrives

A simple stencil loop has been compiled, optimized, and parallelized by Polly. In the internal code the loop body is wrapped in a runtime fork call, its bounds come from a runtime scheduling-init call and are per-thread, the loop is rotated with a guard check, and the array variable appears as a register argument to an outlined parallel region.

Directly echoing this to C, as prior decompilers do, yields non-portable, unnatural code.

Why it works

  • Removing runtime-specific calls is what buys portability. Prior decompilers echo the parallel runtime’s fork and scheduling calls into the source → but those calls only link against one runtime library, so the code is stuck with one toolchain → so by deleting them and re-expressing parallelism as standard OpenMP, SPLENDID makes the compiler’s work recompilable with any compiler.
  • A canonical for-loop is both natural and legally required for the pragma. Loop rotation leaves a do-while loop with a guard check → but an OpenMP for pragma may only be applied to a syntactically canonical for-loop → so de-transforming rotation back into a for-loop simultaneously improves readability and makes the parallel pragma valid.
  • Reversing single-static-assignment restores human-meaningful variables. Optimization split each source variable into many register fragments joined by phi instructions → but collapsing those fragments and mapping them back through debug metadata recovers the original names → so the code reads like something a person wrote, cutting the effort to understand it.
  • Inferring names through inlining reaches variables that lost their metadata. Some instructions, like arguments to an outlined parallel region, carry no debug name → but inlining equates such an argument to a value the sequential code does map to a source variable → so SPLENDID transfers that name across the boundary and recovers the majority of names rather than only the easy ones.
  • De-transforming selectively keeps the code fast and instructive. Undoing every optimization would either break performance or hide what the compiler did → but SPLENDID reverses only loop rotation, which blocks pragmas and naturalness, while keeping performance-critical optimizations → so the output stays fast, portable, and honestly informative about the parallelization.

What it costs (trade-offs and scope)

  • It decompiles internal code, not stripped binaries. SPLENDID operates on parallel LLVM internal representation and leans on debug metadata to recover names; it is not a binary decompiler working on published, stripped executables, where that metadata is gone and its naming techniques would not directly apply.
  • Variable-name recovery is most, not all. Because two register fragments with overlapping lifetimes cannot map to the same source variable (a conflict), and heavily optimized regions can lose their debug intrinsics entirely, SPLENDID recovers the majority of source names but not every one; some variables remain synthesized.
  • The evaluation is on regular scientific kernels. Results are on PolyBench, the affine, loop-heavy benchmark suite that automatic parallelizers like Polly target; how natural and how portable the output is for irregular real-world code with complex pointers and control flow is not demonstrated here.
  • It presupposes a parallelizing compiler and a specific pipeline. The workflow starts from parallel internal code produced by a tool such as Polly; the quality of the collaboration depends on what that upstream parallelizer found, and the de-transformations are tuned to the transforms that pipeline applies.
  • Naturalness is measured by a proxy metric. The 39-times naturalness gain is on BLEU, a text-similarity score borrowed from language translation that compares against reference source; higher BLEU indicates closer-to-human code but is a proxy, not a direct measure of how much programmer effort is saved.

The evaluation

Producing parallel source a programmer can build on

PropertyPrior decompilersAutomatic parallelizer aloneSPLENDID (this work)
How parallelism appears in output
Loop form
Variable namesn/a
Naturalness (BLEU vs source)n/a
Best achievable performance
From the paper's evaluation on the PolyBench suite. SPLENDID-decompiled code recompiled with GCC delivers an average 11-times speedup on 16 benchmarks made available universally outside LLVM, scores 39 times higher on the BLEU naturalness metric than the best prior decompiler, and — with an average of about three lines of manual change on top of its output — doubles the speedup relative to both automatic and manual parallelization alone on 7 kernels.Underlined values explain themselves when clicked.

Check yourself

Check your understanding

  1. 1. Why is parallelization framed as a collaboration between the programmer and the compiler?

  2. 2. Why does decompiled parallel code from prior tools fail to support collaboration?

  3. 3. What are SPLENDID's main techniques for producing portable, natural OpenMP code?

  4. 4. What did SPLENDID achieve, and what are its limits?

Sources

The paper is “SPLENDID: Supporting Parallel LLVM-IR Enhanced Natural Decompilation for Interactive Development” by Zujun Tan, Yebin Chon, Michael Kruse, Johannes Doerfert, Ziyang Xu, Brian Homerding, Simone Campanoni, and David I. August (Princeton University, Northwestern University, and Argonne National Laboratory), published at ASPLOS 2023 (Volume 3), DOI 10.1145/3582016.3582058. It is released under a Creative Commons Attribution license.

This explainer is written from the full text of the authors’ open-access version. Everything above comes from the paper’s own sections: the introduction and motivation on collaborative parallelization and why a compiler cannot use facts only the programmer knows (such as acceptable output reordering or reduced floating-point precision), and why decompilation is the promising bridge but existing decompilers fail on parallel code; the three identified roadblocks — lack of explicit parallelism (runtime-library calls like the LLVM OpenMP runtime fork call baked into the source, breaking portability), obfuscated control-flow translation (loop rotation leaving do-while loops that cannot carry an OpenMP for pragma), and variable names irrelevant to semantics (single-static-assignment splitting a source variable into conflicting register fragments and optimization stripping debug intrinsics); the SPLENDID overview and design, including parallel runtime elimination and OpenMP pragma generation, loop parameter restoration, loop rotation de-transformation and for-loop construction (finding the induction variable, verifying a counted loop, removing the guard check by equivalence to the initial exit condition), and single-static-assignment de-transformation with source variable renaming from debug metadata plus name inference across an inlined parallel region, while deliberately de-transforming only loop rotation and preserving performance-critical optimizations; and the evaluation reporting an average 11-times speedup on 16 PolyBench benchmarks recompiled with GCC outside LLVM, a 39-times higher average BLEU naturalness score over the best prior decompiler, and a doubling of speedup on 7 kernels with an average of about three lines of manual change on top of SPLENDID-generated code, relative to both automatic and manual parallelization alone.

Related papers on Compilers & Programming Languages

See all 162 →