Skip to content

Instantly share code, notes, and snippets.

@kassane
Last active June 4, 2025 12:47
Show Gist options
  • Save kassane/85c71043050714d31a966dcb6a871468 to your computer and use it in GitHub Desktop.
Save kassane/85c71043050714d31a966dcb6a871468 to your computer and use it in GitHub Desktop.
A loop recognition benchmark implementation in D
/+
+ License: ISC
+
+ Ref.:
+ * https://github.com/camblomquist/loop-recognition-bench
+ * https://github.com/hundt98847/multi-language-bench
+ * https://blomqu.ist/posts/2025/loop-recognition/
+
+/
// source/app.d
module app;
import maoloops;
import std.stdio;
import std.exception;
int buildDiamond(MaoCFG cfg, int start) {
try {
BasicBlockEdge(cfg, start, start + 1);
BasicBlockEdge(cfg, start, start + 2);
BasicBlockEdge(cfg, start + 1, start + 3);
BasicBlockEdge(cfg, start + 2, start + 3);
return start + 3;
} catch (Exception e) {
stderr.writefln("Error in buildDiamond at start=%d: %s", start, e.msg);
throw e;
}
}
void buildConnect(MaoCFG cfg, int start, int end) {
try {
BasicBlockEdge(cfg, start, end);
} catch (Exception e) {
stderr.writefln("Error in buildConnect from %d to %d: %s", start, end, e.msg);
throw e;
}
}
int buildStraight(MaoCFG cfg, int start, int n) {
try {
foreach (i; 0 .. n) {
buildConnect(cfg, start + i, start + i + 1);
}
return start + n;
} catch (Exception e) {
stderr.writefln("Error in buildStraight at start=%d, n=%d: %s", start, n, e.msg);
throw e;
}
}
int buildBaseLoop(MaoCFG cfg, int from) {
try {
int header = buildStraight(cfg, from, 1);
int diamond1 = buildDiamond(cfg, header);
int d11 = buildStraight(cfg, diamond1, 1);
int diamond2 = buildDiamond(cfg, d11);
int footer = buildStraight(cfg, diamond2, 1);
buildConnect(cfg, diamond2, d11);
buildConnect(cfg, diamond1, header);
buildConnect(cfg, footer, from);
footer = buildStraight(cfg, footer, 1);
return footer;
} catch (Exception e) {
stderr.writefln("Error in buildBaseLoop at from=%d: %s", from, e.msg);
throw e;
}
}
void main() {
try {
stderr.writeln("Welcome to LoopTesterApp, D edition");
stderr.writeln("Constructing cfg...");
auto cfg = new MaoCFG;
stderr.writeln("Constructing lsg...");
auto lsg = new LoopStructureGraph;
stderr.writeln("Constructing Simple CFG...");
cfg.createNode(0); // top
// Simplified CFG: single loop
BasicBlockEdge(cfg, 0, 1);
BasicBlockEdge(cfg, 1, 2);
BasicBlockEdge(cfg, 2, 1); // Back-edge for loop
BasicBlockEdge(cfg, 2, 3); // Exit
cfg.createNode(3); // bottom
stderr.writeln("100 dummy loops");
foreach (i; 0 .. 100) {
try {
auto lsglocal = new LoopStructureGraph;
findHavlakLoops(cfg, lsglocal);
if (i % 10 == 0) {
stderr.writefln("Completed %d dummy loops", i);
}
} catch (Exception e) {
stderr.writefln("Error in dummy loop %d: %s", i, e.msg);
stderr.writeln("Stack trace:");
stderr.writeln(e.info);
throw e;
} catch (Error e) {
stderr.writefln("Fatal error in dummy loop %d: %s", i, e.msg);
stderr.writeln("Stack trace:");
stderr.writeln(e.info);
throw e;
}
}
stderr.writeln("Constructing CFG...");
int n = 3;
foreach (parlooptrees; 0 .. 10) {
cfg.createNode(n + 1);
buildConnect(cfg, 3, n + 1);
n++;
foreach (i; 0 .. 100) {
int top = n;
n = buildStraight(cfg, n, 1);
foreach (j; 0 .. 25) {
n = buildBaseLoop(cfg, n);
}
int bottom = buildStraight(cfg, n, 1);
buildConnect(cfg, n, top);
n = bottom;
}
buildConnect(cfg, n, 3);
}
stderr.writeln("Performing Loop Recognition\n1 Iteration");
int numLoops = findHavlakLoops(cfg, lsg);
stderr.writeln("Another 50 iterations...");
int sum = 0;
foreach (i; 0 .. 50) {
try {
auto lsglocal = new LoopStructureGraph;
sum += findHavlakLoops(cfg, lsglocal);
stderr.write(".");
} catch (Exception e) {
stderr.writefln("Error in iteration %d: %s", i, e.msg);
stderr.writeln("Stack trace:");
stderr.writeln(e.info);
throw e;
} catch (Error e) {
stderr.writefln("Fatal error in iteration %d: %s", i, e.msg);
stderr.writeln("Stack trace:");
stderr.writeln(e.info);
throw e;
}
}
stderr.writefln("\nFound %d loops (including artificial root node) (%d)", numLoops, sum);
lsg.calculateNestingLevel();
lsg.dump();
} catch (Exception e) {
stderr.writefln("Fatal error: %s", e.msg);
stderr.writeln("Stack trace:");
stderr.writeln(e.info);
exit(1);
} catch (Error e) {
stderr.writefln("Fatal error (Error): %s", e.msg);
stderr.writeln("Stack trace:");
stderr.writeln(e.info);
exit(1);
}
}
private void exit(int code) {
import core.stdc.stdlib : exit;
exit(code);
}
{
"name": "loop-recognition",
"description": "A loop recognition benchmark implementation in D.",
"version": "0.1.0",
"targetPath": "bin",
"authors": [
"Matheus Catarino França"
],
"dflags-dmd": [
"-verrors=context"
],
"dflags-ldc": [
"--verrors-context",
"-mcpu=native"
]
}
/+
+ License: ISC
+
+ Ref.:
+ * https://github.com/camblomquist/loop-recognition-bench
+ * https://github.com/hundt98847/multi-language-bench
+ * https://blomqu.ist/posts/2025/loop-recognition/
+
+/
// source/maoloops.d
module maoloops;
import std.container.array;
import std.container.rbtree;
import std.range;
import std.algorithm;
import std.exception;
import std.format;
import std.stdio;
import core.memory;
// BasicBlockEdge: Represents an edge between two basic blocks
struct BasicBlockEdge {
private BasicBlock from_;
private BasicBlock to_;
this(MaoCFG cfg, int fromName, int toName) {
enforce(cfg !is null, "CFG cannot be null");
from_ = cfg.createNode(fromName);
to_ = cfg.createNode(toName);
enforce(from_ !is null && to_ !is null, "Invalid nodes created");
from_.addOutEdge(to_);
to_.addInEdge(from_);
cfg.addEdge(this);
}
@property BasicBlock src() { return from_; }
@property BasicBlock dst() { return to_; }
}
// BasicBlock: A node in the CFG with in/out edges
class BasicBlock {
private Array!BasicBlock inEdges_, outEdges_;
private int name_;
this(int name) {
this.name_ = name;
}
@property auto in_edges() { return inEdges_[]; }
@property auto out_edges() { return outEdges_[]; }
@property size_t numPred() { return inEdges_.length; }
@property size_t numSucc() { return outEdges_.length; }
void addOutEdge(BasicBlock to) {
enforce(to !is null, "Cannot add null out-edge");
outEdges_.insertBack(to);
}
void addInEdge(BasicBlock from) {
enforce(from !is null, "Cannot add null in-edge");
inEdges_.insertBack(from);
}
int opCmp(const BasicBlock other) const {
return name_ - other.name_;
}
}
// MaoCFG: Control flow graph managing nodes and edges
class MaoCFG {
private BasicBlock[int] basicBlockMap_;
private BasicBlockEdge[] edgeList_;
private BasicBlock startNode_;
~this() {
foreach (name, node; basicBlockMap_) {
destroy(node);
}
}
BasicBlock createNode(int name) {
if (auto node = name in basicBlockMap_) {
return *node;
}
auto node = new BasicBlock(name);
basicBlockMap_[name] = node;
if (basicBlockMap_.length == 1) {
startNode_ = node;
}
return node;
}
void addEdge(BasicBlockEdge edge) {
edgeList_ ~= edge;
}
@property size_t numNodes() { return basicBlockMap_.length; }
@property BasicBlock startBasicBlock() { return startNode_; }
@property BasicBlock dst(BasicBlockEdge edge) { return edge.dst; }
@property BasicBlock src(BasicBlockEdge edge) { return edge.src; }
@property auto basicBlocks() { return basicBlockMap_.byValue; }
}
// SimpleLoop: Represents a loop with basic blocks and hierarchy
class SimpleLoop {
private RedBlackTree!BasicBlock basicBlocks_;
private RedBlackTree!SimpleLoop children_;
private SimpleLoop parent_;
private bool isRoot_;
private int counter_, nestingLevel_, depthLevel_;
this() {
basicBlocks_ = new RedBlackTree!BasicBlock;
children_ = new RedBlackTree!SimpleLoop;
}
void addNode(BasicBlock bb) {
enforce(bb !is null, "Cannot add null basic block");
basicBlocks_.insert(bb);
}
void addChildLoop(SimpleLoop loop) {
enforce(loop !is null, "Cannot add null child loop");
children_.insert(loop);
}
void dump() {
writefln("loop-%d, nest: %d, depth: %d", counter_, nestingLevel_, depthLevel_);
}
@property SimpleLoop parent() { return parent_; }
@property int nesting_level() { return nestingLevel_; }
@property int depth_level() { return depthLevel_; }
@property int counter() { return counter_; }
@property bool is_root() { return isRoot_; }
@property auto getChildren() { return children_[]; }
void set_parent(SimpleLoop p) {
enforce(p !is null, "Cannot set null parent");
parent_ = p;
p.addChildLoop(this);
}
void set_is_root() { isRoot_ = true; }
void set_counter(int value) { counter_ = value; }
void set_nesting_level(int level) {
nestingLevel_ = level;
if (level == 0) set_is_root();
}
void set_depth_level(int level) { depthLevel_ = level; }
int opCmp(const SimpleLoop other) const {
return counter_ - other.counter_;
}
}
// LoopStructureGraph: Manages the loop hierarchy
class LoopStructureGraph {
private SimpleLoop root_;
private SimpleLoop[] loops_;
private int loopCounter_;
this() {
root_ = new SimpleLoop;
root_.set_nesting_level(0);
root_.set_counter(loopCounter_++);
addLoop(root_);
}
~this() { killAll(); }
SimpleLoop createNewLoop() {
auto loop = new SimpleLoop;
loop.set_counter(loopCounter_++);
return loop;
}
void killAll() {
foreach (loop; loops_) {
destroy(loop);
}
loops_.length = 0;
}
void addLoop(SimpleLoop loop) {
enforce(loop !is null, "Cannot add null loop");
loops_ ~= loop;
}
void dump() {
dumpRec(root_, 0);
}
private void dumpRec(SimpleLoop loop, int indent) {
enforce(loop !is null, "Cannot dump null loop");
writefln("%*s", indent * 2, "");
loop.dump();
foreach (child; loop.getChildren) {
dumpRec(child, indent + 1);
}
}
void calculateNestingLevel() {
foreach (loop; loops_) {
if (loop.is_root || loop.parent) continue;
loop.set_parent(root_);
}
calculateNestingLevelRec(root_, 0);
}
private void calculateNestingLevelRec(SimpleLoop loop, int depth) {
enforce(loop !is null, "Cannot calculate nesting for null loop");
loop.set_depth_level(depth);
int maxNest = loop.nesting_level;
foreach (child; loop.getChildren) {
calculateNestingLevelRec(child, depth + 1);
maxNest = max(maxNest, 1 + child.nesting_level);
}
loop.set_nesting_level(maxNest);
}
@property size_t numLoops() { return loops_.length; }
@property SimpleLoop root() { return root_; }
}
// UnionFindNode: Union-find structure for loop detection
struct UnionFindNode {
private UnionFindNode* parent_;
private BasicBlock bb_;
private SimpleLoop loop_;
private int dfsNumber_;
void init(BasicBlock bb, int dfsNumber) @nogc {
assert(bb !is null, "Cannot initialize with null basic block");
parent_ = &this;
this.bb_ = bb;
this.dfsNumber_ = dfsNumber;
}
UnionFindNode* findSet() {
UnionFindNode* node = &this;
UnionFindNode*[] nodeList;
while (node != node.parent_) {
if (node.parent_ != node.parent_.parent_) {
nodeList ~= node;
}
node = node.parent_;
}
foreach (n; nodeList) {
n.parent_ = node.parent_;
}
return node;
}
void union_(UnionFindNode* b) @nogc {
assert(b !is null, "Cannot union with null node");
parent_ = b;
}
@property UnionFindNode* parent() { return parent_; }
@property BasicBlock bb() { return bb_; }
@property SimpleLoop loop() { return loop_; }
@property int dfs_number() { return dfsNumber_; }
void set_parent(UnionFindNode* p) @nogc {
assert(p !is null, "Cannot set null parent");
parent_ = p;
}
void set_loop(SimpleLoop l) {
enforce(l !is null, "Cannot set null loop");
loop_ = l;
}
}
// HavlakLoopFinder: Implements the Havlak loop detection algorithm
class HavlakLoopFinder {
private MaoCFG cfg_;
private LoopStructureGraph lsg_;
enum BasicBlockClass { BB_TOP, BB_NONHEADER, BB_REDUCIBLE, BB_SELF, BB_IRREDUCIBLE, BB_DEAD, BB_LAST }
enum { kUnvisited = -1, kMaxNonBackPreds = 32 * 1024 }
this(MaoCFG cfg, LoopStructureGraph lsg) {
enforce(cfg !is null && lsg !is null, "CFG and LSG cannot be null");
this.cfg_ = cfg;
this.lsg_ = lsg;
}
private bool isAncestor(int w, int v, int[] last) {
enforce(last !is null, "Last array cannot be null");
enforce(w >= 0 && w < last.length && v >= 0, "Invalid ancestor indices");
return w <= v && v <= last[w];
}
private int dfs(BasicBlock current, UnionFindNode[] nodes, ref int[BasicBlock] number, int[] depth, int currentId) {
enforce(current !is null && nodes !is null && depth !is null, "Invalid DFS inputs");
enforce(currentId >= 0 && currentId < nodes.length, "Invalid currentId");
nodes[currentId].init(current, currentId);
number[current] = currentId;
int lastId = currentId;
foreach (target; current.out_edges) {
enforce(target !is null, "Null target in out_edges");
if (target !in number || number[target] == kUnvisited) {
lastId = dfs(target, nodes, number, depth, lastId + 1);
}
}
enforce(number[current] >= 0 && number[current] < depth.length, "Invalid number[current]");
depth[number[current]] = lastId;
return lastId;
}
void findLoops() {
try {
if (!cfg_.startBasicBlock) {
stderr.writeln("No start basic block, exiting findLoops");
return;
}
int size = cast(int)cfg_.numNodes;
enforce(size >= 0, "Invalid CFG size");
stderr.writefln("Initializing DFS with %d nodes", size);
auto nonBackPreds = new RedBlackTree!int[size];
auto backPreds = new int[][size];
auto header = new int[size];
auto type = new BasicBlockClass[size];
auto last = new int[size];
auto nodes = new UnionFindNode[size];
int[BasicBlock] number;
// Initialize nonBackPreds
foreach (i; 0 .. size) {
nonBackPreds[i] = new RedBlackTree!int;
}
// Step a: Initialize and run DFS
foreach (bb; cfg_.basicBlocks) {
enforce(bb !is null, "Null basic block in CFG");
number[bb] = kUnvisited;
}
dfs(cfg_.startBasicBlock, nodes, number, last, 0);
// Step b: Identify back-edges and non-back-edges
stderr.writeln("Identifying back-edges...");
foreach (int w; 0 .. size) {
try {
stderr.writefln("Processing node w=%d", w);
header[w] = 0;
type[w] = BasicBlockClass.BB_TOP;
auto nodeW = nodes[w].bb;
if (!nodeW) {
type[w] = BasicBlockClass.BB_DEAD;
continue;
}
if (nodeW.numPred) {
foreach (nodeV; nodeW.in_edges) {
enforce(nodeV !is null, "Null nodeV in in_edges");
int v = nodeV in number ? number[nodeV] : kUnvisited;
if (v == kUnvisited) continue;
if (isAncestor(w, v, last)) {
backPreds[w] ~= v;
} else {
enforce(nonBackPreds[w] !is null, "Null nonBackPreds[w]");
nonBackPreds[w].insert(v);
}
}
}
} catch (Exception e) {
stderr.writefln("Error processing node w=%d: %s", w, e.msg);
throw e;
}
}
header[0] = 0;
// Step c: Process nodes in reverse DFS order
stderr.writeln("Processing nodes in reverse DFS order...");
foreach_reverse (int w; 0 .. size) {
try {
stderr.writefln("Reverse processing node w=%d", w);
UnionFindNode*[] nodePool;
auto nodeW = nodes[w].bb;
if (!nodeW) continue;
// Step d: Process back-edges
foreach (v; backPreds[w]) {
enforce(v >= 0 && v < size, "Invalid backPred v");
if (v != w) {
auto findSetResult = nodes[v].findSet;
enforce(findSetResult !is null, "Null findSet result");
nodePool ~= findSetResult;
} else {
type[w] = BasicBlockClass.BB_SELF;
}
}
auto worklist = nodePool.dup;
if (!nodePool.empty) {
type[w] = BasicBlockClass.BB_REDUCIBLE;
}
// Step e: Process non-back-edges
while (!worklist.empty) {
enforce(worklist.length > 0, "Empty worklist mismatch");
auto x = worklist[0];
worklist = worklist[1 .. $];
enforce(x !is null, "Null worklist node x");
enforce(x.dfs_number >= 0 && x.dfs_number < size, "Invalid dfs_number");
auto nonBackSize = nonBackPreds[x.dfs_number].length;
if (nonBackSize > kMaxNonBackPreds) {
lsg_.killAll();
stderr.writeln("Aborted due to excessive non-back predecessors");
return;
}
foreach (yId; nonBackPreds[x.dfs_number]) {
enforce(yId >= 0 && yId < size, "Invalid yId");
auto y = nodes[yId];
auto yDash = y.findSet;
enforce(yDash !is null, "Null yDash from findSet");
if (!isAncestor(w, yDash.dfs_number, last)) {
type[w] = BasicBlockClass.BB_IRREDUCIBLE;
nonBackPreds[w].insert(yDash.dfs_number);
} else if (yDash.dfs_number != w) {
if (!nodePool.canFind(yDash)) {
worklist ~= yDash;
nodePool ~= yDash;
}
}
}
}
// Create loop for SCC
if (!nodePool.empty || type[w] == BasicBlockClass.BB_SELF) {
auto loop = lsg_.createNewLoop;
enforce(loop !is null, "Null loop created");
nodes[w].set_loop(loop);
foreach (node; nodePool) {
enforce(node !is null, "Null node in nodePool");
enforce(node.dfs_number >= 0 && node.dfs_number < size, "Invalid node.dfs_number");
header[node.dfs_number] = w;
node.union_(&nodes[w]);
if (node.loop) {
node.loop.set_parent(loop);
} else {
loop.addNode(node.bb);
}
}
lsg_.addLoop(loop);
}
} catch (Exception e) {
stderr.writefln("Error in reverse processing node w=%d: %s", w, e.msg);
throw e;
} catch (Error e) {
stderr.writefln("Fatal error in reverse processing node w=%d: %s", w, e.msg);
stderr.writeln("Stack trace:");
stderr.writeln(e.info);
throw e;
}
}
stderr.writeln("Loop detection completed");
} catch (Exception e) {
stderr.writefln("Error in findLoops: %s", e.msg);
stderr.writeln("Stack trace:");
stderr.writeln(e.info);
throw e;
} catch (Error e) {
stderr.writefln("Fatal error in findLoops: %s", e.msg);
stderr.writeln("Stack trace:");
stderr.writeln(e.info);
throw e;
}
}
}
// External entry point
int findHavlakLoops(MaoCFG cfg, LoopStructureGraph lsg) {
try {
enforce(cfg !is null && lsg !is null, "CFG or LSG is null");
auto finder = new HavlakLoopFinder(cfg, lsg);
finder.findLoops();
return cast(int)lsg.numLoops;
} catch (Exception e) {
stderr.writefln("Error in findHavlakLoops: %s", e.msg);
stderr.writeln("Stack trace:");
stderr.writeln(e.info);
throw e;
} catch (Error e) {
stderr.writefln("Fatal error in findHavlakLoops: %s", e.msg);
stderr.writeln("Stack trace:");
stderr.writeln(e.info);
throw e;
}
}
@kassane
Copy link
Author

kassane commented Jun 3, 2025

build-types availables
dub build --print-builds
  Available build types:
      debug [default]
       plain
       release # disable asserts/contracts/boundchecker
       release-debug
       release-nobounds
# more... skip full-list

based on: stats.txt (by @camblomquist)

tokei

===============================================================================
 Language            Files        Lines         Code     Comments       Blanks
===============================================================================
 D                       2          636          554           16           66
 JSON                    1           15           15            0            0
===============================================================================
 Total                   3          651          569           16           66
===============================================================================

poop test:

# NOTE: all in release mode (x86_64|native)
poop -d 100 './cpp/build/LoopTesterApp' './rs/target/release/loop-recognition' './d/bin/loop-recognition' 
Benchmark 1 (3 runs): ./cpp/build/LoopTesterApp
  measurement          mean ± σ            min … max           outliers         delta
  wall_time          10.7s  ± 51.7ms    10.6s  … 10.7s           0 ( 0%)        0%
  peak_rss            187MB ± 80.1KB     187MB …  187MB          0 ( 0%)        0%
  cpu_cycles         47.5G  ±  149M     47.3G  … 47.6G           0 ( 0%)        0%
  instructions       45.4G  ±  417      45.4G  … 45.4G           0 ( 0%)        0%
  cache_references   2.50G  ± 4.57M     2.49G  … 2.50G           0 ( 0%)        0%
  cache_misses        336M  ± 1.29M      335M  …  337M           0 ( 0%)        0%
  branch_misses       310M  ± 77.0K      309M  …  310M           0 ( 0%)        0%
Benchmark 2 (3 runs): ./rs/target/release/loop-recognition
  measurement          mean ± σ            min … max           outliers         delta
  wall_time          5.82s  ±  124ms    5.73s  … 5.96s           0 ( 0%)        ⚡- 45.6% ±  2.0%
  peak_rss            152MB ±  698KB     152MB …  153MB          0 ( 0%)        ⚡- 18.6% ±  0.6%
  cpu_cycles         25.9G  ±  350M     25.7G  … 26.3G           0 ( 0%)        ⚡- 45.5% ±  1.3%
  instructions       50.7G  ±  704      50.7G  … 50.7G           0 ( 0%)        💩+ 11.5% ±  0.0%
  cache_references    932M  ± 4.71M      928M  …  937M           0 ( 0%)        ⚡- 62.7% ±  0.4%
  cache_misses        144M  ± 1.19M      142M  …  145M           0 ( 0%)        ⚡- 57.2% ±  0.8%
  branch_misses       210M  ±  295K      209M  …  210M           0 ( 0%)        ⚡- 32.3% ±  0.2%
Benchmark 3 (3 runs): ./d/bin/loop-recognition
  measurement          mean ± σ            min … max           outliers         delta
  wall_time          73.8s  ±  420ms    73.4s  … 74.3s           0 ( 0%)        💩+590.3% ±  6.3%
  peak_rss            423MB ± 81.3MB     376MB …  516MB          0 ( 0%)        💩+125.6% ± 69.6%
  cpu_cycles          176G  ± 1.14G      175G  …  177G           0 ( 0%)        💩+270.9% ±  3.9%
  instructions        139G  ± 7.17G      131G  …  143G           0 ( 0%)        💩+206.8% ± 25.3%
  cache_references   4.74G  ± 26.6M     4.71G  … 4.76G           0 ( 0%)        💩+ 89.6% ±  1.7%
  cache_misses        683M  ± 5.70M      677M  …  688M           0 ( 0%)        💩+103.5% ±  2.8%
  branch_misses      1.10G  ± 12.0M     1.09G  … 1.11G           0 ( 0%)        💩+256.6% ±  6.2%

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment