Golang中的并发积分计算

时间:2016-08-21 04:41:33

标签: go concurrency integral

我试图同时计算积分,但我的程序最终比用正常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()
}

2 个答案:

答案 0 :(得分:3)

除非goroutine中的活动所花费的时间比切换上下文所需的时间长,执行任务并使用互斥锁来更新值,否则以串行方式执行它会更快。

看一下稍微修改过的版本。我所做的就是在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

正如您所看到的,如果可能的话,完成任务所需的时间越长,同时执行任务就越有意义。

答案 1 :(得分:3)

3- 为了提高性能,您可以在不使用lock sync.RWMutex 的情况下划分每个CPU内核的任务:

+30x使用频道和runtime.NumCPU()进行优化,这会在2个核心上2ms和8个核心上993µs,而您的示例代码在{2}上需要61ms 8个核心上的核心和40ms

请参阅此工作示例代码和输出:

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个核心所看到的那样,在2个核心上需要110ms,但是在同一个CPU上 使用1核心,215msn := 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个goroutines,请参阅此工作示例代码:

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 Cores CPU上,使用以下代码,结果为:

nCPU: 1,          2,          4,         8,           16
Time: 2.1601236s, 1.1220642s, 1.1060633s, 1.1140637s, 1.1380651s

正如您从nCPU=1看到nCPU=2时间减少的时间足够大但在此之后它并不多,所以2 Cores上的nCPU=2 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
}