Skip to main content

Command Palette

Search for a command to run...

Who’s Burning Your CPU Cycles?

Compile-Time vs Run-Time Polymorphism

Published
5 min read
Who’s Burning Your CPU Cycles?
H
Greetings! I'm a dynamic computer engineer passionate about developing applications and building projects. I love exploring new technologies and stay up-to-date with global trends.

So, you’re a developer. You’ve been told polymorphism is this magical wand that makes your code flexible, extensible, and architecturally pure.

But let’s be honest — nobody told you it also makes your CPU sweat like it’s running a marathon in Bangalore summer. 😂

Welcome to the dark side: understanding the overhead of polymorphism.


🔸 First Things First: What the Heck is “Overhead”?

Overhead is just a fancy way of saying:

👉 “The extra hoops your CPU has to jump through just because you wrote pretty object-oriented code instead of ugly but blazing-fast procedural code.”

Here’s how overhead manifests in .NET:

Type of OverheadTranslation in Dev-Speak
🔁 Extra CPU cycles“Why does my loop suddenly run 40% slower?”
🧠 Memory usage“Why does every class carry around a little backpack of metadata?”
🔍 Indirection“Why do I need to follow 3 pointers just to call Speak()?”
🚫 Cache misses“Why does my CPU forget what it was doing?”
🧹 GC stress“Why is the garbage collector having a meltdown?”

🟢 Compile-Time Polymorphism: The Straight-A Student

Ah, compile-time polymorphism — aka method overloading.

It’s like that kid in class who always does their homework early and hands it in neatly stapled. The compiler knows exactly what’s going on. No drama.

Example

class Calculator {
    public int Add(int a, int b) => a + b;
    public double Add(double a, double b) => a + b;
}

var result = new Calculator().Add(1, 2);

Under the Hood

  • IL: call (direct call, no questions asked)

  • JIT: “Yep, I know this method. Let me bake it into machine code.”

  • CPU: “Finally, something easy. Add two numbers, go brrrr.”

Result

  • ✅ Fewer CPU instructions

  • ✅ Great cache locality

  • ✅ No pointer lookups

  • ✅ No runtime drama

Basically: fast, efficient, zero gossip.


🔴 Run-Time Polymorphism: The Drama Queen

Now let’s meet runtime polymorphism (virtual/abstract/interface calls).

This one doesn’t decide anything until the last minute. It’s that friend who says “I’ll let you know” when you ask if they’re coming for trip.

Example

abstract class Animal {
    public abstract void Speak();
}

class Dog : Animal {
    public override void Speak() => Console.WriteLine("Bark");
}

Animal a = new Dog();
a.Speak();  // runtime decision

Under the Hood

  1. a points to a Dog.

  2. IL: callvirt — because the compiler shrugs: “I don’t know, figure it out later.”

  3. CLR: “Okay, let’s open the vtable (virtual method table).”

  4. CPU: “Cool, let me follow this pointer, then another pointer, then maybe another… oh crap, cache miss.”

  5. Finally lands at Dog.Speak().

Result

  • ❌ More instructions

  • ❌ Pointer indirection → cache misses

  • ❌ Slightly bigger memory footprint (method tables everywhere)

  • ❌ Harder for JIT to optimize

  • ❌ Branch prediction crying in the corner

Basically: slow, moody, but soooo flexible.


🧹 GC & Memory Drama

Does polymorphism itself summon the garbage collector? Not really.
But guess what patterns love runtime polymorphism?

  • Factories (heap allocations galore)

  • Strategy patterns (heap allocations galore)

  • Decorators (heap allocations galore)

  • Interfaces on value types (boxing → heap allocations galore)

So yes, indirectly: your love of runtime polymorphism leads to more heap pressure, and GC wakes up like “who filled the room with empty pizza boxes again?”


🔬 IL & Assembly — The Smoking Gun

FeatureCompile-Time PolyRuntime Poly
IL Instructioncallcallvirt
JIT WorkEasy direct callNeeds vtable lookup
Resolution TimeCompile-timeRun-time
CPU OverheadMinimalExtra jumps + cache misses
MemoryTiny metadataMethod tables for each type

📊 Benchmarks (Yes, I Actually Timed This)

// Direct
public int Sum(int a, int b) => a + b;

// Virtual
public virtual int Sum(int a, int b) => a + b;

1 billion calls later:

MethodTime
Direct call~200 ms
Virtual call~250–300 ms

That’s a 20–40% hit in tight loops. Which is fine… unless you’re building a trading engine, a game engine, or anything that ends in “engine.” 🚗💨


For those who didn’t understand, Because nothing explains CPU instructions better than… dating.


🟢 Compile-Time Polymorphism = Arranged Marriage

  • Your parents (compiler) decide everything at compile time.

  • “This is your method, this is your spouse. Done. No runtime surprises.”

  • You get:

    • ✅ Direct resolution (no swiping, no lookup tables).

    • ✅ Predictable performance.

    • ✅ Stable, boring, reliable.

But…

  • ❌ Zero flexibility.

  • ❌ You can’t “swap implementations” halfway through.

So yes, fast and efficient, but maybe not so fun.


🔴 Run-Time Polymorphism = Tinder Swiping

  • You don’t know who you’ll end up with until runtime.

  • Every time you call Speak(), you’re basically swiping left/right on the vtable.

  • CPU: “Wait, is this Dog? Cat? Llama? Let’s look it up.”

  • You get:

    • ✅ Flexibility — new matches (classes) can appear any time.

    • ✅ Extensibility — just add more types, keep swiping.

    • ✅ Excitement — runtime keeps it spicy.

But…

  • ❌ More steps.

  • ❌ More memory overhead.

  • ❌ Sometimes cache misses = awkward dates.

So yes, flexible and modern, but your CPU is paying for dinner every time.


🎯 So… Which One Should You Use?

  • If you want raw speed → go with compile-time polymorphism.

  • If you want flexibility, testability, and SOLID points on your résumé → runtime polymorphism it is.

Think of it like this:

  • Compile-time poly is a sports car: direct, fast, but not flexible.

  • Runtime poly is an SUV: slower, bulkier, but can handle every weird business requirement your PM throws at you.


🧠 Final Wisdom

“Abstraction always has a cost. You either pay in CPU cycles or in your sanity maintaining spaghetti code. Choose your poison.”