零开销抽象

抽象层在编译期就被展开、消除,运行时不会额外增加指令、不会引入间接跳转、没有额外内存开销,高级抽象代码编译后等价于手写的底层代码。

以C#、C++举例

  • C++ 模板:编译期实例化,每种不同类型生成一份独立机器码,没有运行时类型分发
  • C# 泛型:值类型会单独实例化,但引用类型共享同一份 IL 实现,依赖运行时类型检查、虚方法表,存在开销

C# 接口示例代码

using System;

interface ICalc
{
    int Add(int a, int b);
}

struct CalcImpl : ICalc
{
    public int Add(int a, int b)
    {
        return a + b;
    }
}

// 泛型方法,接收接口
static int Run<T>(T calc, int x, int y) where T : ICalc
{
    return calc.Add(x, y);
}

public static void Main()
{
    CalcImpl c = new CalcImpl();
    Console.WriteLine(Run(c, 1, 2));
}

对应的字节码
.method private hidebysig static int32  Run<T>(!!T calc, int32 x, int32 y) cil managed
{
  .paramtype !T
  .maxstack  3
  .locals init ([0] class ICalc)
  // 装箱, 把值类型 T 转为 ICalc 引用
  box !!T
  stloc.0
  ldloc.0
  ldloc.1
  ldloc.2
  // 接口虚调用 callvirt,间接跳转
  callvirt instance int32 ICalc::Add(int32, int32)
  ret
}

C++接口示例代码

#include <iostream>

struct CalcImpl
{
    int Add(int a, int b) const
    {
        return a + b;
    }
};


template<typename T>
int Run(const T& calc, int x, int y)
{
    return calc.Add(x, y);
}

int main()
{
    CalcImpl c;
    std::cout << Run(c, 1, 2);
    return 0;
}

ASM:

main:
        mov     esi, 2
        mov     edi, 1
        call    Run<CalcImpl>
        mov     edi, eax
        call    std::basic_ostream<char>::operator<<(int)
        xor     eax, eax
        ret

Run<CalcImpl>:
        lea     eax, [rdi+rsi]  ; 直接 a+b,无间接跳转
        ret

C#: callvirt 就是虚调用,运行时查表;box 在堆分配内存,这就是抽象带来的运行时开销。

C++:编译器实例化 Run<CalcImpl>,编译期直接展开;calc.Add 是静态绑定(静态调用),没有虚表、没有间接跳转;没有堆分配、没有装箱;最终生成的机器码,等价于直接手写 return 1+2;


C# 迭代器示例

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<int> list = new List<int> { 1, 2, 3, 4 };
        int sum = 0;
        foreach (var num in list)
        {
            sum += num;
        }
        Console.WriteLine(sum);
    }
}

---

List<int> list = new List<int>{1,2,3,4};
int sum = 0;
List<int>.Enumerator enumerator = list.GetEnumerator();
while (enumerator.MoveNext())
{
    sum += enumerator.Current;
}
enumerator.Dispose();


---


.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  .maxstack  3
  .locals init ([0] class [mscorlib]System.Collections.Generic.List`1<int32> list,
           [1] int32 sum,
           [2] valuetype [mscorlib]System.Collections.Generic.List`1<int32>/Enumerator enumerator,
           [3] bool V_3)
  // 创建List
  newobj instance void class [mscorlib]System.Collections.Generic.List`1<int32>::.ctor()
  stloc.0
  // Add元素省略
  ldloc.0
  callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1<int32>/Enumerator class [mscorlib]System.Collections.Generic.List`1<int32>::GetEnumerator()
  stloc.2
  br.s       IL_002a
IL_0024:
  ldloca.s   enumerator
  call instance int32 valuetype [mscorlib]System.Collections.Generic.List`1<int32>/Enumerator::get_Current()
  add
  stloc.1
IL_002a:
  ldloca.s   enumerator
  call instance bool valuetype [mscorlib]System.Collections.Generic.List`1<int32>/Enumerator::MoveNext()
  stloc.3
  ldloc.3
  brtrue.s   IL_0024
  // Dispose
  ldloca.s   enumerator
  constrained. valuetype [mscorlib]System.Collections.Generic.List`1<int32>/Enumerator
  callvirt instance void [mscorlib]System.IDisposable::Dispose()
  
  // Console.WriteLine省略
  ret
}


1. constrained. + callvirt:虚调用,用来调用 IDisposable.Dispose()。走虚调用分支
2. 语法糖强制遵循 IEnumerator,抽象接口是固定规范;
3. 把 List<int>`赋值给 IEnumerable<int>变量再 foreach:
IEnumerable<int> seq = new List<int>{1,2,3};
foreach(var n in seq) sum += n;
此时 GetEnumerator()返回的枚举器 会装箱到堆上,产生 GC 分配,开销变大
C# foreach 是 基于接口的抽象,抽象层在运行时存在。一旦向上转型为接口,就会装箱。

C++迭代器示例

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> vec = {1,2,3,4};
    int sum = 0;
    for(auto num : vec)
    {
        sum += num;
    }
    std::cout << sum;
    return 0;
}

---

auto &&range = vec;
auto it = range.begin();
auto end = range.end();
for(; it != end; ++it)
{
    auto num = *it;
    sum += num;
}

---

main:
        push    rbx
        mov     edi, 16
        call    operator new
        mov     DWORD PTR [rax], 1
        mov     DWORD PTR [rax+4], 2
        mov     DWORD PTR [rax+8], 3
        mov     DWORD PTR [rax+12], 4
        mov     esi, 10
        mov     rdi, rax
        call    std::basic_ostream<char>::operator<<(int)
        mov     rdi, rax
        call    std::basic_ostream<char>::flush()
        xor     eax, eax
        pop     rbx
        ret

std::vector::iterator 是模板类型,不继承任何接口类
迭代器对象很小,栈分配,无堆分配
抽象(迭代器、range-for 语法)在编译期消失;运行时代码等价于手写指针遍历数组,零开销抽象

  • 你没用到的东西,你不需要为之付出代价(没有隐藏的运行时开销)。
  • 你用到的东西,你自己手写汇编代码也不可能做得更好。

C#的问题

  1. 边界检查 (Bounds Checking): 每次你访问 C# 数组 arr[i],CLR 都会偷偷检查 i 是否越界。这保护了程序,但消耗了 CPU 周期。C++ 默认不检查,如果你要极致速度,它就给你极致速度。
  2. 虚函数表 (VTable): C# 的方法默认很多是虚的(或者说为了多态性做了很多准备)。C++ 除非你显式声明 virtual,否则函数调用就是一次简单的地址跳转。
  3. 垃圾回收 (GC): 为了管理内存,C# 对象头里必须包含一些元数据(比如同步块索引、类型指针)。每个对象都比 C++ 的结构体“胖”那么一点。

零抽象成本(Zero-Cost Abstraction) 是一种核心设计理念,指 高级语言特性(如类、模板、RAII 等)在运行时不会引入额外开销。这意味着开发者可以编写高可读性、高维护性的代码,同时享受与手写底层代码相同的性能。

零抽象成本的核心是 “不为未使用的功能付费”:

– 编译时优化:抽象逻辑在编译期间被处理,运行时无额外开销。

– 等价机器码:抽象生成的代码与手动优化的底层代码效率相同。

– 性能可控:开发者无需为了性能牺牲代码结构。


C++的实现手段:

constexpr

inline

模板元编程

RAII

移动语义

编译时多态