<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>LingoDB - Revolutionizing Data Processing with Compiler Technology Blog</title>
        <link>https://www.lingo-db.com/blog</link>
        <description>LingoDB - Revolutionizing Data Processing with Compiler Technology Blog</description>
        <lastBuildDate>Sun, 30 Aug 2026 17:26:31 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <item>
            <title><![CDATA[Maintainable, Low-Latency and High-Quality Query Compilation with MLIR and TPDE]]></title>
            <link>https://www.lingo-db.com/blog/mlir-tpde</link>
            <guid>https://www.lingo-db.com/blog/mlir-tpde</guid>
            <pubDate>Sun, 30 Aug 2026 17:26:31 GMT</pubDate>
            <description><![CDATA[This post explains how to fine-tune MLIR towards low-latency and how to combine it with TPDE for maintainable, low-latency and high-quality query compilation]]></description>
            <content:encoded><![CDATA[<p>Compiling a TPC-H query in LingoDB used to take around 35ms. It now takes around 5ms, and the generated code is just as fast as before on small datasets. The largest part of that came from machine code generation, which dropped from 26ms to 0.57ms, and the rest from tuning MLIR itself. MLIR-based query engines usually pay 50 to 150ms of compilation latency for a typical analytical SQL query. That overhead dominates the total latency on small datasets, and it is what kept LingoDB behind Hyper, Umbra and DuckDB. In this blog post we explain how to tune MLIR for low-latency and how to combine it with TPDE to allow for near instant query compilation. This blog post builds on our ADMS'26 workshop <a href="https://db.in.tum.de/~jungmair/papers/adms-2026-fast-compilation.pdf" target="_blank" rel="noopener noreferrer">paper</a>: Maintainable, Low-Latency and High-Quality Query Compilation with MLIR and TPDE.</p>
<p>The compilation latency has two sources: MLIR itself is built for flexibility rather than for low latency, and the standard path to machine code runs through LLVM-IR, where a general-purpose compiler does work that a query engine does not need. We attacked both, and the two halves are independent of each other. The MLIR tuning works in any MLIR-based system, even one that keeps LLVM.</p><h2 class="anchor anchorWithStickyNavbar_LWe7" id="lingodbs-compiler-pipeline-a-brief-overview">LingoDB's Compiler Pipeline: A Brief Overview<a href="https://www.lingo-db.com/blog/mlir-tpde#lingodbs-compiler-pipeline-a-brief-overview" class="hash-link" aria-label="Direct link to LingoDB's Compiler Pipeline: A Brief Overview" title="Direct link to LingoDB's Compiler Pipeline: A Brief Overview">​</a></h2><p>Before we dive into the technical details, we first want to provide a quick overview of our query compilation pipeline.</p><figure class="figure_gBOj" id="pipeline-architecture"><img src="https://www.lingo-db.com/img/blog/mlir-tpde/overview-figure.svg" alt="Compiler Frontend and Backend Pipeline" class="tikz-canvas"><figcaption class="caption_hXsc"><span class="label_siMd">Figure<!-- -->&nbsp;<!-- -->1<!-- -->.</span> <!-- -->The MLIR-TPDE Compiler Frontend and Backend Pipeline architecture.</figcaption></figure><p>As shown in <a href="https://www.lingo-db.com/blog/mlir-tpde#pipeline-architecture">Figure<!-- -->&nbsp;<!-- -->1</a>, the query parsing stage converts the given SQL statement directly into a high level MLIR module, containing well-known relational operations like joins and tablescans. This MLIR module is then optimized and lowered in layers (<a href="https://www.lingo-db.com/docs/ForDevelopers/MLIR/Dialects/RelAlg/RelAlgDialect">RelAlg</a> -&gt; <a href="https://www.lingo-db.com/docs/ForDevelopers/MLIR/Dialects/SubOperator/SubOperatorDialect">SubOp</a> -&gt; Basic Ops) until it eventually only consists of operators from low-level MLIR dialects like <code>arith</code>, <code>scf</code> and <code>func</code>.</p><p>After passing through the compiler frontend, the module has to be lowered to machine code. Previously, we used the common approach to first transform the code into LLVM-IR to then use the existing LLVM infrastructure. However, this approach suffered from long compilation latencies. To avoid this compilation latency hurdle, we want to present our novel MLIR-TPDE backend, which finally brings low-latency compilation to MLIR.</p><h2 class="anchor anchorWithStickyNavbar_LWe7" id="speeding-up-mlir-itself">Speeding up MLIR itself<a href="https://www.lingo-db.com/blog/mlir-tpde#speeding-up-mlir-itself" class="hash-link" aria-label="Direct link to Speeding up MLIR itself" title="Direct link to Speeding up MLIR itself">​</a></h2><p>Before we dive into TPDE, we first want to give a brief overview of the techniques used to speed up MLIR. The following five techniques are independent of the backend and thus work in any MLIR-based system. Start here even if you keep LLVM.</p><p><strong>Pre-initialize contexts:</strong> Registering all dialects, ops, types, and attributes on a fresh <code>MLIRContext</code> costs roughly 0.5ms per query. We keep a pool of pre-built contexts and refill it asynchronously. The init cost leaves the critical path. This one looks better than it is: it cuts the SQL phase by 28% to 45%, but only around 3% of the total, because the lowering phase dominates everything else.</p><p><strong>Disable verification in production:</strong> MLIR verifies the Intermediate Representation (IR) after every transformation. That is great during development and acts like asserts. In production it is pure overhead. Turning it off cut the optimization and lowering phases by about 24%.</p><p><strong>Tune pattern application and canonicalization:</strong> MLIR's rewrite driver runs several expensive simplification steps by default that changed almost nothing in our pipeline. The canonicalization pass also does region simplification you can switch off via config. Tuning the pattern driver cut those phases by about 21%.</p><p><strong>Fold proactively:</strong> Instead of cleaning up with a full canonicalization pass, use <code>createOrFold</code> when you build operations. For example, a box operation immediately followed by an unbox operation cancels at construction time. You keep the benefit and skip a pass over the whole module. When you still need cleanup, run a small targeted pattern set rather than general canonicalization.</p><p><strong>Generate less IR in the first place:</strong> Every operation created early is an operation that every later pass has to walk. Instead of generating code for filters, we run vectorized filters over data chunks. We also removed unnecessary null checks and unboxing operations, and introduced fused low-level operations for common cases: checking a tagged pointer against a hash value, setting and testing specific bits in integers, and loading and storing struct members without explicitly calculating the address.</p><h2 class="anchor anchorWithStickyNavbar_LWe7" id="integrating-tpde">Integrating TPDE<a href="https://www.lingo-db.com/blog/mlir-tpde#integrating-tpde" class="hash-link" aria-label="Direct link to Integrating TPDE" title="Direct link to Integrating TPDE">​</a></h2><p><a href="https://github.com/tpde2/tpde" target="_blank" rel="noopener noreferrer">TPDE</a> is a novel framework for compiler backends that directly works on the IR at hand. Its aim is low-latency code generation, while preserving reasonable (<code>-O0</code>) code quality.
After creating a custom adapter to your IR, TPDE is able to directly iterate over it. As TPDE provides cheap liveness analysis, register allocation and instruction selection, developers can focus on the high-level IR and let TPDE handle the low-level details.</p><p>MLIR-TPDE supports <code>func</code>, <code>cf</code> and <code>arith</code>, and we deliberately stopped there. Supporting structured control flow like <code>scf.for</code> would force the backend to create virtual basic blocks for loops, which increases complexity significantly. Since our frontend lowers to basic dialects anyway, a built-in MLIR pass converts <code>scf</code> to <code>cf</code> for us, and TPDE can then read our IR directly using a much simpler adapter.</p><p>Three components form this new compiler backend:</p><p><strong>The IR Adapter</strong> bridges MLIR to TPDE. The hard part is reconciling two SSA models. TPDE expects φ-nodes. MLIR uses block arguments. A φ-node selects a value depending on which predecessor edge control flow arrived from, which is what a loop variable needs: the entry edge carries the initial value, the back edge carries the updated one. MLIR encodes the same information from the other side, as an argument on the jump operation in each predecessor. Our adapter therefore emulates φ-nodes by looking up every predecessor block and reading the jump operations that feed the current block's arguments.</p><p><strong>Snippets</strong> allow specifying the semantics of an operation by writing tiny C(++) functions. TPDE's EncodeGen tool compiles them to LLVM-IR at build time, then turns that into encoder functions that emit ISA instructions. The snippets are platform-agnostic. Write the semantics once, get x86-64 and aarch64 for free.</p><p>Here is a simple example for a snippet that adds two unsigned integers:</p><div class="language-cpp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-cpp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">unsigned</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">addU32</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">unsigned</span><span class="token plain"> a</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">unsigned</span><span class="token plain"> b</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> a </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> b</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">unsigned</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">long</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">addU64</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">unsigned</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">long</span><span class="token plain"> a</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">unsigned</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">long</span><span class="token plain"> b</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> a </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> b</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div><p>In the backend you grab value references and call the generated encoder by bit width. A real implementation folds whole operation categories into a lookup table of encoder pointers instead of a switch.</p><div class="language-cpp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-cpp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">bool</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">compile_arith_addi</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">IRInstRef inst</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// prepare value references from op arguments</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">auto</span><span class="token plain"> lhs_ref </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token function" style="color:#d73a49">val_ref</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">inst</span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token function" style="color:#d73a49">getOperand</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">auto</span><span class="token plain"> rhs_ref </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token function" style="color:#d73a49">val_ref</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">inst</span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token function" style="color:#d73a49">getOperand</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// prepare result reference</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">auto</span><span class="token plain"> res_ref </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token function" style="color:#d73a49">result_ref</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">inst</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">auto</span><span class="token plain"> bitwidth </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> inst</span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token function" style="color:#d73a49">getResult</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">getType</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">getBitWidth</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// call encoder generated from the snippets depending on bitwidth</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">switch</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">bitwidth</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">32</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">derived</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token function" style="color:#d73a49">encode_addU32</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">lhs_ref</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">part</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> rhs_ref</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">part</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> res_ref</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">part</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">64</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">derived</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token function" style="color:#d73a49">encode_addU64</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">lhs_ref</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">part</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> rhs_ref</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">part</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> res_ref</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">part</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div><p><strong>Target-specific backends:</strong> This layer is small. It covers platform-specifics like calling conventions, jumps and conditional branches, and resolving global symbols through the GOT. It also drives instruction fusion, for example merging a comparison into the following branch. TPDE's builder helpers do most of the work.</p><h2 class="anchor anchorWithStickyNavbar_LWe7" id="custom-operations-are-cheap">Custom operations are cheap<a href="https://www.lingo-db.com/blog/mlir-tpde#custom-operations-are-cheap" class="hash-link" aria-label="Direct link to Custom operations are cheap" title="Direct link to Custom operations are cheap">​</a></h2><p>Snippets pay off most for domain-specific operations. A database engine needs primitives that no general-purpose compiler ships with, and in a hand-written backend each of them requires one implementation per target architecture. With TPDE's snippets, the semantics are written once in C++ and TPDE's EncodeGen tool derives the x86-64 and aarch64 encoders from them. A tagged-pointer probe against a 16-bit filter (as described <a href="https://db.in.tum.de/~birler/papers/hashtable.pdf" target="_blank" rel="noopener noreferrer">in Altan Birler's hash table paper</a>) is a good example:</p><div class="language-cpp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-cpp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">bool</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">ptr_tag_matches</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">void</span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> ptr</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">uint64_t</span><span class="token plain"> hash</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">void</span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> bloomMaskPtr</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">uint64_t</span><span class="token plain"> slot </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> hash </span><span class="token operator" style="color:#393A34">&gt;&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">53</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">uint16_t</span><span class="token plain"> tag </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">uint16_t</span><span class="token operator" style="color:#393A34">*</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> bloomMaskPtr</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">slot</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">uint16_t</span><span class="token plain"> entry </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">uint16_t</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">uintptr_t</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> ptr</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">tag </span><span class="token operator" style="color:#393A34">&amp;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">entry </span><span class="token operator" style="color:#393A34">^</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0xFFFF</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">==</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div><p>Like in our previous snippet example, we only have to call the derived method from this snippet in the backend to provide our engine with custom operations.</p><h2 class="anchor anchorWithStickyNavbar_LWe7" id="results">Results<a href="https://www.lingo-db.com/blog/mlir-tpde#results" class="hash-link" aria-label="Direct link to Results" title="Direct link to Results">​</a></h2><p>To quantify the impact of our solution, we provide in this section some numbers from our workshop paper. The x86-64 experiments ran on an <code>m7a.4xlarge</code> instance, the aarch64 experiments on a Graviton 4 <code>m8g.4xlarge</code>. We compared against Umbra 26.02, Tableau Hyper 0.0.22106 and DuckDB 1.4.3, using 5 warmup runs and the median over 20 measured runs.</p><h3 class="anchor anchorWithStickyNavbar_LWe7" id="mlir-speedup">MLIR speedup<a href="https://www.lingo-db.com/blog/mlir-tpde#mlir-speedup" class="hash-link" aria-label="Direct link to MLIR speedup" title="Direct link to MLIR speedup">​</a></h3><p>In this ablation study, we want to isolate the impact of each individual MLIR tuning step. Every row adds one technique on top of the row above it, and the percentages are relative to that previous row.</p><div class="blog-table"><figure class="table_u3Oy" id="mlir-speedup"><div class="scroll_lM8M"><table><thead><tr><th>Benchmark</th><th>Optimization</th><th>SQL</th><th>Opt.</th><th>Lowerings</th><th>∑</th></tr></thead><tbody><tr><td><strong>TPC-H</strong></td><td>-</td><td>1.0</td><td>1.4</td><td>7.0</td><td>9.4</td></tr><tr><td></td><td>NoVerification</td><td>1.0 <em>+2%</em></td><td>1.1 <em>-20%</em></td><td>5.2 <em>-26%</em></td><td>7.3 <em>-22%</em></td></tr><tr><td></td><td>AsyncContext</td><td>0.6 <em>-45%</em></td><td>1.2 <em>+5%</em></td><td>5.2 <em>+2%</em></td><td>7.0 <em>-4%</em></td></tr><tr><td></td><td>NoCleanup</td><td>0.5 <em>-6%</em></td><td>1.2 <em>+0%</em></td><td>3.7 <em>-30%</em></td><td>5.4 <em>-23%</em></td></tr><tr><td></td><td>Patterns</td><td>0.5 <em>+0%</em></td><td>1.0 <em>-17%</em></td><td>2.8 <em>-23%</em></td><td>4.3 <em>-20%</em></td></tr><tr><td><strong>TPC-DS</strong></td><td>-</td><td>1.6</td><td>3.3</td><td>17.8</td><td>22.7</td></tr><tr><td></td><td>NoVerification</td><td>1.6 <em>+0%</em></td><td>2.6 <em>-22%</em></td><td>12.9 <em>-27%</em></td><td>17.1 <em>-25%</em></td></tr><tr><td></td><td>AsyncContext</td><td>1.2 <em>-28%</em></td><td>2.6 <em>+2%</em></td><td>13.0 <em>+0%</em></td><td>16.8 <em>-2%</em></td></tr><tr><td></td><td>NoCleanup</td><td>1.2 <em>+0%</em></td><td>2.6 <em>+0%</em></td><td>8.6 <em>-34%</em></td><td>12.4 <em>-26%</em></td></tr><tr><td></td><td>Patterns</td><td>1.1 <em>-1%</em></td><td>2.1 <em>-19%</em></td><td>6.2 <em>-28%</em></td><td>9.5 <em>-23%</em></td></tr></tbody></table></div><figcaption class="caption_hXsc"><span class="label_siMd">Table<!-- -->&nbsp;<!-- -->1<!-- -->.</span> <!-- -->Average compilation latency across high-level compilation phases [ms]. Each row adds one optimization on top of the previous one.</figcaption></figure></div><p>As <a href="https://www.lingo-db.com/blog/mlir-tpde#mlir-speedup">Table<!-- -->&nbsp;<!-- -->1</a> shows, pre-initializing contexts cuts the SQL-&gt;MLIR phase by 28% to 45%, while running no verification steps in production and tuning the pattern application cut the optimization and lowering phases by 20-27% each. Lastly, avoiding canonicalization by using <code>createOrFold</code> further cuts the lowering phase by around 30%. Thus, by applying the techniques explained above, we can halve the time spent in the frontend compiler pipeline.</p><h3 class="anchor anchorWithStickyNavbar_LWe7" id="comparison-with-our-previous-llvm-backend">Comparison with our previous LLVM-Backend<a href="https://www.lingo-db.com/blog/mlir-tpde#comparison-with-our-previous-llvm-backend" class="hash-link" aria-label="Direct link to Comparison with our previous LLVM-Backend" title="Direct link to Comparison with our previous LLVM-Backend">​</a></h3><p>In contrast to the previous experiment, we now take a look at the compiler backend improvements.</p><div class="blog-table"><figure class="table_u3Oy" id="backend-latency"><div class="scroll_lM8M"><table><thead><tr><th></th><th>Backend</th><th>MLIR transform</th><th>Opt.</th><th>Code generation</th><th>Total compile</th></tr></thead><tbody><tr><td><strong>x86-64</strong></td><td>MLIR-TPDE</td><td>0.28 ms</td><td>0.00 ms</td><td>0.29 ms</td><td>0.57 ms</td></tr><tr><td></td><td>LLVM O0</td><td>4.43 ms</td><td>0.00 ms</td><td>6.45 ms</td><td>10.88 ms</td></tr><tr><td></td><td>LLVM Opt</td><td>4.51 ms</td><td>2.38 ms</td><td>19.28 ms</td><td>26.17 ms</td></tr><tr><td><strong>aarch64</strong></td><td>MLIR-TPDE</td><td>0.31 ms</td><td>0.00 ms</td><td>0.31 ms</td><td>0.62 ms</td></tr><tr><td></td><td>LLVM O0</td><td>4.98 ms</td><td>0.00 ms</td><td>6.70 ms</td><td>11.69 ms</td></tr><tr><td></td><td>LLVM Opt</td><td>5.12 ms</td><td>2.38 ms</td><td>20.31 ms</td><td>27.81 ms</td></tr></tbody></table></div><figcaption class="caption_hXsc"><span class="label_siMd">Table<!-- -->&nbsp;<!-- -->2<!-- -->.</span> <!-- -->Average backend compilation latency for TPC-H on x86-64 and aarch64</figcaption></figure></div><p>As <a href="https://www.lingo-db.com/blog/mlir-tpde#backend-latency">Table<!-- -->&nbsp;<!-- -->2</a> shows, by avoiding the costly translation to LLVM-IR and faster TPDE tooling, our MLIR-TPDE backend can compile code in less than a ms. Looking at machine code generation alone, MLIR-TPDE needs 20x less time than LLVM O0 and 66x less than LLVM Opt. Over the whole backend, it is 16x to 19x faster than LLVM O0. The aarch64 numbers are close to the x86-64 ones, which is exactly the point of the snippet approach: we did not write a second backend to get them.</p><p>More interesting than beating our own LLVM path is the comparison to a hand-written one. Hyper's custom backend needs 1.27ms on TPC-H and 2.61ms on TPC-DS, compared to 0.58ms and 1.55ms for MLIR-TPDE. So LingoDB generates code 41% to 54% faster than a custom backend that is built on a custom, latency-optimized IR, while we wrote almost none of the low-level code ourselves.</p><h3 class="anchor anchorWithStickyNavbar_LWe7" id="comparison-to-other-systems">Comparison to other systems<a href="https://www.lingo-db.com/blog/mlir-tpde#comparison-to-other-systems" class="hash-link" aria-label="Direct link to Comparison to other systems" title="Direct link to Comparison to other systems">​</a></h3><p>After looking at our changes in the compiler front- and backend, we now want to put our results into perspective with other systems.</p><figure class="figure_gBOj" id="benchmark"><img src="https://www.lingo-db.com/img/blog/mlir-tpde/benchmark_latency_grid.svg" alt="End-to-end Latency on x86 and AArch64.
LingoDB (MLIR-TPDE) significantly reduces compilation overhead compared to the LLVM backend." class="tikz-canvas"><figcaption class="caption_hXsc"><span class="label_siMd">Figure<!-- -->&nbsp;<!-- -->2<!-- -->.</span> <!-- -->End-to-end Latency on x86 and AArch64. LingoDB (MLIR-TPDE) significantly reduces compilation overhead compared to the LLVM backend.</figcaption></figure><p>Overall, as <a href="https://www.lingo-db.com/blog/mlir-tpde#benchmark">Figure<!-- -->&nbsp;<!-- -->2</a> illustrates, our MLIR-TPDE backend compiles 7x faster compared to the LLVM backend, and now beats Hyper and DuckDB on end-to-end latency. In terms of execution speed, MLIR-TPDE matches LLVM Opt on scaling factor (SF) 1, but suffers from up to 26% slower execution times on SF10 datasets, where TPDE's <code>-O0</code> code quality starts to show once execution dominates. However, due to the way faster compilation, the total query latency shrinks across all datasets: SF1 finishes 3.2x faster, SF10 still at least 20% faster.</p><p>While our backend is faster than Hyper's, our frontend is not. Hyper reaches an imperative representation 2.3x to 2.5x faster than we do, and most of that gap comes from the lowering phase, where we take around 3.5x longer. This is what our layered RelAlg -&gt; SubOp -&gt; Basic Ops design costs: it lets us reason about each optimization at the abstraction level that fits it, and we pay for it in frontend latency. Since both systems spend more time in the frontend than in the backend, Hyper still compiles faster overall, but executes slower than LingoDB. Umbra outperforms all tested systems, achieving the smallest compilation and execution times, at a significantly more complex code base. DuckDB, which avoids compilation at the cost of interpretation overhead, achieves good performance but is still slower than our MLIR-TPDE approach across all tested datasets.</p><h3 class="anchor anchorWithStickyNavbar_LWe7" id="implementation-effort">Implementation effort<a href="https://www.lingo-db.com/blog/mlir-tpde#implementation-effort" class="hash-link" aria-label="Direct link to Implementation effort" title="Direct link to Implementation effort">​</a></h3><p>While having great performance is awesome, a frequently overlooked aspect is the implementation effort and maintenance cost. Maintenance cost is hard to quantify, but we can at least give a brief overview on the implementation effort.</p><div class="blog-table"><figure class="table_u3Oy" id="impl-effort"><div class="scroll_lM8M"><table><thead><tr><th>Component</th><th>Lines of Code</th></tr></thead><tbody><tr><td>Target-independent adapter</td><td>1922</td></tr><tr><td>LingoDB backend glue</td><td>456</td></tr><tr><td>Snippets</td><td>242</td></tr><tr><td>x86-specific</td><td>249</td></tr><tr><td>aarch64-specific</td><td>230</td></tr><tr><td><strong>Total</strong></td><td><strong>3099</strong></td></tr></tbody></table></div><figcaption class="caption_hXsc"><span class="label_siMd">Table<!-- -->&nbsp;<!-- -->3<!-- -->.</span> <!-- -->Lines of code of the MLIR-TPDE backend by component</figcaption></figure></div><p>When we take a look at the required lines of code (LOC) of the novel MLIR-TPDE compiler backend (<a href="https://www.lingo-db.com/blog/mlir-tpde#impl-effort">Table<!-- -->&nbsp;<!-- -->3</a>), we notice that the vast majority is target-independent. The largest component is the platform-independent adapter (1922 lines), which bridges MLIR and TPDE, and compiles most operations through encoders generated from the Snippets (242 lines). The 456 lines of LingoDB backend glue bring MLIR into a form that TPDE can read, plus error handling. Only around 0.5k of the 3.1k lines are platform-specifics, like how to encode comparisons and conditional jumps.</p><p>For comparison, Umbra's x86 and aarch64 backends are 5.3k and 8k LOC, or 13.3k together. We hit just slightly slower backend compilation speed with under a quarter of that. This is of course not a free lunch, as the complexity moved into TPDE, which has a core code base of around 17k lines. The difference is that TPDE is a separate, actively maintained project that is shared across all of its users, while an in-house backend is 13.3k lines that your own team has to port to every new architecture.</p><h2 class="anchor anchorWithStickyNavbar_LWe7" id="reusing-this-in-your-system">Reusing this in your system<a href="https://www.lingo-db.com/blog/mlir-tpde#reusing-this-in-your-system" class="hash-link" aria-label="Direct link to Reusing this in your system" title="Direct link to Reusing this in your system">​</a></h2><p>Lastly, we want to provide an overview of how to reuse this approach in your own system.</p><p>If you use MLIR and want to keep LLVM, take the five techniques from above. They are mostly config changes and small refactors, and they halve the high-level compilation time.</p><p>If you use MLIR and your pipeline eventually lowers to basic dialects like <code>arith</code>, <code>cf</code> or <code>func</code>, the MLIR-TPDE backend can be reused as well. Most parts of our TPDE integration carry over with minimal changes, by e.g. adding a few snippets and wiring them in the backend. The vast majority of the target-dependent code will also require barely any changes, given that it encodes comparisons and jump operations, which are required by most systems anyway.</p><p>If you build a database system, even more of it is already done: Umbra-style 128bit variable-length strings, bit manipulation and hash-table specific implementations are already available in the MLIR-TPDE backend.</p><h3 class="anchor anchorWithStickyNavbar_LWe7" id="a-tour-of-the-source-code">A tour of the source code<a href="https://www.lingo-db.com/blog/mlir-tpde#a-tour-of-the-source-code" class="hash-link" aria-label="Direct link to A tour of the source code" title="Direct link to A tour of the source code">​</a></h3><p>The whole backend lives in <a href="https://github.com/lingo-db/lingo-db/tree/main/src/execution/baseline" target="_blank" rel="noopener noreferrer"><code>src/execution/baseline</code></a> and is around 3.5k lines of code. The table below describes what each file does and how much of it you would have to touch when porting the backend to another MLIR-based engine.</p><table><thead><tr><th>File</th><th>What it does</th><th>Porting effort</th></tr></thead><tbody><tr><td><a href="https://github.com/lingo-db/lingo-db/blob/main/src/execution/baseline/Adaptor.hpp" target="_blank" rel="noopener noreferrer"><code>Adaptor.hpp</code></a></td><td>The IR adapter. Maps MLIR's <code>ModuleOp</code>, <code>Block</code>, <code>Operation</code> and <code>Value</code> onto the entities TPDE expects, enumerates functions and blocks, emulates φ-nodes from block arguments, and reports value liveness.</td><td>Almost none. It only depends on <code>func</code>, <code>cf</code> and MLIR core.</td></tr><tr><td><a href="https://github.com/lingo-db/lingo-db/blob/main/src/execution/baseline/snippets.c" target="_blank" rel="noopener noreferrer"><code>snippets.c</code></a></td><td>Operation semantics as plain C functions, one per operation and bit width: <code>arith</code> integer and float math, loads and stores, and the LingoDB-specific primitives (128-bit varlen strings, hashing, tagged pointers, bit manipulation). EncodeGen turns them into encoder functions at build time.</td><td>Add your own operations here. Everything already present stays useful.</td></tr><tr><td><a href="https://github.com/lingo-db/lingo-db/blob/main/src/execution/baseline/CompilerBase.hpp" target="_blank" rel="noopener noreferrer"><code>CompilerBase.hpp</code></a></td><td>The platform-independent backend and the bulk of the code. One method per supported operation, each calling the encoders generated from the snippets, plus address-expression building, calling conventions for runtime calls, and constant materialization.</td><td>Drop the methods for operations you do not have, add your own. The <code>arith</code>, <code>cf</code> and <code>func</code> handling carries over unchanged.</td></tr><tr><td><a href="https://github.com/lingo-db/lingo-db/blob/main/src/execution/baseline/CompilerX64.hpp" target="_blank" rel="noopener noreferrer"><code>CompilerX64.hpp</code></a>, <a href="https://github.com/lingo-db/lingo-db/blob/main/src/execution/baseline/CompilerA64.hpp" target="_blank" rel="noopener noreferrer"><code>CompilerA64.hpp</code></a></td><td>The target-specific layers, ~300 lines each. Integer comparison, conditional branches with comparison fusion, and loading global symbols through the GOT.</td><td>None in practice. Every engine needs comparisons and branches.</td></tr><tr><td><a href="https://github.com/lingo-db/lingo-db/blob/main/src/execution/baseline/BaselineBackend.cpp" target="_blank" rel="noopener noreferrer"><code>BaselineBackend.cpp</code></a></td><td>The glue to LingoDB. Runs the <code>scf</code>-to-<code>cf</code> conversion, picks the backend for the host architecture, drives compilation, and calls the generated <code>main</code>.</td><td>Replace with your engine's equivalent entry point.</td></tr><tr><td><a href="https://github.com/lingo-db/lingo-db/blob/main/src/execution/baseline/Loader.hpp" target="_blank" rel="noopener noreferrer"><code>Loader.hpp</code></a></td><td>Makes the emitted code callable. <code>InMemoryLoader</code> maps TPDE's ELF output into memory and resolves runtime symbols via <code>dlsym</code>, <code>DebugLoader</code> instead writes an object file and links it, so that generated code can be inspected.</td><td>Reusable as-is.</td></tr><tr><td><a href="https://github.com/lingo-db/lingo-db/blob/main/src/execution/baseline/CMakeLists.txt" target="_blank" rel="noopener noreferrer"><code>CMakeLists.txt</code></a></td><td>The build wiring that makes snippets work: compile <code>snippets.c</code> to LLVM bitcode with Clang, then run <code>tpde_encodegen</code> on it to generate the encoder header.</td><td>Copy the two custom commands, change the snippet file name.</td></tr></tbody></table><p>In combination, the MLIR-level tunings and the reusable MLIR-TPDE backend substantially reduce both compilation latency and implementation effort for low-latency, MLIR-based code generation.
The source code of LingoDB is available on <a href="https://github.com/lingo-db/lingo-db" target="_blank" rel="noopener noreferrer">GitHub</a>.</p>]]></content:encoded>
            <category>query compilation</category>
            <category>TPDE</category>
            <category>MLIR</category>
            <category>maintainable</category>
            <category>open source</category>
            <category>low-latency</category>
        </item>
    </channel>
</rss>