5-29.【性能分析与优化】Swift 并发(async/await、Task)下,ARC 行为与传统 GCD 有哪些性能差异?

0 阅读3分钟

一句话结论(先立住直觉)

Swift 并发(Task / async-await)下,ARC 的“单位成本”通常更低,但“调用次数”更容易被放大。
GCD 下,ARC 次数少,但每次更贵、更不可控。

所以:

  • 少量、粗粒度任务 → Task 更快
  • 大量、细粒度任务 → Task 更容易 ARC 爆炸

一、ARC 成本模型的根本不同

GCD:线程 + block

DispatchQueue.global().async {
    self.work()
}

ARC 行为

  • block 是 heap object
  • 捕获 self → retain
  • 跨线程 → retain/release 是 原子操作
  • 生命周期 = block 执行完

👉 ARC 次数少,但每次都很重


Swift 并发:Task + continuation

Task {
    await self.work()
}

ARC 行为

  • Task 是结构体 + runtime object
  • 捕获值 → 被复制进 Task context
  • suspend / resume → context retain/release
  • continuation 是 heap object

👉 ARC 更“细碎”,但单次更轻


二、关键差异 ①:ARC 调用“位置”不同

GCD

  • ARC 主要发生在:

    • block 创建
    • block 执行结束
retain self
↓
执行 block
↓
release self

生命周期清晰、边界明显


Swift 并发

ARC 分布在:

  • Task 创建
  • await suspend
  • await resume
  • continuation 链接
  • Task 完成
retain
suspend → retain
resume → release
await → retain

👉 一次 async 函数,可能触发多次 retain/release


三、关键差异 ②:值捕获 vs 引用捕获

GCD

  • 捕获的引用:

    • 明确 retain
    • 生命周期 = block

Swift 并发

  • 值类型会被复制
  • 引用类型会被 retain
  • 有时 struct 仍会被 box(context)
Task {
    let x = bigStruct   // copy
    await f(x)
}
  • 大 struct → copy 成本
  • 或被提升到 heap → ARC

👉 不是“用 struct 就没 ARC”


四、关键差异 ③:优化空间不同

GCD 的限制

  • block 是 escaping closure

  • 编译器假设:

    • 任意线程
    • 任意时间
  • 很多优化直接放弃


Swift 并发的优势

  • 语言级语义

  • 编译器知道:

    • suspend 点
    • isolation
    • actor 边界

👉 在理想结构下,Task 的 ARC 更容易被合并 / 消除

但前提是——结构得对


五、Task 中 ARC 特别容易“炸”的场景

🔥 1️⃣ 细粒度 Task

for item in items {
    Task {
        await process(item)
    }
}
  • 每个 Task:

    • context
    • continuation
    • ARC

👉 比 GCD 更容易炸


🔥 2️⃣ Task + 捕获 self

Task {
    await self.update()
}
  • self 被 capture
  • suspend 时 self 必须存活
  • retain 跨多个 await

🔥 3️⃣ actor hopping

await actorA.work()
await actorB.work()
  • 每次 hop:

    • retain context
    • retain self
    • message send

六、GCD 反而更“稳”的场景

✅ 长任务 + 少 await

DispatchQueue.global().async {
    heavyWork() // 50ms
}
  • ARC:1 次 retain + release
  • 无 suspend

✅ C / ObjC 桥接重的代码

  • async/await 反而增加 context

七、如何在 Swift 并发下“驯服 ARC”

🧠 1️⃣ 控制 Task 粒度(最重要)

Task 是重量级语义,不是 map 的替代品

// ❌
items.map {
    Task { await process($0) }
}

// ✅
await withTaskGroup(of: Void.self) { group in
    for chunk in chunks {
        group.addTask {
            for item in chunk {
                await process(item)
            }
        }
    }
}

🧠 2️⃣ 避免 Task 捕获 self

let service = self.service
Task {
    await service.run()
}

🧧 3️⃣ 减少 suspend 点

// ❌ 多个 await
await a()
await b()

// ✅ 合并
let (x, y) = await (a(), b())

🧠 4️⃣ 用 nonisolated / @inlinable 辅助优化

让编译器更容易消除 ARC


八、Instruments 中的典型信号

Swift 并发 ARC 问题

  • swift_task_alloc
  • swift_task_dealloc
  • swift_retain
  • swift_release
  • swift_continuation_*

九、最终总结(记住这张对比表)

维度GCDSwift 并发
ARC 次数
单次 ARC 成本
优化空间
易踩坑点跨线程 retain细粒度 Task
热路径适配需设计

最重要的一句话

Swift 并发不是“更便宜的 GCD”,而是“更聪明但更啰嗦的 runtime”。
如果你把它当 GCD 用,ARC 一定会惩罚你。