Golang中的并发积分计算
Concurrent integral calculation in Golang
我尝试同时计算积分,但我的程序最终比使用普通 for 循环计算积分慢。我做错了什么?
package main
import (
"fmt"
"math"
"sync"
"time"
)
type Result struct {
result float64
lock sync.RWMutex
}
var wg sync.WaitGroup
var result Result
func main() {
now := time.Now()
a := 0.0
b := 1.0
n := 100000.0
deltax := (b - a) / n
wg.Add(int(n))
for i := 0.0; i < n; i++ {
go f(a, deltax, i)
}
wg.Wait()
fmt.Println(deltax * result.result)
fmt.Println(time.Now().Sub(now))
}
func f(a float64, deltax float64, i float64) {
fx := math.Sqrt(a + deltax * (i + 0.5))
result.lock.Lock()
result.result += fx
result.lock.Unlock()
wg.Done()
}
除非 goroutine 中 activity 花费的时间比切换上下文、执行任务和使用互斥锁更新值所需的时间多得多,否则这样做会更快连续。
看看稍微修改过的版本。我所做的只是在 f()
函数中添加 1 微秒的延迟。
package main
import (
"fmt"
"math"
"sync"
"time"
)
type Result struct {
result float64
lock sync.RWMutex
}
var wg sync.WaitGroup
var result Result
func main() {
fmt.Println("concurrent")
concurrent()
result.result = 0
fmt.Println("serial")
serial()
}
func concurrent() {
now := time.Now()
a := 0.0
b := 1.0
n := 100000.0
deltax := (b - a) / n
wg.Add(int(n))
for i := 0.0; i < n; i++ {
go f(a, deltax, i, true)
}
wg.Wait()
fmt.Println(deltax * result.result)
fmt.Println(time.Now().Sub(now))
}
func serial() {
now := time.Now()
a := 0.0
b := 1.0
n := 100000.0
deltax := (b - a) / n
for i := 0.0; i < n; i++ {
f(a, deltax, i, false)
}
fmt.Println(deltax * result.result)
fmt.Println(time.Now().Sub(now))
}
func f(a, deltax, i float64, concurrent bool) {
time.Sleep(1 * time.Microsecond)
fx := math.Sqrt(a + deltax*(i+0.5))
if concurrent {
result.lock.Lock()
result.result += fx
result.lock.Unlock()
wg.Done()
} else {
result.result += fx
}
}
加上延迟,结果如下(并发版本快很多):
concurrent
0.6666666685900424
624.914165ms
serial
0.6666666685900422
5.609195767s
没有延迟:
concurrent
0.6666666685900428
50.771275ms
serial
0.6666666685900422
749.166µs
如您所见,完成一项任务所需的时间越长,如果可能的话,同时执行它就越有意义。
3- 为了提高性能,您可以在不使用 lock sync.RWMutex
的情况下按 CPU 个核心划分任务:
+30x
使用通道和 runtime.NumCPU()
的优化,这需要 2ms
2 核和 993µs
8 核,而您的示例代码需要 61ms
2 核和 40ms
8 核:
查看此工作示例代码和输出:
package main
import (
"fmt"
"math"
"runtime"
"time"
)
func main() {
nCPU := runtime.NumCPU()
fmt.Println("nCPU =", nCPU)
ch := make(chan float64, nCPU)
startTime := time.Now()
a := 0.0
b := 1.0
n := 100000.0
deltax := (b - a) / n
stepPerCPU := n / float64(nCPU)
for start := 0.0; start < n; {
stop := start + stepPerCPU
go f(start, stop, a, deltax, ch)
start = stop
}
integral := 0.0
for i := 0; i < nCPU; i++ {
integral += <-ch
}
fmt.Println(time.Now().Sub(startTime))
fmt.Println(deltax * integral)
}
func f(start, stop, a, deltax float64, ch chan float64) {
result := 0.0
for i := start; i < stop; i++ {
result += math.Sqrt(a + deltax*(i+0.5))
}
ch <- result
}
2 核输出:
nCPU = 2
2.0001ms
0.6666666685900485
8 核输出:
nCPU = 8
993µs
0.6666666685900456
您的示例代码,2 核输出:
0.6666666685900424
61.0035ms
您的示例代码,8 核输出:
0.6666666685900415
40.9964ms
2- 为了获得良好的基准统计数据,请使用大量样本(大 n):
正如您在此处看到的那样,使用 2 个内核,这需要 110ms
在 2 个内核上,但在相同的 CPU 上
使用 1 个核心需要 215ms
和 n := 10000000.0
:
使用 n := 10000000.0
和单个 goroutine,请参阅此工作示例代码:
package main
import (
"fmt"
"math"
"time"
)
func main() {
now := time.Now()
a := 0.0
b := 1.0
n := 10000000.0
deltax := (b - a) / n
result := 0.0
for i := 0.0; i < n; i++ {
result += math.Sqrt(a + deltax*(i+0.5))
}
fmt.Println(time.Now().Sub(now))
fmt.Println(deltax * result)
}
输出:
215.0123ms
0.6666666666685884
使用 n := 10000000.0
和 2 个 goroutine,请参阅此工作示例代码:
package main
import (
"fmt"
"math"
"runtime"
"time"
)
func main() {
nCPU := runtime.NumCPU()
fmt.Println("nCPU =", nCPU)
ch := make(chan float64, nCPU)
startTime := time.Now()
a := 0.0
b := 1.0
n := 10000000.0
deltax := (b - a) / n
stepPerCPU := n / float64(nCPU)
for start := 0.0; start < n; {
stop := start + stepPerCPU
go f(start, stop, a, deltax, ch)
start = stop
}
integral := 0.0
for i := 0; i < nCPU; i++ {
integral += <-ch
}
fmt.Println(time.Now().Sub(startTime))
fmt.Println(deltax * integral)
}
func f(start, stop, a, deltax float64, ch chan float64) {
result := 0.0
for i := start; i < stop; i++ {
result += math.Sqrt(a + deltax*(i+0.5))
}
ch <- result
}
输出:
nCPU = 2
110.0063ms
0.6666666666686073
1- Goroutines的数量有一个最佳点,从这个点开始增加Goroutines的数量并不会减少程序的执行时间:
在 2 个核心 CPU 上,使用以下代码,结果是:
nCPU: 1, 2, 4, 8, 16
Time: 2.1601236s, 1.1220642s, 1.1060633s, 1.1140637s, 1.1380651s
如您所见,从 nCPU=1
到 nCPU=2
时间减少足够大,但在这一点之后就不再多了,所以 nCPU=2
on 2 Cores CPU 是最佳选择指向此示例代码,因此在这里使用 nCPU := runtime.NumCPU()
就足够了。
package main
import (
"fmt"
"math"
"time"
)
func main() {
nCPU := 2 //2.1601236s@1 1.1220642s@2 1.1060633s@4 1.1140637s@8 1.1380651s@16
fmt.Println("nCPU =", nCPU)
ch := make(chan float64, nCPU)
startTime := time.Now()
a := 0.0
b := 1.0
n := 100000000.0
deltax := (b - a) / n
stepPerCPU := n / float64(nCPU)
for start := 0.0; start < n; {
stop := start + stepPerCPU
go f(start, stop, a, deltax, ch)
start = stop
}
integral := 0.0
for i := 0; i < nCPU; i++ {
integral += <-ch
}
fmt.Println(time.Now().Sub(startTime))
fmt.Println(deltax * integral)
}
func f(start, stop, a, deltax float64, ch chan float64) {
result := 0.0
for i := start; i < stop; i++ {
result += math.Sqrt(a + deltax*(i+0.5))
}
ch <- result
}
我尝试同时计算积分,但我的程序最终比使用普通 for 循环计算积分慢。我做错了什么?
package main
import (
"fmt"
"math"
"sync"
"time"
)
type Result struct {
result float64
lock sync.RWMutex
}
var wg sync.WaitGroup
var result Result
func main() {
now := time.Now()
a := 0.0
b := 1.0
n := 100000.0
deltax := (b - a) / n
wg.Add(int(n))
for i := 0.0; i < n; i++ {
go f(a, deltax, i)
}
wg.Wait()
fmt.Println(deltax * result.result)
fmt.Println(time.Now().Sub(now))
}
func f(a float64, deltax float64, i float64) {
fx := math.Sqrt(a + deltax * (i + 0.5))
result.lock.Lock()
result.result += fx
result.lock.Unlock()
wg.Done()
}
除非 goroutine 中 activity 花费的时间比切换上下文、执行任务和使用互斥锁更新值所需的时间多得多,否则这样做会更快连续。
看看稍微修改过的版本。我所做的只是在 f()
函数中添加 1 微秒的延迟。
package main
import (
"fmt"
"math"
"sync"
"time"
)
type Result struct {
result float64
lock sync.RWMutex
}
var wg sync.WaitGroup
var result Result
func main() {
fmt.Println("concurrent")
concurrent()
result.result = 0
fmt.Println("serial")
serial()
}
func concurrent() {
now := time.Now()
a := 0.0
b := 1.0
n := 100000.0
deltax := (b - a) / n
wg.Add(int(n))
for i := 0.0; i < n; i++ {
go f(a, deltax, i, true)
}
wg.Wait()
fmt.Println(deltax * result.result)
fmt.Println(time.Now().Sub(now))
}
func serial() {
now := time.Now()
a := 0.0
b := 1.0
n := 100000.0
deltax := (b - a) / n
for i := 0.0; i < n; i++ {
f(a, deltax, i, false)
}
fmt.Println(deltax * result.result)
fmt.Println(time.Now().Sub(now))
}
func f(a, deltax, i float64, concurrent bool) {
time.Sleep(1 * time.Microsecond)
fx := math.Sqrt(a + deltax*(i+0.5))
if concurrent {
result.lock.Lock()
result.result += fx
result.lock.Unlock()
wg.Done()
} else {
result.result += fx
}
}
加上延迟,结果如下(并发版本快很多):
concurrent
0.6666666685900424
624.914165ms
serial
0.6666666685900422
5.609195767s
没有延迟:
concurrent
0.6666666685900428
50.771275ms
serial
0.6666666685900422
749.166µs
如您所见,完成一项任务所需的时间越长,如果可能的话,同时执行它就越有意义。
3- 为了提高性能,您可以在不使用 lock sync.RWMutex
的情况下按 CPU 个核心划分任务:
+30x
使用通道和 runtime.NumCPU()
的优化,这需要 2ms
2 核和 993µs
8 核,而您的示例代码需要 61ms
2 核和 40ms
8 核:
查看此工作示例代码和输出:
package main
import (
"fmt"
"math"
"runtime"
"time"
)
func main() {
nCPU := runtime.NumCPU()
fmt.Println("nCPU =", nCPU)
ch := make(chan float64, nCPU)
startTime := time.Now()
a := 0.0
b := 1.0
n := 100000.0
deltax := (b - a) / n
stepPerCPU := n / float64(nCPU)
for start := 0.0; start < n; {
stop := start + stepPerCPU
go f(start, stop, a, deltax, ch)
start = stop
}
integral := 0.0
for i := 0; i < nCPU; i++ {
integral += <-ch
}
fmt.Println(time.Now().Sub(startTime))
fmt.Println(deltax * integral)
}
func f(start, stop, a, deltax float64, ch chan float64) {
result := 0.0
for i := start; i < stop; i++ {
result += math.Sqrt(a + deltax*(i+0.5))
}
ch <- result
}
2 核输出:
nCPU = 2
2.0001ms
0.6666666685900485
8 核输出:
nCPU = 8
993µs
0.6666666685900456
您的示例代码,2 核输出:
0.6666666685900424
61.0035ms
您的示例代码,8 核输出:
0.6666666685900415
40.9964ms
2- 为了获得良好的基准统计数据,请使用大量样本(大 n):
正如您在此处看到的那样,使用 2 个内核,这需要 110ms
在 2 个内核上,但在相同的 CPU 上
使用 1 个核心需要 215ms
和 n := 10000000.0
:
使用 n := 10000000.0
和单个 goroutine,请参阅此工作示例代码:
package main
import (
"fmt"
"math"
"time"
)
func main() {
now := time.Now()
a := 0.0
b := 1.0
n := 10000000.0
deltax := (b - a) / n
result := 0.0
for i := 0.0; i < n; i++ {
result += math.Sqrt(a + deltax*(i+0.5))
}
fmt.Println(time.Now().Sub(now))
fmt.Println(deltax * result)
}
输出:
215.0123ms
0.6666666666685884
使用 n := 10000000.0
和 2 个 goroutine,请参阅此工作示例代码:
package main
import (
"fmt"
"math"
"runtime"
"time"
)
func main() {
nCPU := runtime.NumCPU()
fmt.Println("nCPU =", nCPU)
ch := make(chan float64, nCPU)
startTime := time.Now()
a := 0.0
b := 1.0
n := 10000000.0
deltax := (b - a) / n
stepPerCPU := n / float64(nCPU)
for start := 0.0; start < n; {
stop := start + stepPerCPU
go f(start, stop, a, deltax, ch)
start = stop
}
integral := 0.0
for i := 0; i < nCPU; i++ {
integral += <-ch
}
fmt.Println(time.Now().Sub(startTime))
fmt.Println(deltax * integral)
}
func f(start, stop, a, deltax float64, ch chan float64) {
result := 0.0
for i := start; i < stop; i++ {
result += math.Sqrt(a + deltax*(i+0.5))
}
ch <- result
}
输出:
nCPU = 2
110.0063ms
0.6666666666686073
1- Goroutines的数量有一个最佳点,从这个点开始增加Goroutines的数量并不会减少程序的执行时间:
在 2 个核心 CPU 上,使用以下代码,结果是:
nCPU: 1, 2, 4, 8, 16
Time: 2.1601236s, 1.1220642s, 1.1060633s, 1.1140637s, 1.1380651s
如您所见,从 nCPU=1
到 nCPU=2
时间减少足够大,但在这一点之后就不再多了,所以 nCPU=2
on 2 Cores CPU 是最佳选择指向此示例代码,因此在这里使用 nCPU := runtime.NumCPU()
就足够了。
package main
import (
"fmt"
"math"
"time"
)
func main() {
nCPU := 2 //2.1601236s@1 1.1220642s@2 1.1060633s@4 1.1140637s@8 1.1380651s@16
fmt.Println("nCPU =", nCPU)
ch := make(chan float64, nCPU)
startTime := time.Now()
a := 0.0
b := 1.0
n := 100000000.0
deltax := (b - a) / n
stepPerCPU := n / float64(nCPU)
for start := 0.0; start < n; {
stop := start + stepPerCPU
go f(start, stop, a, deltax, ch)
start = stop
}
integral := 0.0
for i := 0; i < nCPU; i++ {
integral += <-ch
}
fmt.Println(time.Now().Sub(startTime))
fmt.Println(deltax * integral)
}
func f(start, stop, a, deltax float64, ch chan float64) {
result := 0.0
for i := start; i < stop; i++ {
result += math.Sqrt(a + deltax*(i+0.5))
}
ch <- result
}