go用协程优化串行的坑

开协程比串行更慢的情况: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()
		}
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值