我写了一个非常简单的程序来测试并行程序的性能。我写了一个非常简单的程序,它通过分裂试验来分析一个大的半英数。由于没有涉及通信,我预计几乎完美的加速。然而,该计划似乎非常严重。
我使用系统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的并行性能。为什么?你对此有任何提及吗?
答案 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个整数,那么这些处理器中的一个将至少花费时间来分解最难的整数,然后分解其余负载的时间。
也许你做过类似的事情?