开协程比串行更慢的情况:2e8次时 不开协程需要0.2s,而开协程需要1.1s
package tests
import (
"fmt"
"github.com/kevwan/mapreduce"
"testing"
)
type node struct {
x int64
y int64
}
var round int = 2e8
func addx(a *node) {
for i := 0; i < round; i++ {
a.x += 1
}
}
func addy(a *node) {
for i := 0; i < round; i++ {
a.y += 1
}
}
func TestGo(t *testing.T) {
var a node
mapreduce.FinishVoid(func() {
addx(&a)
}, func() {
addy(&a)
})
fmt.Println(a)
}
对以上代码稍作改动,运行时间变为0.05s:
package tests
import (
"fmt"
"github.com/kevwan/mapreduce"
"testing"
)
type node struct {
x int64
y int64
}
var round int = 2e8
func addx() int64 {
var x int64 = 0
for i := 0; i < round; i++ {
x += 1
}
return x
}
func addy() int64 {
var y int64 = 0
for i := 0; i < round; i++ {
y += 1
}
return y
}
func TestGo(t *testing.T) {
var a node
mapreduce.FinishVoid(func() {
a.x = addx()
}, func() {
a.y = addy()
})
fmt.Println(a)
}
这其中的原因涉及到计算机组成原理中的cache。。。
CPU 缓存的目的
cache是介于CPU和主存之间的小容量存储器,其存取速度比主存快,但容量远小于主存。cache能高速地向CPU提供指令和数据,加快程序的执行速度。这样在读取或写入时,CPU 都不必每次都到达主存。
CPU 的每个核都有自己的本地缓存,不与其他核共享。如果有n 个 CPU 内核,意味着最多可以有 n + 1 个相同数据的副本:一个在主存中,剩下的在每个 CPU 内核的缓存中。
当 CPU 内核更改其本地缓存中的值时,必须在某个时刻将其同步回主存。同样,如果缓存的值在主存中被更改(由另一个 CPU 内核),则缓存的值失效,需要从主内存刷新。
为了以有效的方式同步高速缓存和主存储器,数据以通常 64 字节的块同步,这些块称为缓存行。当缓存值更改时,整个缓存行将同步回主存。同样,包含此缓存行的所有其他 CPU 内核的高速缓存也必须同步此缓存行以避免对旧数据进行操作。
如果并发循环操作的元素存储在连续的空间中,那么相邻的元素可能会共享相同的高速缓存行,比如同一切片中相邻的元素,比如同一结构体中的不同成员。
现在好戏开始了,n 个具有高速缓存的 CPU 内核读取和操作同一高速缓存行中的元素,只要一个 CPU 内核更新其中的元素,所有其他 CPU 内核的高速缓存行都会失效。必须先将新的的高速缓存行写回主存,然后所有其他 CPU 内核再从主存更新其各自的高速缓存行。
这就是最开始的代码并发循环比串行还慢的原因,对结构体变量a的并发操作导致频繁的缓存行同步更新。即使每个内核访问a的不同成员!
那么解决方案就是我们必须将操作对象转换为 n 个单独的变量,这些变量被隔离存储,以便它们不共享相同的高速缓存行。
另外,由于启动一个新的 Goroutine 会在堆栈上分配 2KB 到 8KB 的数据,而局部变量只会在创建它的 Goroutine 之中被引用,它不会转移到堆。所以我们可以肯定各个 Goroutine 中的局部变量不会在同一个缓存行。
所以千万不要出现类似这样的代码:
package tests
import (
"fmt"
"github.com/kevwan/mapreduce"
"testing"
)
func TestGo(t *testing.T) {
var x, y int
mapreduce.FinishVoid(func() {
for i := 0; i < 2e8; i++ {
x += 1
}
}, func() {
for i := 0; i < 2e8; i++ {
y += 1
}
})
fmt.Println(x, y)
}
必须改成类似这样的形式:
package tests
import (
"fmt"
"github.com/kevwan/mapreduce"
"testing"
)
func TestGo(t *testing.T) {
var xx, yy int
mapreduce.FinishVoid(func() {
x := 0
for i := 0; i < 2e8; i++ {
x += 1
}
xx = x
}, func() {
y := 0
for i := 0; i < 2e8; i++ {
y += 1
}
yy = y
})
fmt.Println(xx, yy)
}
如果想要对列表中的每一个元素执行相同的操作:
并行处理列表:
package tool
import (
"runtime"
"sync"
)
type DoWithEach func(index int) error
func ParallelDealList(length int, doWithEach DoWithEach) error {
var wg sync.WaitGroup
var num = runtime.NumCPU()
dealATime := length / num
var f = func(i int, wg *sync.WaitGroup) error {
defer wg.Done()
start := dealATime * i
var end int
if i == num-1 {
end = length
} else {
end = dealATime * (i + 1)
}
for j := start; j < end; j++ {
err2 := doWithEach(j)
if err2 != nil {
return err2
}
}
return nil
}
var err error
for i := 0; i < num; i++ {
wg.Add(1)
i1 := i
go func() {
err2 := f(i1, &wg)
if err2 != nil {
err = err2
}
}()
}
wg.Wait()
return err
}
type DoWithEachVoid func(index int)
func ParallelDealListVoid(length int, doWithEachVoid DoWithEachVoid) {
var num = runtime.NumCPU()
dealATime := length / num
var f = func(i int, wg *sync.WaitGroup) {
start := dealATime * i
var end int
if i == num-1 {
end = length
} else {
end = dealATime * (i + 1)
}
for j := start; j < end; j++ {
doWithEachVoid(j)
}
wg.Done()
}
var wg sync.WaitGroup
for i := 0; i < num; i++ {
wg.Add(1)
go f(i, &wg)
}
wg.Wait()
}
测试代码:
func TestItoA(t *testing.T) {
var length int = 1e7
var s = make([]string, length)
var x = make([]int, length)
ParallelDealListVoid(length, func(index int) {
x[index] = index
})
ParallelDealListVoid(length, func(index int) {
s[index] = strconv.Itoa(x[index])
})
for i := 0; i < length; i += 2e3 {
if s[i] != strconv.Itoa(x[i]) {
t.Error()
}
}
}