利用并发向量化函数
Vectorise a function taking advantage of concurrency
对于一个简单的神经网络,我想将一个函数应用于 gonum VecDense
的所有值。
Gonum 有一个用于密集矩阵的 Apply
方法,但没有用于向量的方法,所以我手动执行此操作:
func sigmoid(z float64) float64 {
return 1.0 / (1.0 + math.Exp(-z))
}
func vSigmoid(zs *mat.VecDense) {
for i := 0; i < zs.Len(); i++ {
zs.SetVec(i, sigmoid(zs.AtVec(i)))
}
}
这似乎是一个明显的并发执行目标,所以我尝试了
var wg sync.WaitGroup
func sigmoid(z float64) float64 {
wg.Done()
return 1.0 / (1.0 + math.Exp(-z))
}
func vSigmoid(zs *mat.VecDense) {
for i := 0; i < zs.Len(); i++ {
wg.Add(1)
go zs.SetVec(i, sigmoid(zs.AtVec(i)))
}
wg.Wait()
}
这不起作用,也许并不意外,因为 Sigmoid()
不以 wg.Done()
结尾,因为 return 语句(完成所有工作)紧随其后.
我的问题是:如何使用并发将函数应用于 gonum 向量的每个元素?
首先请注意,这种并发计算的尝试假定 SetVec()
和 AtVec()
方法对于不同索引的并发使用是安全的。如果不是这种情况,尝试的解决方案本质上是不安全的,可能会导致数据竞争和未定义的行为。
wg.Done()
应该被调用以表明 "worker" goroutine 完成了它的工作。但是只有 goroutine 完成它的工作。
在你的例子中,worker goroutine 中的 运行 不是(仅)sigmoid()
函数,而是 zs.SetVec()
。所以你应该在 zs.SetVec()
返回时调用 wg.Done()
,而不是更早。
一种方法是在 SetVec()
方法的末尾添加一个 wg.Done()
(它也可以在其开头添加一个 defer wg.Done()
),但它不会引入这种依赖是可行的(SetVec()
不应该知道任何等待组和 goroutines,这会严重限制它的可用性)。
在这种情况下,最简单和最干净的方法是启动一个匿名函数(函数文字)作为工作协程,您可以在其中调用 zs.SetVec()
,并且可以在其中调用 wg.Defer()
一旦上述函数返回。
像这样:
for i := 0; i < zs.Len(); i++ {
wg.Add(1)
go func() {
zs.SetVec(i, sigmoid(zs.AtVec(i)))
wg.Done()
}()
}
wg.Wait()
但仅此一项 不会 工作,因为函数文字(闭包)引用并发修改的循环变量,因此函数文字应该使用它自己的副本, 例如:
for i := 0; i < zs.Len(); i++ {
wg.Add(1)
go func(i int) {
zs.SetVec(i, sigmoid(zs.AtVec(i)))
wg.Done()
}(i)
}
wg.Wait()
另请注意,goroutine(尽管可能是轻量级的)确实有开销。如果他们所做的工作是 "small",开销可能会超过利用多核/多线程带来的性能提升,并且总体而言,您可能无法通过并发执行此类小任务来获得性能提升(见鬼,您甚至可能比不使用更糟协程)。测量。
此外,您正在使用 goroutines 来完成最少的工作,一旦 goroutines 完成 "tiny" 工作,您可以通过不 "throwing" 离开 goroutines 来提高性能,但您可以 "reuse"他们。参见相关问题:
对于一个简单的神经网络,我想将一个函数应用于 gonum VecDense
的所有值。
Gonum 有一个用于密集矩阵的 Apply
方法,但没有用于向量的方法,所以我手动执行此操作:
func sigmoid(z float64) float64 {
return 1.0 / (1.0 + math.Exp(-z))
}
func vSigmoid(zs *mat.VecDense) {
for i := 0; i < zs.Len(); i++ {
zs.SetVec(i, sigmoid(zs.AtVec(i)))
}
}
这似乎是一个明显的并发执行目标,所以我尝试了
var wg sync.WaitGroup
func sigmoid(z float64) float64 {
wg.Done()
return 1.0 / (1.0 + math.Exp(-z))
}
func vSigmoid(zs *mat.VecDense) {
for i := 0; i < zs.Len(); i++ {
wg.Add(1)
go zs.SetVec(i, sigmoid(zs.AtVec(i)))
}
wg.Wait()
}
这不起作用,也许并不意外,因为 Sigmoid()
不以 wg.Done()
结尾,因为 return 语句(完成所有工作)紧随其后.
我的问题是:如何使用并发将函数应用于 gonum 向量的每个元素?
首先请注意,这种并发计算的尝试假定 SetVec()
和 AtVec()
方法对于不同索引的并发使用是安全的。如果不是这种情况,尝试的解决方案本质上是不安全的,可能会导致数据竞争和未定义的行为。
wg.Done()
应该被调用以表明 "worker" goroutine 完成了它的工作。但是只有 goroutine 完成它的工作。
在你的例子中,worker goroutine 中的 运行 不是(仅)sigmoid()
函数,而是 zs.SetVec()
。所以你应该在 zs.SetVec()
返回时调用 wg.Done()
,而不是更早。
一种方法是在 SetVec()
方法的末尾添加一个 wg.Done()
(它也可以在其开头添加一个 defer wg.Done()
),但它不会引入这种依赖是可行的(SetVec()
不应该知道任何等待组和 goroutines,这会严重限制它的可用性)。
在这种情况下,最简单和最干净的方法是启动一个匿名函数(函数文字)作为工作协程,您可以在其中调用 zs.SetVec()
,并且可以在其中调用 wg.Defer()
一旦上述函数返回。
像这样:
for i := 0; i < zs.Len(); i++ {
wg.Add(1)
go func() {
zs.SetVec(i, sigmoid(zs.AtVec(i)))
wg.Done()
}()
}
wg.Wait()
但仅此一项 不会 工作,因为函数文字(闭包)引用并发修改的循环变量,因此函数文字应该使用它自己的副本, 例如:
for i := 0; i < zs.Len(); i++ {
wg.Add(1)
go func(i int) {
zs.SetVec(i, sigmoid(zs.AtVec(i)))
wg.Done()
}(i)
}
wg.Wait()
另请注意,goroutine(尽管可能是轻量级的)确实有开销。如果他们所做的工作是 "small",开销可能会超过利用多核/多线程带来的性能提升,并且总体而言,您可能无法通过并发执行此类小任务来获得性能提升(见鬼,您甚至可能比不使用更糟协程)。测量。
此外,您正在使用 goroutines 来完成最少的工作,一旦 goroutines 完成 "tiny" 工作,您可以通过不 "throwing" 离开 goroutines 来提高性能,但您可以 "reuse"他们。参见相关问题: