0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Swift6.2】nonisolated(nonsending)でActor hopを排除してUIレスポンスを高速化する

Last updated at Posted at 2025-07-27

はじめに

Swift Concurrencyが正式登場して数年が経ち、async/awaitTaskactorを採用したアプリも珍しくなくなりました。
しかし「Actorを使ったらUIがカクついた」ことはありませんか?

大きな要因として考えられるのがActor hopです。Actorのプロパティにアクセスするたびに実行コンテキストが呼び出し元とActorを行き来し、この時にわずかなオーバーヘッドが発生します。
UIスクロールやリアルタイムストリーム処理ではこのわずかなオーバーヘッドが蓄積し、UIがカクつく場合があります。

Swift 6.2 (Xcode 26)に追加された nonisolated(nonsending) は、呼び出し元ActorのExecutorをそのまま再利用できるため、Actor hopを原理的にゼロにできます。

Actorを使ったらパフォーマンスが出ない、Xcode 26の新機能を追いかけたいという方はぜひ参考にしてみてください。

Actor hopとは

Actor内部状態にアクセスすると呼び出し元ExecutorからActor用Executorへとコンテキストスイッチ(hop)が発生します。1回は数µs〜数十µsでも、UIスクロールや大量ループでは合算レイテンシがボトルネックになります。

3つの実行モードとhopの違い

実行モード 実行場所 hopする? Sendable制約
①actor-isolated そのActor専用シリアルExecutor 外部から呼ぶと必ずhop なし
②旧nonisolated Actor外で動く共通スレッドプール*1 毎回hop あり
③新nonisolated(nonsending) 呼び出し元Actorと同じExecutor hopなし なし
MainActor ─┬─① hop         UIが詰まりがち
           ├─② hop         毎回外へ移動
           └─③ hopなし★    UI上で完結

*1 共通スレッドプール=SwiftRuntimeが管理する並列実行環境。公式には“global concurrent executor”と呼ばれ、APIglobalConcurrentExecutorが用意されている。

Xcode 26で試すにあたって

Xcode 26にてnonisoated(nonsending) by Defaultフラグが YES の場合、nonisolatedは原則呼び出し元Actorと同じExecutorで動作します。
本記事ではこのフラグの挙動については詳しく触れません。
興味がある方は以下の記事が参考になりましたのでご覧ください。

本記事で紹介するコードはこのフラグの値に関係なく動作するよう以下のように記載しています。

旧挙動 = @concurrent nonisolated
新挙動 = nonisolated(nonsending)

サンプルベンチマーク

簡単なサンプルコードを用意し、①actor-isolated, ②旧nonisolated, ③新nonisolated(nonsending)のパフォーマンスを比較してみました。

Cache.swift
/// 1KBの疑似フレーム
/// ※ 実アプリならUIImageやDataなどを想定
struct Frame {
    // 0で埋めた1KBのバイト列を保持するだけ
    let bytes = [UInt8](repeating: 0, count: 1024)
}

actor Cache {
    private let frame = Frame()
    
    /// actor-isolated (hopあり)
    func readWithActorIsolated() -> Frame { frame }
    /// 旧nonisolated (hopあり)
    @concurrent
    nonisolated func readWithConcurrent() async -> Frame { frame }
    /// nonisolated(nonsending) (hopなし)
    nonisolated(nonsending) func readWithNonsending() async -> Frame { frame }
}

上記のようなActorを用意し、以下のViewから1万回呼び出してみます。

ContentView.swift
struct ContentView: View {
    @State private var result = "(未計測)"

    var body: some View {
        VStack(spacing: 20) {
            Text("Actor Hop Benchmark")
                .font(.headline)

            // 計測結果ラベル
            Text(result)
                .font(.system(.body, design: .monospaced))

            // ボタンを押すと並列タスクでベンチを実行
            Button("10,000回読み込みを計測") {
                Task.detached { await runBenchmark() }
            }
            .buttonStyle(.borderedProminent)
        }
        .padding()
    }

    /// 関数をそれぞれ1万回実行し、計測結果を`@State`変数へ反映
    @MainActor
    private func runBenchmark() async {
        // actor-isolated (hopあり)の処理時間
        let timeActorIsolated  = await measure { await Cache().readWithActorIsolated()  }
        // 旧nonisolated (hopあり)の処理時間
        let timeConcurrent = await measure { await Cache().readWithConcurrent() }
        // nonisolated(nonsending) (hopなし)の処理時間
        let timeNonsending = await measure { await Cache().readWithNonsending() }

        // ラベルを更新
        result = """
        actor-isolated: \(String(format: "%.2f", timeActorIsolated )) ms
        旧nonisolated: \(String(format: "%.2f", timeConcurrent )) ms
        nonisolated(nonsending): \(String(format: "%.2f", timeNonsending)) ms
        """
    }

    /// 与えられたクロージャを1万回`await`し、経過時間をmsで返す
    private func measure(_ work: @escaping () async -> Frame) async -> Double {
        let start = DispatchTime.now()
        // 処理を1万回呼び出し
        for _ in 0..<10000 { _ = await work() }
        let end = DispatchTime.now()
        return Double(end.uptimeNanoseconds - start.uptimeNanoseconds) / 1000000
    }
}

ベンチマーク結果

nonisolated(nonsending)約10倍高速だということがわかりました。

実行環境

  • MacBook Pro 13-inch, 2018
    • 2.7 GHz クアッドコアIntel Core i7
    • MacOS Sequoia 15.5(24F74)
  • Xcode 26.0 beta 2 (17A5241o)
  • Simulator 26.0 (1051)
    • iPhone 16 Plus iOS 26.0 (23A5276e)

実行モードの使いどころの指針

目的 実行モード 理由
同じ実行コンテキストで回数の多い軽量処理を最小コストで回したい nonisolated(nonsending) 呼び出し元Executorをそのまま使うためhopなし・Sendable確認も不要
CPUコアをフルに使い並列化したい(重い計算・I/Oなど) @concurrent nonisolated Actor外の並列スレッドプールに逃がして実行、バックグラウンド向き
共有の可変状態を安全に操作したい actor-isolated 専用シリアルExecutorで順序・排他を自動保証

まとめ

  • nonisolated(nonsending)によるActor hop排除で多発箇所では大幅に高速化することが期待できる
  • 旧挙動が必要な箇所には@concurrentを明示しよう
  • ベンチマークを確認しつつ安全に移行しましょう

参考

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?