5

There's quite a few threads on this however Using Grand Central Dispatch in Swift to parallelize and speed up “for" loops? uses Swift <3.0 code and I can't get the equivalent to work in 3 (see code). Process Array in parallel using GCD uses pointers and it gets a bit ugly so I'm going to assert right here that I'm looking for the nice Swift 3 way to do it (as efficiently as possible of course). I also heard groups are slow (?) maybe someone can confirm that. I couldn't get groups to work either.

Here is my implementation of a striding parallel map function (in an extension of Array). It want's to execute on the global queue so as to not block the UI. It may be that the concurrent bit doesn't need to be in the scope, only the remainder loop.

extension Array {

    func parallelMap<R>(striding n: Int, f: @escaping (Element) -> R, completion: @escaping ([R]) -> ()) {
        let N = self.count
        var res = [R?](repeating: nil, count: self.count)
        DispatchQueue.global(qos: .userInitiated).async {
            DispatchQueue.concurrentPerform(iterations: N/n) {k in
                for i in (k * n)..<((k + 1) * n) {
                    res[i] = f(self[i]) //Error 1 here
                }
            }
            //remainder loop
            for i in (N - (N % n))..<N {
                res[i] = f(self[i])
            }
            DispatchQueue.main.sync { //But it will pause on this line.
                print("\nPlease work!\n") //This doesn't execute either.
                completion(unwrap(res)!) //Error 2 here, or rather "not here"
            }
        }
    }

}   

public func unwrap<T>(_ arr: Array<T?>) -> Array<T>? {
    if (arr.contains{$0 == nil}) {
        return nil
    } else {
        return arr.map{(x: T?) -> T in x!}
    }
}

Error 1: our old friend EXC_BAD_ACCESS on the inner array assignment line about half the times I test it. I'm guessing this suggests a simultaneous access issue.

Error 2: completion never executes!

Error 3: errors go on forever, I'm sure this will happen once the above are fixed.

Finally: code for the fastest parallel (making sure it's as parallel as possible, I don't like the 'concurrent' in my code) map/for function possible. This is in addition to fixing my code.

Community
  • 1
  • 1
Richard Birkett
  • 771
  • 9
  • 20
  • 1
    btw, checkout `flatMap`. It unwraps optionals similar to your `unwrap` function – Alexander Dec 19 '16 at 02:36
  • 1
    This whole approach can't work. `Array` is a value type. When you assign to `res[i]`, you're creating a completely different array, unrelated to the original `res`. If you look at MartinR's solution, he built it with `withUnsafeMutableBufferPointer` so that he could work directly on the Array's memory, and that is still a reasonable approach in Swift3. – Rob Napier Dec 19 '16 at 03:01
  • @RobNapier I thought that when you change the elements of an array it's like a mutating function? – Richard Birkett Dec 19 '16 at 03:46
  • @RichardBirkett it is like a mutating function. Those behave the same way. If there are two observers (and there are here), then modifying the array includes copying it. – Rob Napier Dec 19 '16 at 03:55
  • 1
    Remember: concurrentPerform is just a function. You're passing a closure to it. That closure captures the value of the array (not a reference to it). – Rob Napier Dec 19 '16 at 03:57
  • I guess I thought that in the case of closure capture it was magically a reference. Thanks! – Richard Birkett Dec 19 '16 at 04:25
  • @RichardBirkett did you end up finding your solution? – Rodrigo Ruiz Oct 15 '17 at 20:02

1 Answers1

9

Martin's original approach is still the right way to do this. Merging your approach with his and converting to Swift 3 is fairly straightforward (though I got rid of your optionals and just handled the memory by hand).

extension Array {
    func parallelMap<R>(striding n: Int, f: @escaping (Element) -> R, completion: @escaping ([R]) -> ()) {
        let N = self.count

        let res = UnsafeMutablePointer<R>.allocate(capacity: N)

        DispatchQueue.concurrentPerform(iterations: N/n) { k in
            for i in (k * n)..<((k + 1) * n) {
                res[i] = f(self[i])
            }
        }

        for i in (N - (N % n))..<N {
            res[i] = f(self[i])
        }

        let finalResult = Array<R>(UnsafeBufferPointer(start: res, count: N))
        res.deallocate(capacity: N)

        DispatchQueue.main.async {
            completion(finalResult)
        }
    }
}

Martin's version avoids the extra copy because he has a "zero" value to initialize the array to. If you know your type has a trivial init(), you can avoid the extra copy:

protocol TriviallyInitializable {
    init()
}

extension Array {
    func parallelMap<R>(striding n: Int, f: @escaping (Element) -> R, completion: @escaping ([R]) -> ()) where R: TriviallyInitializable {
        let N = self.count

        var finalResult = Array<R>(repeating: R(), count: N)

        finalResult.withUnsafeMutableBufferPointer { res in
            DispatchQueue.concurrentPerform(iterations: N/n) { k in
                for i in (k * n)..<((k + 1) * n) {
                    res[i] = f(self[i])
                }
            }
        }

        for i in (N - (N % n))..<N {
            finalResult[i] = f(self[i])
        }

        DispatchQueue.main.async {
            completion(finalResult)
        }
    }
}
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • I'm still not getting anything logged (Array(0..<10), using trivial f, completion prints whatever). – Richard Birkett Dec 19 '16 at 04:48
  • How are you testing it? Remember that you'll need a "requiresInfiniteExecution" (or whatever it's called) in a playground or the playground will stop executing before it's done running. – Rob Napier Dec 19 '16 at 12:51
  • In a test function. func testExample() { // This is an example of a functional test case. Array(0..<10).parallelMap(f: {$0}, completion: { print("\n\($0)\n") }) } I added a default value (of 4) to the stride. – Richard Birkett Dec 19 '16 at 12:52
  • And this is in a full iOS app? Or you have some way you're keeping this running until finished? In either a playground or a command line app or unit test, the program will exit then the main queue is done even if other queues are still running. – Rob Napier Dec 19 '16 at 13:05
  • I'm not at a computer now to edit the answer, but the whole thing needs to be in an async block. concurrentPerform runs synchronously so this blocks the current thread – Rob Napier Dec 19 '16 at 13:16
  • Oh I see. Can I make unit tests keep running until this is done? – Richard Birkett Dec 19 '16 at 18:13
  • yes; use a DispatchGroup. Enter it outside the call, and leave it in your completion handler. – Rob Napier Dec 19 '16 at 18:22
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/130985/discussion-between-richard-birkett-and-rob-napier). – Richard Birkett Dec 19 '16 at 22:57
  • 1
    I think it would be better for the `parallelMap` to be synchronous (use a dispatch group within). If somebody wants to make it async, that can do that on their end. Seems like a synchronous call would be the more likely usecase – Alexander Apr 22 '17 at 04:58
  • Any way to make this synchronous for the user? As in make the parallelMap return the array instead of having a completion block. Also, what is that `striding` parameter? – Rodrigo Ruiz Oct 16 '17 at 03:05
  • 1
    To be synchronous, just return `finalResult`. `concurrentPerform` is synchronous. `striding` is the size of the block to operate on (so an array of 20 with a stride of 4 would dispatch 5 blocks. – Rob Napier Oct 16 '17 at 12:45
  • I guess it should return a Future or Promise for the abilities of function chaining. Unfortunately Swift has no Future/Promise lib out of the box :( – denis631 Nov 02 '17 at 15:56
  • And probably never will. Swift will probably go the async/await route which fits Swift much better than Future/Promise. https://gist.github.com/lattner/31ed37682ef1576b16bca1432ea9f782 But for now the correct Swift-way to do things is with completion handlers (which will be much easier to mechanically convert to async/await in the future). (Not to say that it's wrong to create a Future if it fits your use-case really well. It's very easy to write. But they tend to be very hard to debug in my experience trying to use them in Swift.) – Rob Napier Nov 02 '17 at 16:03
  • 1
    I am slightly concerned about `res[i] = f(self[i])`. Would it be the case that Swift tries to release `res[i]` before doing the assignment? It feels better to get the pointer through `res.baseAddress!.advanced(by: i)` and then using `initialize(to:)` to do the assignment. Maybe my concern is unnecessary, but I just want to be careful. – Minsheng Liu Oct 22 '18 at 23:04