<div dir="ltr"><div><div>Hi folks,<br><br>This summer I'm working on improving dominators during my internship at Google. Below is an RFC on switching to dynamic dominators, which you can also read as a <a href="https://docs.google.com/document/d/1wPYeWykeO51YDPLYQEg4KNTlDIGIdyF65OTfhSMaNHQ/edit?usp=sharing">Google Doc</a> if you prefer so. Please let us know what you think.<br><br>~Kuba<br><br>=======================================================================<br><br><b>1. Context</b></div><div><br></div><div>Currently LLVM uses the Simple Lengauer-Tarjan algorithm to build the (post)dominator tree. The only way to update it is by manually setting IDoms which is not obvious in many cases and can be extremely error-prone. And because updating it manually is so difficult, programmers tend to just recompute it after changing the CFG (by not AU.addPreserved()'ing the DomTree). This causes DomTree calculation to fire very often even if only a very small portion of it gets really affected by the changes in CFG. As an example, when optimizing a Full LTO clang bitcode, DominatorTreeWrapperPass alone calls DT.recalculate over 6.5 million times, which takes 20s on my machine.</div><div> </div><div>Using an incremental algorithm it would be much easier to keep an up-to-date DomTree without custom update logic, which will save us the time currently spent during DomTree recalculations and reduce the number of bugs caused by manual updates. It would also make it feasible to maintain postdominators and use them more broadly, which currently can be too complicated and expensive.</div><div><br></div><div><b>2. Proposal</b></div><div><br></div><div>The proposal is to make dominators use the incremental algorithm that allows to keep (post)dominator tree up to date as CFG changes. To achieve that, we would implement the dynamic depth based search algorithm (DBS) described in this paper <a href="https://arxiv.org/pdf/1604.02711.pdf">[1]</a> and expose 2 main new functions: insertArc(From, To) and deleteArc(From, To). The algorithm uses SemiNCA under the hood which would replace Lengauer-Tarjan implementation currently used.</div><div> </div><div>The second part of the proposal is to gradually deprecate and remove the existing API for manually manipulating dominator tree (changeImmediateDominator, addNewBlock) and replace its use within LLVM with the new incremental API.  </div><div><br></div><div><b>3. Proof of concept</b></div><div><br></div><div>The prototype implementation can be found in my LLVM fork <a href="https://github.com/kuhar/llvm-dominators">[2]</a>. It comes with several layers of verification and was tested on clang, llvm test suite and a few open source libraries.</div><div>The code provides the basic functionality and tries be ‘API-equivalent’ with the current DomTree implementation. The NewDomTree is also able to convert itself to the current one for testing and verification purposes.</div><div>Dynamic dominators are hacked into 3 existing passes (DomTreeWrapperPass, LoopDeletion, LoopSimplifyCFG) to test correctness and experiment with the use of the new API.</div><div> </div><div>The prototype also comes with a bunch of testing utilities and a simple benchmarking tool.</div><div><br></div><div><b>4. Performance</b></div><div><br></div><div>The real life performance of full dynamic DBS-based DomTree recalculation is between 20% and 2% better on a machine with two Xeon E5-2680v2 CPUs than the existing SLT algorithm, which is consistent with the findings from this thesis <a href="ftp://ftp.cs.princeton.edu/reports/2005/737.pdf">[3]</a>. The advantage is not that visible on debug builds, where the DBS-algorithm can be up to 8% slower. It is most like possible to speed up debug builds, but I haven’t looked into that yet.</div><div>The benchmark performed <a href="https://docs.google.com/document/d/1wPYeWykeO51YDPLYQEg4KNTlDIGIdyF65OTfhSMaNHQ/edit?usp=sharing">[4]</a> loads of a (full LTO) bitcode file and builds DomTress of all functions 15 times.</div><div> </div><div>The current DomTree updates DFS in-out numbers eagerly upon construction, while the new one postpones is until they are actually needed. To make the benchmark fair, numbers were collected for both eager and lazy strategy for the new DomTree.</div><div> </div><div>The main advantage of the incremental algorithm comes from the fact that it allows incremental updates without rebuilding the whole tree, not from the slightly faster construction.</div><div>What’s more, the insertArc / deleteArc API doesn’t force updates to happen immediately -- they can be queued behind the scenes and happen in batches if we decide to pursue that design later.</div><div><br></div><div><b>5. Transition plan</b></div><div><br></div><div>There are several possibilities when it comes to transition. The biggest problem is that the current DomTree exposes an interface for manual updates (setIDom, changeImmediateDominator), and for manual construction (addNewBlock). Because of the additional data stored in the incremental algorithm (relative dominators, preorder parents, levels) it is not really possible to use the old API while keeping this data up-to-date. The incremental algorithm relies on this information when performing fast arc deletions; It is still able to perform them without it -- deletions are then just slower in some cases.</div><div>The most straightforward solutions are:</div><div> </div><div>a) Keep the existing DomTree and gradually replace its uses with the new one. It is possible to convert the DBS-based dominators to the old ones.</div><div>b) Replace the existing DomTree with the new, dynamic dominators. Nuke all of the old update functions and replace their uses with the new incremental API in one commit.</div><div>c) Replace the existing DomTree with the new one, but keep the old API around and mark it as deprecated. If someone invokes one of the old update functions, mark the the additional information as invalid for dynamic deletions. Follow the pessimistic but correct dynamic deletion path if the additional information is marked as invalidated. Gradually modernize the projects to use the new API instead and then remove the old update functions.</div><div> </div><div>Maintaining the old DT and the new one simultaneously seems like a waste of time as the DBS offers better or similar performance to the existing SLT-based implementation.</div><div>The problem with replacing the old API with the new one is that it’s used in many places (~100) across the project and I believe that doing that all at once would be tricky to get correct. Gradual update seems much easier to ensure correctness and the transitional API (invalid additional data check) is trivial to implement. Because of that, I propose to follow the option (c). </div><div><br><br><br></div><div>[1] Georgiadis et al., An Experimental Study of Dynamic Dominators, <a href="https://arxiv.org/pdf/1604.02711.pdf">https://arxiv.org/pdf/1604.02711.pdf</a></div><div>[2] llvm-dominators LLVM fork on Github, <a href="https://github.com/kuhar/llvm-dominators">https://github.com/kuhar/llvm-dominators</a></div><div>[3] L. Georgiadis, Linear-Time Algorithms for Dominators and Related Problems, <a href="ftp://ftp.cs.princeton.edu/reports/2005/737.pdf">ftp://ftp.cs.princeton.edu/reports/2005/737.pdf</a> p. 38</div><div>[4] Google Doc with the performance numbers, <a href="https://docs.google.com/document/d/1wPYeWykeO51YDPLYQEg4KNTlDIGIdyF65OTfhSMaNHQ/edit?usp=sharing">https://docs.google.com/document/d/1wPYeWykeO51YDPLYQEg4KNTlDIGIdyF65OTfhSMaNHQ/edit?usp=sharing</a></div></div>
</div>