Почему распараллеливание замедляет мою программу? - PullRequest
0 голосов
/ 02 марта 2020

У меня есть две программы. Они решают систему линейных уравнений. Они оба работают правильно (они дают одинаковый результат).

Первая программа работает без параллелизма.

вторая программа очень похожа на первую, за исключением того, что я добавил параллелизм в некоторые места. Эти места отмечены в коде.

Вот две программы:

Первая. Без параллелизма.

package main

import (
        "fmt"
        "math"
        "os"
        "time"
)

func main() {
        start := time.Now()
        N := 1000
        a := CreateRandomMatrix(N)
        b := CreateRandomVector(N)

        index := make([]int, len(a))
        for i := range index {
            index[i] = i
        }

        for i := 0; i < len(a); i++ {

            r := a[i][index[i]]

            var kk int

            var maxElemInRow float64
            for k := i; k < len(a); k++ {
                if math.Abs(a[i][index[k]]) > maxElemInRow {
                    kk = k
                    maxElemInRow = math.Abs(a[i][index[k]])
                }
            }

            index[i], index[kk] = index[kk], index[i]

            r = a[i][index[i]]

            if r == 0 {
                if b[i] == 0 {
                    fmt.Println("a lot of solutions")
                } else {
                    fmt.Println("no solutions")
                }
                os.Exit(1)
            }

            for j := 0; j < len(a[i]); j++ {
                a[i][index[j]] /= r
            }
            b[i] /= r

            for k := i + 1; k < len(a); k++ {
                r = a[k][index[i]]
                for j := 0; j < len(a[i]); j++ {
                    a[k][index[j]] = a[k][index[j]] - a[i][index[j]]*r
                }
                b[k] = b[k] - b[i]*r
            }

        }

        var x vector = make(vector, len(b))

        for i := len(a) - 1; i >= 0; i-- {
            x[i] = b[i]

            for j := i + 1; j < len(a); j++ {
                x[i] = x[i] - (x[j] * a[i][index[j]])
            }
        }

        result := make([]string, len(x))
        for i, val := range index {
            result[val] = fmt.Sprintf("%.2f", x[i])
        }
        fmt.Println("tested part took:", time.Now().Sub(start))
    }

Второй:

package main

import (
    "fmt"
    "math"
    "os"
    "sync"
    "time"
)

const (
    workers = 8
)

var wg sync.WaitGroup

func main() {

    start := time.Now()
    N := 1000
    a := CreateRandomMatrix(N)
    b := CreateRandomVector(N)

    index := make([]int, len(a))
    for i := range index {
        index[i] = i
    }

    for i := 0; i < len(a); i++ {

        r := a[i][index[i]]

        var kk int
        var max float64

        for k := i; k < len(a); k++ {
            if math.Abs(a[i][index[k]]) > max {
                kk = k
                max = math.Abs(a[i][index[k]])
            }
        }

        index[i], index[kk] = index[kk], index[i]

        r = a[i][index[i]]

        if r == 0 {
            if b[i] == 0 {
                fmt.Println("a lot of solutions")
            } else {
                fmt.Println("no solutions")
            }
            os.Exit(1)
        }

        // concurrency here
        for w := 0; w < workers; w++ {
            wg.Add(1)
            go func(w int) {
                start := len(a[i]) / workers * w
                end := len(a[i]) / workers * (w + 1)

                if end > len(a[i]) {
                    end = len(a[i])
                }

                for j := start; j < end; j++ {
                    a[i][index[j]] /= r
                }
                wg.Done()
            }(w)
        }

        b[i] /= r
        wg.Wait()

        for k := i + 1; k < len(a); k++ {
            r = a[k][index[i]]
            for j := 0; j < len(a[i]); j++ {
                a[k][index[j]] = a[k][index[j]] - a[i][index[j]]*r
            }
            b[k] = b[k] - b[i]*r
        }

    }

    var x vector = make(vector, len(b))

    for i := len(a) - 1; i >= 0; i-- {
        x[i] = b[i]

        for j := i + 1; j < len(a); j++ {
            x[i] = x[i] - (x[j] * a[i][index[j]])
        }
    }

    result := make([]string, len(x))
    for i, val := range index {
        result[val] = fmt.Sprintf("%.2f", x[i])
    }
    fmt.Println("tested part took:", time.Now().Sub(start))
}

И дополнительный блок кода одинаков для обеих программ

package main

import "math/rand"

type matrix [][]float64
type vector []float64

func CreateRandomMatrix(n int) matrix {
    m := make(matrix, n)
    for i := 0; i < n; i++ {
        m[i] = make(vector, n)
        for j := 0; j < n; j++ {
            m[i][j] = float64(rand.Intn(100))
        }
    }
    return m
}

func CreateRandomVector(n int) vector {
    v := make(vector, n)
    for i := 0; i < n; i++ {
        v[i] = float64(rand.Intn(100))
    }
    return v
}

Итак. Вот проблема:

Теоретически вторая программа должна работать быстрее, потому что некоторые вычисления распределены по ядрам процессора. Но этого не происходит. С каждым добавлением элемента параллелизма вторая программа начинает тормозить.

Я проверил его для больших значений N, а также для маленьких. Время работы второй версии программы значительно отстает от первой. Например, если вы установите N = 3500, разница в выполнении составит около 10 секунд.

Кроме того, если вы установите число работников равным 1, вторая программа начнет работать быстрее.

Почему это происходит? Я где-то ошибаюсь? Как ускорить работу распределенных вычислений?

GO ВЕРСИЯ: 1.14. Но я также проверил этот код в версии 1.13.

ADD: Я обнаружил, что если программа обслуживается с матрицами большого размера, то параллельная версия начинает догонять последовательную.

РЕДАКТИРОВАТЬ РЕЗЮМЕ: Во второй программе кусок с параллелизмом был удален в месте, где kk и max рассчитаны, чтобы избавиться от гонок данных.

Ответы [ 2 ]

0 голосов
/ 03 марта 2020

Итак, спасибо всем, кто помог мне и объяснил мне, в чем я был неправ. Я посмотрел на код и нашел место, где его можно оптимизировать путем распараллеливания вычислений и получить реальное увеличение скорости благодаря обоснованности этого подхода в этом месте.

Все моменты, в которых я пытался Распараллеливая ранее, я вернулся в исходное последовательное состояние.

Кусок кода, который я заменил с помощью параллельного подхода, помечен комментарием.

Вот код:

package main

import (
    "fmt"
    "math"
    "os"
    "sync"
    "time"
)

const (
    workers = 8
)

var wg sync.WaitGroup

func main() {
    N := 3000

    a := CreateRandomMatrix(N)
    b := CreateRandomVector(N)
    start := time.Now()

    index := make([]int, len(a))
    for i := range index {
        index[i] = i
    }

    for i := 0; i < len(a); i++ {

        r := a[i][index[i]]

        var kk int
        var max float64

        for k := i; k < len(a); k++ {
            if math.Abs(a[i][index[k]]) > max {
                kk = k
                max = math.Abs(a[i][index[k]])
            }
        }

        index[i], index[kk] = index[kk], index[i]

        r = a[i][index[i]]

        if r == 0 {
            if b[i] == 0 {
                fmt.Println("a lot of solutions")
            } else {
                fmt.Println("no solutions")
            }
            os.Exit(1)
        }

        for j := 0; j < len(a[i]); j++ {
            a[i][index[j]] /= r
        }
        b[i] /= r

        //concurrency
        chunk := len(a) / workers
        for w := 0; w < workers; w++ {
            wg.Add(1)
            go func(start int) {
                end := start + chunk
                if end > len(a) {
                    end = len(a)
                }
                for k := start; k < end; k++ {
                    r := a[k][index[i]]

                    for j := 0; j < len(a[i]); j++ {
                        a[k][index[j]] = a[k][index[j]] - a[i][index[j]]*r
                    }
                    b[k] = b[k] - b[i]*r
                }
                wg.Done()
            }(w*chunk + i + 1)

        }
        wg.Wait()

    }

    var x vector = make(vector, len(b))

    for i := len(a) - 1; i >= 0; i-- {
        x[i] = b[i]

        for j := i + 1; j < len(a); j++ {
            x[i] = x[i] - (x[j] * a[i][index[j]])
        }
    }

    result := make([]string, len(x))
    for i, val := range index {
        result[val] = fmt.Sprintf("%.2f", x[i])
    }
    fmt.Println(time.Since(start))
}

При размере матрицы N = 3000 программа с параллелизмом вычисляет систему линейных уравнений за 19 секунд , тогда как обычная версия тратит 43 секунды. !!!

0 голосов
/ 02 марта 2020

Вы настраиваете здесь большую сложность для очень небольшого объема работы:

    for j := 0; j < len(a[i]); j++ {
        a[i][index[j]] /= r
    }

Это требует N делений, так что в вашем примере около 1000. Плюс некоторая бухгалтерия.

Вы заменяете это следующим:

            start := len(a[i]) / workers * w
            end := len(a[i]) / workers * (w + 1)

            if end > len(a[i]) {
                end = len(a[i])
            }

            for j := start; j < end; j++ {
                a[i][index[j]] /= r
            }

Это выполняет 125 делений (1000/8), плюс 2 дополнительных деления, два дополнительных умножения, плюс дополнительное сложение и дополнительное вычитание (вот как это делается >) в строке. Другая бухгалтерия такая же. Так что примерно 3% вычислительных затрат еще до того, как вы начнете. Это нормально в параллельной работе; Я просто напоминаю вам, что параллельная работа всегда начинается в дыре, из которой она должна выкопать себя.

Вы добавляете к этим 8 созданиям подпрограмм и 16 операций WaitGroup (плюс ожидание) на строку. Это тысячи горутин, созданных и уничтоженных. Горутины дешевы, но они не такие 1013 * дешевые. И они определенно недешевые внутри всего oop, который вы запускаете тысячу раз.

И это для ускорения фрагмента кода, который, по моим измерениям, составляет 0,05% от вашего l oop , Вы проверяете время на основе всего цикла (включая создание матриц в первую очередь). Но когда я проверяю основной внешний l oop (N = 1000), он составляет около 20 мс (последовательный). Деление l oop составляет около 10 мкс. Разделение l oop не является интересной частью для распределения по процессорам. Программа, которая существует только в течение 10 мкс, не годится для использования ресурсов.

Вы упомянули: «Перед этим я открыл соответствующую литературу с кодом C#, в котором даются рекомендации по распараллеливанию этой программы, и есть результаты. которые показывают значительное увеличение скорости ". Это не похоже, что это будет быстро в C#, либо. Если бы вы создали 8 задач в строке для выполнения деления, я ожидал бы, что у вас будут те же проблемы, что и в Go.

Возможно, здесь также возникают проблемы с локальностью памяти. Гораздо эффективнее вытащить память из оперативной памяти порциями и работать с ней последовательно, чем перепрыгивать между областями памяти, аннулируя кэши ЦП и тратя часть строки кэша. (Но я подозреваю, что это перегружено стоимостью генерации и уничтожения 8000 процедур.)

Я на самом деле очень удивлен тем, насколько быстро работает параллельный код.

Вместо того, чтобы выбирать один маленький биты, которые должны выполняться параллельно между ядрами, вы должны структурировать свой алгоритм так, чтобы вы могли разделить большую работу на небольшое количество работников, а не на небольшую работу для большого количества работников.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...