Interfaces vs Direct Calls in C#: What Happens Under the Hood?

As a C# developer, you often face the choice between using interfaces or virtual methods versus direct calls. Maybe you want the flexibility of dependency injection or the ability to override behavior later—you never know, right? I personally use interfaces a lot, but there are some important performance considerations to keep in mind. I'm talking here about release-mode not debug-mode. To read about release/debug-mode here are some (generated) infos about this.
Let’s take a simple example:
public interface ISystem {
int doit(int input);
}
public class Sys1 : ISystem {
public int doit(int input){
return input * 3 * input;
}
}
public class C {
Sys1 s1;
ISystem sys;
public C(){
s1 = new Sys1();
sys = s1;
}
public int direct(int i) {
int value1 = s1.doit(i);
return value1;
}
public int viaInterface(int i) {
int value1 = sys.doit(i);
return value1;
}
public int useThem() {
int v1 = direct(10);
int v2 = viaInterface(20);
int res = v1 + v2;
return res;
}
}
Here, I’m using the same object twice: once directly (direct
method using s1) and once via its interface (viaInterface
method using sys). What happens behind the scenes?
direct
method: The JIT compiler can inline the call todoit
because it knows exactly which method is being called. In fact, in release mode, the JIT often eliminates the call entirely by embedding the calculation directly. (Note: only simple methods that meet certain criteria are automatically inlined.)viaInterface
method: Since the call goes through an interface, the runtime must look up the actual method implementation in a virtual method table (vtable) and then call it. This introduces overhead due to the indirection.
Now, looking at the useThem
method:
- Since all variables and types are known and cannot change, the JIT can compute
v1 = direct(10)
at JIT compile time, replacing it with a constant. - On the other hand,
v2 = viaInterface(20)
cannot be inlined because it involves an interface call. The JIT emits code to look up the method address at runtime and call it indirectly.
This means that if you have a small, frequently called method hidden behind an interface, it can have a surprisingly large performance impact, especially inside tight loops.
When Can a Method Be Inlined?
The JIT compiler decides to inline a method based on several conditions, including:
- The method is small and simple (usually just a few instructions).
- The method is not virtual or called via an interface (unless the JIT can devirtualize it).
- The method does not contain complex control flow or exception handling.
- The call site is “hot” or frequently executed.
- The method is not too large to cause code bloat.
If these conditions are met, the JIT replaces the call with the method’s body, eliminating call overhead and enabling further optimizations.
Real-World Evidence
Here is a SharpLab.io output backing this up (disclaimer: I’m not an assembly expert, at least not since 8086 assembly days 😉). You can see how the direct call is optimized away, while the interface call remains an indirect call.

Final Thoughts
Interfaces provide great flexibility and are essential for many design patterns, but they come with a runtime cost. When performance matters, especially in hot paths or tight loops, consider whether you can use direct calls or other techniques to avoid interface dispatch overhead.
Disclaimer: I wrote the raw-version of the text, but let it smooth out via AI.