Ускорение проблем с го - PullRequest
2 голосов
/ 09 марта 2012

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

Я рассчитал программу с 1, 2, 4 и 8 процессами, запущенными на компьютере с 8 (реальными, не HT) ядрами, используя систему timeкоманда.Число, которое я разложил, является «28808539627864609».Вот мои результаты:

cores  time (sec) speedup
  1   60.0153      1
  2   47.358       1.27
  4   34.459       1.75
  8   28.686       2.10

Как объяснить такие плохие ускорения?Это ошибка в моей программе, или это проблема с Go Runtime?Как я мог получить лучшие выступления?Я говорю не о самом алгоритме (я знаю, что есть лучшие алгоритмы для факторизации чисел полупростых чисел), а о том, как я распараллелил его.

Вот исходный код моей программы:

package main

import (
    "big"
    "flag"
    "fmt"
    "runtime"
)

func factorize(n *big.Int, start int, step int, c chan *big.Int) {

    var m big.Int
    i := big.NewInt(int64(start))
    s := big.NewInt(int64(step))
    z := big.NewInt(0)

    for {
        m.Mod(n, i)
        if m.Cmp(z) == 0{
            c <- i
        }
        i.Add(i, s)
    }
}

func main() {
    var np *int = flag.Int("n", 1, "Number of processes")
    flag.Parse()

    runtime.GOMAXPROCS(*np)

    var n big.Int
    n.SetString(flag.Arg(0), 10) // Uses number given on command line
    c := make(chan *big.Int)
    for i:=0; i<*np; i++ {
        go factorize(&n, 2+i, *np, c)
    }
    fmt.Println(<-c)
}

РЕДАКТИРОВАТЬ

Проблема действительно связана с функцией Mod.Замена на Rem дает лучшие, но все же несовершенные характеристики и ускорения.Замена на QuoRem дает в 3 раза более высокую производительность и идеальное ускорение.Вывод: похоже, что распределение памяти убивает параллельные исполнения в Go.Зачем?Есть ли у вас какие-либо упоминания об этом?

Ответы [ 3 ]

3 голосов
/ 09 марта 2012

Big.Int методы обычно должны выделять память, обычно для хранения результата вычисления.Проблема в том, что существует только одна куча, и все операции с памятью сериализуются.В этой программе числа довольно малы, и (параллелизуемое) время вычислений, необходимое для таких вещей, как Mod и Add, мало по сравнению с непараллелизуемыми операциями многократного выделения всех крошечных кусочков памяти.

КакЧто касается ускорения, есть очевидный ответ: не используйте big.Ints, если вам не нужно.Ваш пример номер вписывается в 64 бит.Однако, если вы планируете работать с действительно большими числами, эта проблема исчезнет сама собой.Вы будете тратить гораздо больше времени на вычисления, и время, проведенное в куче, будет относительно намного меньше.

Кстати, в вашей программе есть ошибка, хотя она не связана с производительностью.Когда вы находите коэффициент и возвращаете результат на канале, вы отправляете указатель на локальную переменную i.Это хорошо, за исключением того, что вы не выходите из цикла.Цикл в goroutine продолжает увеличивать i, и к тому времени, когда основная goroutine начинает ловить указатель из канала и следовать за ним, значение почти наверняка будет неправильным.

3 голосов
/ 10 марта 2012

После отправки i по каналу i следует заменить на вновь выделенное big.Int:

if m.Cmp(z) == 0 {
    c <- i
    i = new(big.Int).Set(i)
}

Это необходимо, поскольку нет гарантии, что fmt.Println обработаетцелое число получено в строке fmt.Println(<-c).* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * При нерегулярном использовании fmt.Println происходит переключение режима работы, поэтому, если i не заменяется вновь выделенным big.Int, и время выполнения переключается обратно на выполнение цикла for в функции factorize, тогдацикл for перезапишет i перед печатью - в этом случае программа не распечатает целое число 1st , отправленное по каналу.


Тот факт, что fmt.Println может вызвать переключение между циклами, что означает, что цикл for в функции factorize может потенциально потребляет много времени ЦП между моментом, когда программа main получает из канала c, и моментом, когдаmain процедура завершается.Примерно так:

run factorize()
<-c in main()
call fmt.Println()
continue running factorize()   // Unnecessary CPU time consumed
return from fmt.Println()
return from main() and terminate program

Еще одна причина небольшого многоядерного ускорения - выделение памяти .Функция (*Int).Mod внутренне использует (*Int).QuoRem и будет создавать новый big.Int при каждом вызове.Чтобы избежать выделения памяти, используйте QuoRem напрямую:

func factorize(n *big.Int, start int, step int, c chan *big.Int) {
    var q, r big.Int
    i := big.NewInt(int64(start))
    s := big.NewInt(int64(step))
    z := big.NewInt(0)

    for {
        q.QuoRem(n, i, &r)
        if r.Cmp(z) == 0 {
            c <- i
            i = new(big.Int).Set(i)
        }
        i.Add(i, s)
    }
}

К сожалению, в планировщике goroutine в версии Go r60.3 есть ошибка, которая не позволяет этому коду использовать все ядра ЦП.Когда программа запускается с -n=2 (GOMAXPROCS = 2), время выполнения будет использовать только 1 поток.

Go еженедельный выпуск имеет лучшее время выполнения и может использовать 2потоки, если n=2 передается в программу.Это дает ускорение примерно на 1,9 на моей машине.


Еще один потенциальный фактор, способствующий замедлению многоядерных процессоров, был упомянут в ответе пользователя "High Performance Mark".Если программа разбивает работу на несколько подзадач, и результат получается только из одной подзадачи, это означает, что другие подзадачи могут выполнять некоторую «дополнительную работу».Запуск программы с n>=2 может в общей сложности потребляет больше процессорного времени, чем запуск программы с n=1.

Узнать, сколько дополнительной работы выполняется, вы можете захотеть (каким-то образом) распечатать значения всех i во всех процедурах в момент выхода из функции main().

0 голосов
/ 09 марта 2012

Я не читаю go, так что это, вероятно, ответ на вопрос, который вы не задали.Если это так, то понизьте или удалите, как хотите.

Если бы вы строили график «время для факторизации целого числа n» по отношению к «n», вы бы получили график, который в некоторой степени повышается или понижается.Для любого n, который вы выберете, будет целое число в диапазоне 1..n, для которого требуется больше времени на разложение на один процессор.Если ваша стратегия распараллеливания состоит в том, чтобы распределить n целых чисел по p-процессорам, то одному из этих процессоров потребуется как минимум время, чтобы разложить самое сложное целое число, а затем время, чтобы разложить оставшуюся нагрузку.

Возможно, вы сделали что-то подобное?

...