Have Your Efficiency, and Flexibility TooMetaprogramming Techniques For No-Compromise Codeby Nick SabalauskyFull source code for this article is available on GitHub, or can be downloaded here. View this article on a Single Page or Table of Contents:
First Attempt: Send Efficiency and Flexibility to Dr. Oop's Couples TherapyDr. Oop has had much success helping many couples overcome their differences. He's often the go-to guy for many programming difficulties, and for very good reason. After listening to our protagonists' story, he prescribes interfaces and subclassing. To avoid any need for multiple inheritance or code duplication (both are known to have problems), he'll also add in a touch of composition. From ex2_objectOriented.d: interface ISpinner { @property bool isSpinnable(); void spin(); } final class SpinnerStub : ISpinner { @property bool isSpinnable() { return false; } void spin() { // Do nothing } } final class Spinner : ISpinner { @property bool isSpinnable() { return true; } int spinCount; void spin() { spinCount++; // Spinning! Wheeee! } } abstract class Gizmo { this() { spinner = createSpinner(); } @property int numPorts(); void doStuff(); ISpinner spinner; ISpinner createSpinner(); } class OnePortGizmo : Gizmo { override ISpinner createSpinner() { return new SpinnerStub(); } private OutputPort[1] ports; override @property int numPorts() { return 1; } override void doStuff() { ports[0].zap(); } } class TwoPortGizmo : Gizmo { override ISpinner createSpinner() { return new SpinnerStub(); } private OutputPort[2] ports; override @property int numPorts() { return 2; } override void doStuff() { ports[0].zap(); ports[1].zap(); } } class MultiPortGizmo : Gizmo { 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; } override ISpinner createSpinner() { return new SpinnerStub(); } private OutputPort[] ports; override @property int numPorts() { return ports.length; } override void doStuff() { foreach(port; ports) port.zap(); } } final class SpinnyOnePortGizmo : OnePortGizmo { override ISpinner createSpinner() { return new Spinner(); } } final class SpinnyTwoPortGizmo : TwoPortGizmo { override ISpinner createSpinner() { return new Spinner(); } } final class SpinnyMultiPortGizmo : MultiPortGizmo { this(int numPorts) { super(numPorts); } override ISpinner createSpinner() { return new Spinner(); } }Oh dear God, what have we done?! Blech! Ok, calm down...Deep breaths now...Stay with me...Breathe...Breathe...Maybe it's not as bad as it seems. Maybe it'll be worth it. After all, it's technically flexible. Not pretty, but flexible. Maybe the efficiency will be good enough to make it a worthwhile compromise. Let's see... The code to test this version is almost the same as before so I won't show it here. But you can view it in ex2_objectOriented.d if you'd like. On my system, this takes 40 seconds and 11.3 MB. That's nearly twice the time and 10% more memory as before. Hmm, uhh...nope, no good. Well, that was a bust. So what went wrong? The problem is, object orientation involves some overhead. Polymorphism requires each instance of any Gizmo type to store some extra hidden data, which not only increases memory usage but also allows fewer Gizmos to fit into the cache. Polymorphism also means an extra indirection when calling a member function. This extra indirection can only sometimes be optimized away. Each Gizmo needs to be individually allocated (although it's possible to get around that in certain languages, including D, but it's still yet another thing to do). The by-reference nature of objects means the Gizmo arrays only contain pointers. Not only does that mean greater memory usage, but it can also decrease data locality (how "close together" related data is in memory) which leads to more cache misses. Using composition for the spin capability also decreased data locality, increased indirection, and increased memory usage. All things considered, we wound up doing the exact opposite of what we tried to do: Our attempts to decrease time and memory increased them instead, and also gave us less maintainable code. In many cases, the overhead of object orientation isn't really a big problem, so object orientation can be a very useful tool (although perhaps not so much in this case). But in highly performance-sensitive sections, the overhead can definitely add up and cause trouble. So for all the successes Dr. Oop has had, efficiency and flexibility are just too strongly opposed. Flexibility is left unhappy with the complexity required, and poor efficiency nearly had a heart attack! This time, Dr. Oop's solution just isn't quite what the patients has been hoping for. Oops, indeed. Programmers familiar with templated classes might be annoyed at this point that I've rejected the object oriented approach without considering the use of template classes. Those familiar with D are likely screaming at me, "Mixins! Mixins!" And then there's C++'s preprocessor, too. Well, frankly, I agree. Such things can certainly improve an object oriented design. But those are all forms of metaprogramming, which I haven't gotten to just yet. Besides, the main point I want to get across is this: Object orientation isn't a general substitute for metaprogramming and does have limitations in how well it can marry efficiency with flexibility. Next: Respecting the Classics: Old-School Handcrafting Table of Contents:
|