为什么 GCD、ObjC 和 Swift 之间的性能差距如此之大

Why is the performance gap between GCD, ObjC and Swift so large

OC:模拟器iPhoneSE iOS13; (60-80 秒)

    NSTimeInterval t1 = NSDate.date.timeIntervalSince1970;
    NSInteger count = 100000;

    for (NSInteger i = 0; i < count; i++) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            NSLog(@"op begin: %ld", i);
            self.idx = i;
            NSLog(@"op end: %ld", i);
            if (i == count - 1) {
                NSLog(@"耗时: %f", NSDate.date.timeIntervalSince1970-t1);
            }
        });
     }

Swift:模拟器iPhoneSE iOS13; (10-14 秒)

let t1 = Date().timeIntervalSince1970
let count = 100000
for i in 0..<count {
            DispatchQueue.global().async {
                print("subOp begin i:\(i), thred: \(Thread.current)")
                self.idx = i
                print("subOp end i:\(i), thred: \(Thread.current)")
                if i == count-1 {
                    print("耗时: \(Date().timeIntervalSince1970-t1)")
                }
            }
}

我之前的工作是用oc写代码。最近学会了使用swift。我对通用 GCD 的巨大性能差距感到惊讶

tl;博士

您很可能正在进行调试构建。在 Swift 中,调试版本会进行各种安全检查。如果你做一个发布版本,它会关闭那些安全检查,并且性能在很大程度上与 Objective-C 再现没有区别。


几点观察:

  1. 它们与 index 的交互都不是线程安全的,即您与此 属性 的交互未同步。如果您要从多个线程更新 属性,则必须同步您的访问(使用锁、串行队列、reader-writer 并发队列等)。

  2. 在您的 Swift 代码中,您是在进行发布构建吗?在调试版本中,调试版本中存在安全检查,这些检查未使用 Objective-C 再现执行。对两者进行“发布”构建以比较同类。

  3. 您的 Objective-C 再现使用 DISPATCH_QUEUE_PRIORITY_HIGH,但您的 Swift 迭代使用 .default 的 QoS。我建议使用 .userInteractive.userInitiated 的 QoS 进行比较。

  4. 全局队列是并发队列。因此,检查 i == count - 1 不足以了解是否所有当前任务都已完成。通常我们会使用调度组或 concurrentPerform/dispatch_apply 来知道它们何时完成。

  5. FWIW,不建议将 100,000 个任务分派到全局队列,因为您将很快耗尽工作线程。不要做这种线程爆炸。如果这样做,您的应用程序可能会出现意外锁定。在 Swift 中,我们将使用 concurrentPerform。在 Objective-C 中,我们将使用 dispatch_apply.

  6. 你在 Objective-C 中做 NSLog,在 Swift 中做 print。这些不是一回事。如果你想比较性能,我建议在两者中都做 NSLog

  7. 性能测试时,我可能会建议使用单元测试的measure例程,它会重复多次。

无论如何,在纠正所有这些问题时,Swift 代码的时间与 Objective-C 性能在很大程度上没有区别。

以下是我使用的例程。在 Swift:

class SwiftExperiment {

    // This is not advisable, because it suffers from thread explosion which will exhaust
    // the very limited number of worker threads.

    func experiment1(completion: @escaping (TimeInterval) -> Void) {
        let t1 = Date()
        let count = 100_000
        let group = DispatchGroup()

        for i in 0..<count {
            DispatchQueue.global(qos: .userInteractive).async(group: group) {
                NSLog("op end: %ld", i);
            }
        }

        group.notify(queue: .main) {
            let elapsed = Date().timeIntervalSince(t1)
            completion(elapsed)
        }
    }

    // This is safer (though it's a poor use of `concurrentPerform` as there's not enough
    // work being done on each thread).

    func experiment2(completion: @escaping (TimeInterval) -> Void) {
        let t1 = Date()
        let count = 100_000

        DispatchQueue.global(qos: .userInteractive).async() {
            DispatchQueue.concurrentPerform(iterations: count) { i in
                NSLog("op end: %ld", i);
            }
            let elapsed = Date().timeIntervalSince(t1)
            completion(elapsed)
        }
    }
}

以及等效的 Objective-C 例程:

// This is not advisable, because it suffers from thread explosion which will exhaust
// the very limited number of worker threads.

- (void)experiment1:(void (^ _Nonnull)(NSTimeInterval))block {
    NSDate *t1 = [NSDate date];
    NSInteger count = 100000;
    dispatch_group_t group = dispatch_group_create();

    for (NSInteger i = 0; i < count; i++) {
        dispatch_group_async(group, dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
            NSLog(@"op end: %ld", i);
        });
    }
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSTimeInterval elapsed = [NSDate.date timeIntervalSinceDate:t1];
        NSLog(@"耗时: %f", elapsed);
        block(elapsed);
    });
}

// This is safer (though it's a poor use of `dispatch_apply` as there's not enough
// work being done on each thread).

- (void)experiment2:(void (^ _Nonnull)(NSTimeInterval))block {
    NSDate *t1 = [NSDate date];
    NSInteger count = 100000;

    dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
        dispatch_apply(count, dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^(size_t index) {
            NSLog(@"op end: %ld", index);
        });

        NSTimeInterval elapsed = [NSDate.date timeIntervalSinceDate:t1];
        NSLog(@"耗时: %f", elapsed);
        block(elapsed);
    });
}

我的单元测试:

class MyApp4Tests: XCTestCase {

    func testSwiftExperiment1() throws {
        let experiment = SwiftExperiment()

        measure {
            let e = expectation(description: "experiment1")

            experiment.experiment1 { elapsed in
                e.fulfill()
            }

            wait(for: [e], timeout: 1000)
        }
    }

    func testSwiftExperiment2() throws {
        let experiment = SwiftExperiment()

        measure {
            let e = expectation(description: "experiment2")

            experiment.experiment2 { elapsed in
                e.fulfill()
            }

            wait(for: [e], timeout: 1000)
        }
    }

    func testObjcExperiment1() throws {
        let experiment = ObjectiveCExperiment()

        measure {
            let e = expectation(description: "experiment1")

            experiment.experiment1 { elapsed in
                e.fulfill()
            }

            wait(for: [e], timeout: 1000)
        }
    }

    func testObjcExperiment2() throws {
        let experiment = ObjectiveCExperiment()

        measure {
            let e = expectation(description: "experiment2")

            experiment.experiment2 { elapsed in
                e.fulfill()
            }

            wait(for: [e], timeout: 1000)
        }
    }
}

结果是