13-2.【OC】【性能】objc_msgSend 的调用成本到底有多高?

0 阅读3分钟

关于 objc_msgSend 的调用成本,我们不能只看一个绝对的数值,而要从它的查找链路汇编优化以及与静态派发的对比三个维度来看。

简单来说:在缓存命中的情况下,它的开销极低(几纳秒);但在缓存未命中时,开销会呈指数级增长。


1. 核心成本:三级查找链路

objc_msgSend 的执行路径决定了它的成本波动:

  1. 第一级:快速路径(Fast Path)—— 缓存命中

    • 流程:直接在类的 cache_t 中通过哈希查找。
    • 成本:大约 2~5 个 CPU 周期
    • 底层优化:这部分是用纯汇编编写的,尽可能利用寄存器,避免了栈帧的开辟,性能极度接近 C 函数调用。
  2. 第二级:慢速路径(Slow Path)—— 查表

    • 流程:如果缓存没中,进入 lookUpImpOrForward。这时会遍历类的方法列表(method_list),如果还没中,就往父类找,直到 NSObject
    • 成本几十到几百个 CPU 周期。这涉及到二分查找或线性遍历,耗时大幅增加。
  3. 第三级:消息转发(Message Forwarding)

    • 流程:如果方法最终没找到,会触发 methodSignatureForSelector:forwardInvocation:
    • 成本数千个 CPU 周期。这会涉及到创建 NSInvocation 对象、内存堆栈的拷贝等极其沉重的操作。

2. 数量级对比

为了让你有直观的感受,我们可以对比不同调用方式在现代 CPU 上的平均耗时(示意):

调用方式耗时 (参考)成本说明
C 函数直接调用~0.5 ns只有一条 call 指令,编译器可内联。
Swift 静态派发~0.5 ns同 C 函数,编译期确定地址。
Swift V-Table 派发~1.1 ns查一次表,一次间接跳转。
objc_msgSend (Cache Hit)~2.6 ns汇编级哈希查找 + 跳转。
objc_msgSend (Cache Miss)~10-50+ ns涉及循环遍历、父类链追溯。
消息转发 (Forwarding)~1000+ ns涉及对象创建、运行时重写栈信息。

3. 为什么它看起来“很快”?

尽管 objc_msgSend 比 C 函数慢了约 5 倍,但在大多数 iOS 应用中,它并不会成为瓶颈,原因有二:

  • 硬件加持:现代 CPU 的分支预测器(Branch Predictor)非常强大。对于高频调用的方法,CPU 会预取缓存,使得哈希查找几乎瞬间完成。
  • 局部性原理:一个运行中的 App,90% 的调用都集中在 10% 的方法上。这意味着方法缓存的命中率通常在 95% 以上

4. 什么时候它的成本会成为“灾难”?

在以下场景中,objc_msgSend 的成本会显现出来:

  • 紧密循环(Tight Loops) :如果你在 for 循环中执行 100 万次简单的属性访问(Getter),objc_msgSend 的累积开销可能占到循环耗时的 50% 以上。
  • 缓存抖动(Cache Thrashing) :如果一个类有几千个方法,且被高频随机调用,会导致方法缓存频繁扩容、清空和重填,这时 CPU 会大量浪费在“慢速路径”上。

💡 优化建议

如果你通过 Instruments 发现某个高频方法的 objc_msgSend 耗时占比过高,可以尝试:

  1. 使用 static inline 函数封装逻辑。
  2. 获取 IMP 函数指针 并缓存,直接通过指针调用。
  3. 在 Swift 混编时,去掉不必要的 @objc,回归 静态派发