利用并发向量化函数

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"他们。参见相关问题: