Использование Grand Central Dispatch в Swift для распараллеливания и ускорения циклов for?

Я пытаюсь понять, как использовать GCD для распараллеливания и ускорения моделирования Монте-Карло. Большинство/все простые примеры представлены для Objective C, и мне действительно нужен простой пример для Swift, поскольку Swift — мой первый «настоящий» язык программирования.

Минимальная рабочая версия симуляции методом Монте-Карло в Swift будет выглядеть примерно так:

import Foundation

import Cocoa
var winner = 0
var j = 0
var i = 0
var chance = 0
var points = 0
for j=1;j<1000001;++j{
    var ability = 500

    var player1points = 0

    for i=1;i<1000;++i{
        chance = Int(arc4random_uniform(1001))
        if chance<(ability-points) {++points}
        else{points = points - 1}
    }
    if points > 0{++winner}
}
    println(winner)

Код работает непосредственно вставленным в проект программы командной строки в xcode 6.1.

Самый внутренний цикл не может быть распараллелен, так как новое значение переменной «точки» используется в следующем цикле. Но самое внешнее просто запускает самое внутреннее моделирование 1000000 раз и подсчитывает результаты и должно быть идеальным кандидатом на распараллеливание.

Итак, мой вопрос: как использовать GCD для распараллеливания самого внешнего цикла for?


person user2523167    schedule 24.11.2014    source источник


Ответы (2)


«Многопоточная итерация» может быть выполнена с помощью dispatch_apply():

let outerCount = 100    // # of concurrent block iterations
let innerCount = 10000  // # of iterations within each block

let the_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(UInt(outerCount), the_queue) { outerIdx -> Void in
    for innerIdx in 1 ... innerCount {
       // ...
    }
}

(Вы должны выяснить наилучшее соотношение между внешними и внутренними счетами.)

Есть две вещи, на которые стоит обратить внимание:

Тогда это будет выглядеть примерно так:

let outerCount = 100     // # of concurrent block iterations
let innerCount = 10000   // # of iterations within each block

let the_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

var winners = [Int](count: outerCount, repeatedValue: 0)
winners.withUnsafeMutableBufferPointer { winnersPtr -> Void in

    dispatch_apply(UInt(outerCount), the_queue) { outerIdx -> Void in
        var seed = arc4random() // seed for rand_r() in this "thread"

        for innerIdx in 1 ... innerCount {
            var points = 0
            var ability = 500

            for i in 1 ... 1000 {
                let chance = Int(rand_r(&seed) % 1001)
                if chance < (ability-points) { ++points }
                else {points = points - 1}
            }
            if points > 0 {
                winnersPtr[Int(outerIdx)] += 1
            }
        }
    }
}

// Add results:
let winner = reduce(winners, 0, +)
println(winner)
person Martin R    schedule 24.11.2014
comment
Не знал этого о arc4random()! Будьте осторожны при изменении массивов из нескольких потоков - см. этот вопрос и ответ для получения дополнительной информации: title="массив процессов Swift параллельно с использованием gcd">stackoverflow.com/questions/26693838/ - person Nate Cook; 24.11.2014
comment
Я думаю, что мой второй ответ (не принятый) на этот Q является самым быстрым, поскольку он явно получает непрерывный буфер памяти для массива. - person Nate Cook; 24.11.2014
comment
@NateCook: У меня уже было ощущение, что я видел что-то подобное, но не мог найти. Спасибо за ссылку и ваше решение (+1), я соответственно обновил ответ. - person Martin R; 24.11.2014
comment
Спасибо. Особенно за использование моего очень простого примера. Так у меня будет шанс понять, что происходит. Не могу дождаться, чтобы попробовать это позже сегодня. Однако один вопрос: вы разбиваете 1000000 «циклов» на куски, прежде чем ставить их в очередь. Это чтобы избежать слишком больших накладных расходов? В коде, который я на самом деле использую, самый внутренний цикл имеет гораздо больше манипуляций со многими переменными. Повлияет ли это на то, как разделить исходные 1000000 циклов, или является более важным фактором, сколько «кусков» вы отправляете в очередь? - person user2523167; 24.11.2014
comment
@user2523167: см. dispatch_apply(3): Иногда, когда блок, переданный в dispatch_apply(), прост, использование шага может повысить производительность. Расчет оптимального шага лучше оставить экспериментировать. ... - person Martin R; 24.11.2014
comment
Может кто-нибудь показать версию Swift 3? Это действительно сбивает с толку, потому что вы не можете указать конкретной очереди выполнить PerformConcurrent, и ни один из моих кодов не работает. - person Richard Birkett; 18.12.2016
comment
@RichardBirkett: Да, я тоже об этом думал. Посмотрите комментарии после этого ответа stackoverflow.com/a/39949292/1187415. - person Martin R; 18.12.2016
comment
@MartinR Спасибо! Так что он должен просто работать нормально, и он будет выбирать свою очередь совершенно независимо от того, в какую очередь он отправлен. Поэтому, если я не хочу, чтобы параллельный процесс блокировал поток, я могу отправить его асинхронно в очередь с любым приоритетом и содержимым все еще будет в приоритетной очереди? В любом случае мой код все еще не работает :( - person Richard Birkett; 18.12.2016

Просто чтобы обновить это для современного синтаксиса, мы теперь используем concurrentPerform (который заменяет dispatch_apply).

Таким образом, мы можем заменить

for j in 0 ..< 1_000_000 {
    for i in 0 ..< 1000 {
        ...
    }
}

С участием

DispatchQueue.concurrentPerform(1_000_000) { j in
    for i in 0 ..< 1000 {
        ...
    }
}

Обратите внимание, что распараллеливание вносит небольшие накладные расходы как в базовый механизм диспетчеризации GCD, так и в синхронизацию результатов. Если бы у вас было 32 итерации в параллельном цикле, это было бы несущественно, но у вас есть миллион итераций, и они начнут складываться.

Обычно мы решаем это путем «шагания»: вместо того, чтобы распараллеливать 1 миллион итераций, вы можете сделать только 100 параллельных итераций, выполняя по 10 000 итераций каждая. Например. что-то типа:

let totalIterations = 1_000_000
let stride = 10_000
let (quotient, remainder) = totalIterations.quotientAndRemainder(dividingBy: stride)
let iterations = quotient + remainder == 0 ? 0 : 1

DispatchQueue.concurrentPerform(iterations: iterations) { iteration in
    for j in iteration * stride ..< min(totalIterations, (iteration + 1) * stride) {
        for i in 0 ..< 1000 {
            ...
        }
    }
}
person Rob    schedule 09.05.2020