Have Your Efficiency, and Flexibility Too

Metaprogramming Techniques For No-Compromise Code

by Nick Sabalausky

Full source code for this article is available on GitHub, or can be downloaded here.

View this article on a Single Page or Multiple Pages

Table of Contents:

  1. Have Your Efficiency, and Flexibility Too
  2. First Attempt: Send Efficiency and Flexibility to Dr. Oop's Couples Therapy
  3. Respecting the Classics: Old-School Handcrafting
  4. Success at Dr. Metaprogramming's Clinic
  5. It Walks Like a Duck and Quacks Like a Duck...Kill It!
  6. Metaprogramming Plus: The Flexibility Enhancements
  7. The Last Remaining Elephant In The Room: Runtime Conversion
  8. Curtain Call

Respecting the Classics: Old-School Handcrafting

Object orientation may not have worked out well for efficiency, but efficient code has been around since long before objects became popular. How did they do it back then? With good old-fashioned handcrafting, of course. Time for Efficiency and Flexibility to pay a visit to the town elder...

After the elder introduces himself, Efficiency and Flexibility ask him to have a look at their problem.

"Eh? You want I should look at your problem?"
"Yes, we'd like you to help us out."
"Help on your problem right? You want I should look at?"
"Umm...yes..."
"Ok...Hi! I'm the town elder!"

Clearly this guy has a problem repeating himself. But eventually he pulls out his trusty oak-finished toolchain and gets to work. After what seems like an eternity later, he's done:

From ex3_handcrafted.d:

struct OnePortGizmo { static immutable isSpinnable = false; static immutable numPorts = 1; private OutputPort[numPorts] ports; void doStuff() { ports[0].zap(); } void spin() { // Do nothing } } struct TwoPortGizmo { static immutable isSpinnable = false; static immutable numPorts = 2; private OutputPort[numPorts] ports; void doStuff() { ports[0].zap(); ports[1].zap(); } void spin() { // Do nothing } } struct MultiPortGizmo { this(int numPorts) { if(numPorts < 1) throw new Exception("A portless Gizmo is useless!"); if(numPorts == 1 || numPorts == 2) throw new Exception("Wrong type of Gizmo!"); ports.length = numPorts; } static immutable isSpinnable = false; private OutputPort[] ports; @property int numPorts() { return ports.length; } void doStuff() { foreach(port; ports) port.zap(); } void spin() { // Do nothing } } struct SpinnyOnePortGizmo { static immutable isSpinnable = true; static immutable numPorts = 1; private OutputPort[numPorts] ports; void doStuff() { ports[0].zap(); } int spinCount; void spin() { spinCount++; // Spinning! Wheeee! } } struct SpinnyTwoPortGizmo { static immutable isSpinnable = true; static immutable numPorts = 2; private OutputPort[numPorts] ports; void doStuff() { ports[0].zap(); ports[1].zap(); } int spinCount; void spin() { spinCount++; // Spinning! Wheeee! } } struct SpinnyMultiPortGizmo { this(int numPorts) { if(numPorts < 1) throw new Exception("A portless Gizmo is useless!"); if(numPorts == 1 || numPorts == 2) throw new Exception("Wrong type of Gizmo!"); ports.length = numPorts; } static immutable isSpinnable = true; private OutputPort[] ports; @property int numPorts() { return ports.length; } void doStuff() { foreach(port; ports) port.zap(); } int spinCount; void spin() { spinCount++; // Spinning! Wheeee! } }

It certainly matches the old man's speech patterns, but just look at the careful attention to detail and workmanship! Two handmade single-port Gizmos, one spinny and one not. Two handmade double-port Gizmos. And even a couple general-purpose multi-port jobs. Let's take 'er for a...ahem...a spin...

On my system, that clocks in at 10.5 seconds and 9.4 MB. Hey, not bad! That's definitely an improvement over the original. It's twice as fast, and uses about 10% less memory.

The old guy's a bit eccentric, and his methods may be a bit out of date, but he really knows his stuff. Too bad the approach is too meticulous and error-prone to be useful for the rest of us mere mortals. Or for those of us working on modern large-scale high-complexity software.

I should point out that since the Gizmos are now separate types, with no common base type, they can no longer be stored all in one array. But that's not a big problem, we can just keep a separate array for each type. No big deal. And if we wanted, we could create a struct GizmoGroup that kept arrays of all the different gizmo types in a convenient little package. The town elder didn't actually make such a struct, but in any case, here's the updated UltraGiz:

From ex3_handcrafted.d:

struct UltraGiz { OnePortGizmo[] gizmosA; SpinnyOnePortGizmo[] gizmosB; TwoPortGizmo[] gizmosC; SpinnyTwoPortGizmo[] gizmosD; MultiPortGizmo[] gizmosE; SpinnyMultiPortGizmo[] gizmosF; int numTimesUsedSpinny; int numTimesUsedTwoPort; // Ok, technically this is a simple form of metaprogramming, so I'm // cheating slightly. But I just can't bring myself to copy/paste the // exact same function six times even for the sake of an example. void useGizmo(T)(ref T gizmo) { gizmo.doStuff(); gizmo.spin(); if(gizmo.isSpinnable) numTimesUsedSpinny++; if(gizmo.numPorts == 2) numTimesUsedTwoPort++; } void run() { StopWatch stopWatch; stopWatch.start(); // Create gizmos gizmosA.length = 10_000; gizmosB.length = 10_000; gizmosC.length = 10_000; gizmosD.length = 10_000; gizmosE.length = 5_000; gizmosF.length = 5_000; foreach(i; 0..gizmosE.length) gizmosE[i] = MultiPortGizmo(5); foreach(i; 0..gizmosF.length) gizmosF[i] = SpinnyMultiPortGizmo(5); // Use gizmos foreach(i; 0..10_000) { foreach(ref gizmo; gizmosA) useGizmo(gizmo); foreach(ref gizmo; gizmosB) useGizmo(gizmo); foreach(ref gizmo; gizmosC) useGizmo(gizmo); foreach(ref gizmo; gizmosD) useGizmo(gizmo); foreach(ref gizmo; gizmosE) useGizmo(gizmo); foreach(ref gizmo; gizmosF) useGizmo(gizmo); } writeln(stopWatch.peek.msecs, "ms"); assert(numTimesUsedSpinny == 25_000 * 10_000); assert(numTimesUsedTwoPort == 20_000 * 10_000); } }

In reality, specially handcrafting only-slightly-different versions is such a meticulous, repetitive maintenance nightmare that you'd normally only make one or two specially-tweaked versions, and leave the rest of the cases up to a general-purpose version. And even that can be a pain. So as happy as efficiency may be, flexibility is storming out of the room. We're getting close, but haven't succeeded yet. What we need is a better twist on this handcrafting approach...

Next: Success at Dr. Metaprogramming's Clinic

Table of Contents:

  1. Have Your Efficiency, and Flexibility Too
  2. First Attempt: Send Efficiency and Flexibility to Dr. Oop's Couples Therapy
  3. Respecting the Classics: Old-School Handcrafting
  4. Success at Dr. Metaprogramming's Clinic
  5. It Walks Like a Duck and Quacks Like a Duck...Kill It!
  6. Metaprogramming Plus: The Flexibility Enhancements
  7. The Last Remaining Elephant In The Room: Runtime Conversion
  8. Curtain Call