SWIFT 5.9

我正在try 同时对数组的一些非重叠子范围进行排序.到目前为止,我的方法看起来像这样:

// Not the real array setup, but sufficiently representative
let arraySize = 1_000_000
var someArray = [Int]()
someArray.reserveCapacity(arraySize)
for _ in 0..<arraySize {
    someArray.append(.random(in: 0...1_000_000))
}

// Let's say we want to break the array into 6 SubSequences of
// roughly equal size and sort each one
let divisions = 6
var divisionLength = arraySize / divisions
// Guarantee that the last division will be at least as large
// as the remainder of the array
if arraySize % divisions != 0 { divisionLength += 1 }

await withTaskGroup(of: Void.self) { taskGroup in
    var currentIndex = 0

    while currentIndex < someArray.endIndex {
        let sliceStart = currentIndex
        let sliceEnd = min(currentIndex + divisionLength, someArray.endIndex)
        currentIndex = sliceEnd

        taskGroup.addTask {
            // compilation error: mutation of captured var 'someArray'
            // in concurrently-executing code

            someArray[sliceStart..<sliceEnd].sort()
        }
    }

    await taskGroup.waitForAll()
}

我有一个潜在的怀疑,由于Swift处理可变值类型的方式,不存在一种机制来说"Trust Me Bro™"关于并发可变性.

这是基于我的理解,即使我将排序函数&amp;mutex抽象为它操作的范围,CoW也会使其他线程持有的引用无效.

尽管如此,我还是想确切地知道--是否存在某种并发安全覆盖来允许我这样做?或者,有没有完全不同的方法?

推荐答案

有两种基本方法:

  1. 你可以使原始数组不可变,从而解决不安全的并发访问.但是你会让每个任务返回一个新的sorted数组的相关切片.因为这些任务可以以随机顺序完成,你可以让任务组返回一个"division"的元组以及它的排序子数组,所以我们可以在最后整理结果:

    let arraySize = 60
    let array: [Int] = (0 ..< arraySize).map { _ in .random(in: 1000...9999) }
    
    var divisions = defaultDevisions
    var (divisionLength, remainder) = size.quotientAndRemainder(dividingBy: divisions)
    if remainder != 0 { divisionLength += 1 }
    (divisions, remainder) = size.quotientAndRemainder(dividingBy: divisionLength)
    if remainder != 0 { divisions += 1 }
    
    let result = await withTaskGroup(of: (Int, [Int]).self) { group in
        for division in 0 ..< divisions {
            let start = division * divisionLength
            let end = min((division + 1) * divisionLength, array.endIndex)
    
            group.addTask {
                (division, array[start..<end].sorted())
            }
        }
    
        let dictionary: [Int: [Int]] = await group.reduce(into: [:]) { $0[$1.0] = $1.1 }
        return (0 ..< divisions).flatMap { dictionary[$0]! }
    }
    

    这显然有更多的开销(无论是在空间上还是时间上).但它说明了一种常见的线程安全模式,即不可变值类型.

  2. 您可以放弃Array,转而使用不安全的指针/缓冲区,例如UnsafeMutableBufferPointer:

    let size = 60
    let pointer = UnsafeMutableBufferPointer<Int>.allocate(capacity: size)
    pointer.initialize(from: (0 ..< size).map { _ in .random(in: 1000 ... 9999) })
    
    defer {
        pointer.deinitialize()
        pointer.deallocate()
    }
    
    var divisions = defaultDevisions
    var (divisionLength, remainder) = size.quotientAndRemainder(dividingBy: divisions)
    if remainder != 0 { divisionLength += 1 }
    (divisions, remainder) = size.quotientAndRemainder(dividingBy: divisionLength)
    if remainder != 0 { divisions += 1 }
    
    await withTaskGroup(of: Void.self) { group in
        for division in 0 ..< divisions {
            let start = division * divisionLength
            let end = min((division + 1) * divisionLength, size)
    
            group.addTask {
                pointer[start..<end].sort()
            }
        }
    }
    

    但请注意,在使用不安全指针时,(A)您是在担保对此缓冲区的"不安全"访问;以及(B)当您使用完指针时,您要承担手动取消初始化和释放指针的责任.


对于傻笑和咧嘴一笑,我使用divisions计数20(M1 Ultra)对ArrayUnsafeMutableBufferPointer方法进行了基准测试,这是每个排序需要多少秒.为了比较,我还添加了串行实现的基准测试:

Array size Parallel array Parallel raw buffer Serial
64 0.000069 0.000051 0.000009
128 0.000095 0.000073 0.000022
256 0.000076 0.000050 0.000025
512 0.000068 0.000060 0.000018
1,024 0.000089 0.000059 0.000033
2,048 0.000156 0.000102 0.000124
4,096 0.000195 0.000146 0.000197
8,192 0.000220 0.000212 0.000456
16,384 0.000438 0.000233 0.000861
32,768 0.000573 0.000305 0.001704
65,536 0.000998 0.000579 0.003271
131,072 0.001121 0.000754 0.005821
262,144 0.002095 0.001288 0.011233
524,288 0.003953 0.002360 0.024545
1,048,576 0.008017 0.004527 0.052295
2,097,152 0.017985 0.009507 0.112165
4,194,304 0.031620 0.018867 0.242490
8,388,608 0.069403 0.039062 0.516160
16,777,216 0.116921 0.071195 1.094520
33,554,432 0.239149 0.154589 2.308661
67,108,864 0.507110 0.311157 4.903246
134,217,728 1.077651 0.626984 10.333774

或者,下面是使用Swift Collections Benchmark进行的类似分析.但是,虽然上表仅隔离了排序的性能,但下表包括随机值数组的准备,这使得串行和并行算法之间的总体性能差异要小得多.但它仍然传达着同样的基本故事:

enter image description here

显然,根据硬件、排序的复杂性等不同,这些结果会有所不同,但我从上面两组基准测试结果中得出几个结论:

  • 在我的硬件上,我必须有一个包含8k项的数组才会有任何差异(如果项更少,连续呈现实际上可以更快).

  • 我需要大约100多万个项目才能让用户看到差异.

  • 由于条目数少于一百万,每个线程上的工作量根本不足以抵消并行性带来的适度开销.

更广泛地说,我们可以得出这样的结论:

  • 数组方法看起来效率要低得多,但实际上并没有那么糟糕(并且具有简单得多的内存语义).

  • 对于实际排序,原始缓冲区指针技术可能会更快一些.(请注意,我上面的基准测试不包括将数组转换为不安全缓冲区的可能性,因此如果您从数组开始,这可能会抵消我们看到的任何好处.)

    我个人只使用这种缓冲区指针技术,如果已经在世界上的不安全缓冲区(例如,像素缓冲器),在这一点上这是非常高性能的.

  • 人们需要在每个线程上做足够的工作才能享受并行的好处;简单排序只有在数组很大(或者排序比较逻辑更复杂)时才能享受到显著的好处.

Swift相关问答推荐

NSWindow中以编程方式创建的TextField快捷方式与SwiftUI

如何在自定义视图中读取修改符子元素?

在Swift中如何用变量计算0.9的立方

TabView 无法在屏幕上正确显示

如何在 swiftui 中设置给定字符串类型日期的日期倒计时?

我怎样才能缩短(提高效率)这段代码?

在 struct 中的序列和非序列泛型函数之间进行 Select

RxSwift 提高性能

SwiftUI 视图的默认成员初始化器 VS 自定义初始化器

iOS SwiftUI - 无法转换 ObservedObject 类型的值

符合协议 - 错误?

ZStack 中的 ProgressView 与 View 的行为不同

任何使 ScrollView 像 List 一样执行更灵活的修饰符?

Xcode swift ui 错误KeyPathComparator仅在 macOS 12.0 或更高版本中可用

在 Swift for iOS 中沿圆形路径绘制文本

由于编译器中的内部保护级别,无法访问框架 init 中的公共 struct

Swift 3:日期与 NSDate?

如何发送静默推送通知有效负载

如何在 SwiftUI 中为删除和禁用配置 ContextMenu 按钮?

在 xcode8 中找不到 ModuleName-Swift.h 文件