加速问题

时间:2012-03-09 16:40:12

标签: performance parallel-processing go

我写了一个非常简单的程序来测试并行程序的性能。我写了一个非常简单的程序,它通过分裂试验来分析一个大的半英数。由于没有涉及通信,我预计几乎完美的加速。然而,该计划似乎非常严重。

我使用系统time命令在1个,2个,4个和8个进程上对程序进行计时,在8(真实的,非HT)核心计算机上运行。我分解的数字是“28808539627864609”。以下是我的结果:

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

如何解释这种糟糕的加速?这是我的程序中的错误,还是运行时的问题?我怎么能得到更好的表现?我不是在谈论算法本身(我知道有更好的算法来分解半素数),但关于我并行化的方法。

以下是我的程序的源代码:

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 个答案:

答案 0 :(得分:3)

Big.Int方法通常必须分配内存,通常用于保存计算结果。问题是只有一个堆,所有内存操作都是序列化的。在这个程序中,数字相当小,与重复分配所有微小内存的不可并行操作相比,Mod和Add等所需的(可并行化)计算时间很短。

就加速而言,有一个明显的答案是不要使用big.Ints如果你不需要。您的示例数字恰好适合64位。如果你计划与真正的大数字一起工作,那么这个问题就会自行消失。您将花费更多时间进行计算,并且在堆中花费的时间将相对少得多。

顺便说一句,你的程序中有一个错误,虽然它与性能无关。当您找到一个因子并在通道上返回结果时,您会发送一个指向局部变量i的指针。这很好,除了你没有突破循环。 goroutine中的循环继续递增i,当主goroutine到处捕捉指针离开通道并跟随它时,该值几乎肯定是错误的。

答案 1 :(得分:3)

通过频道发送i后,i应替换为新分配的big.Int

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

这是必要的,因为无法保证fmt.Println何时处理在线fmt.Println(<-c)上收到的整数。 fmt.Println导致goroutine切换并不常见,因此如果i没有被新分配的big.Int替换,并且运行时切换回执行函数factorize中的for循环然后for循环将在打印之前覆盖i - 在这种情况下,程序不会打印出发送的 1st 整数通过渠道。


fmt.Println可能导致goroutine切换的事实意味着函数factorize中的for循环可能潜在地在{{{}之间消耗大量CPU时间1}} goroutine从频道main收到c goroutine终止的那一刻。像这样:

main

小型多核加速的另一个原因是内存分配。函数(*Int).Mod在内部使用(*Int).QuoRem,每次调用时都会创建一个新的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 。要避免内存分配,请直接使用big.Int

QuoRem

不幸的是,Go版本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调度程序包含一个错误,该错误会阻止此代码使用所有CPU内核。当程序以r60.3(GOMAXPROCS = 2)启动时,运行时将只使用1个线程。

Go 每周发布有更好的运行时间,如果-n=2传递给程序,则可以使用2个线程。这在我的机器上提供了大约1.9的加速。


用户对#34; High Performance Mark&#34;的回答中提到了多核减速的另一个潜在因素。如果程序将工作分成多个子任务并且结果仅来自1个子任务,则意味着其他子任务可以执行一些额外的工作&#34;。使用n=2运行程序可能总计比使用n>=2运行程序消耗更多的CPU时间。

了解正在进行多少额外工作,您可能希望(以某种方式)在程序退出函数{{1时打印出所有goroutine中的所有n=1的值}}

答案 2 :(得分:0)

我不读go所以这可能是一个问题的答案,而不是你问的问题。如果是这样,请按照您的意愿进行投票或删除。

如果你要制作一个'对整数n进行因子分解的时间'对'n'的图,你会得到一个随机上下变化的图。对于您选择的任何n,将存在1..n范围内的整数,该整数在一个处理器上的分解时间最长。如果您的并行化策略是在p个处理器之间分配n个整数,那么这些处理器中的一个将至少花费时间来分解最难的整数,然后分解其余负载的时间。

也许你做过类似的事情?