コンテンツへスキップ

Go言語の並行処理の書き方と最大のパフォーマンスを引き出すテクニック

この記事は株式会社 株式会社Gincoのテックブログとして書いています。

概要

並行処理は、タスクを複数CPUで同時に稼働させて、結果としてシステム全体の実行時間を短縮する、というアプローチです。 
この記事ではgoの並行処理のコードのサンプルを示し、実行環境のCPUコア数と並列数の最適値の関係を調べてみました。

並行性と並列性

並行性はコードの性質を指し、並列性は動作しているプログラムの性質を指します。

(引用元: Go言語による並行処理)

  • 並行性
    • 並列に実行して欲しいと思うコードの書き方のこと
  • 並列性
    • 複数の処理が同時に実行されること

2つの領域を利用する並行なコードを書いても、実行環境が1コアだったら、2つの領域は素早く切り替えて逐次動作します。実行環境が2コアだったら2つの領域は並列に動作します。

gantt title CPUコア数による並行処理の振る舞いの違い dateFormat HH:mm axisFormat %H:%M section 1コアの場合 task-1 :t1, 00:00, 1m task-2 :t2, after t1 , 1m task-3 :t3, after t2 , 1m task-4 :t4, after t3 , 1m section 2コアの場合 task-1 :t11, 00:00, 1m task-2 :t12, 00:00, 1m task-3 :t13, after t11 , 1m task-4 :t14, after t12 , 1m

並行処理のサンプルコード

package main

import (
    "sync"
    "testing"
    "time"
)

func BenchmarkFn(b *testing.B) {
    workers := 2 // 並列数を変更する

    ch := make(chan int, workers)

    var wg sync.WaitGroup

    for i := 0; i < workers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for range ch {
                // 1. CPUリソースに依存する処理
                // 素数を見つける
                // findPrimeNumbers(10000)
                // 2. CPUリソースに依存しない処理
                // 10ミリ秒待つことで処理を模擬する
                time.Sleep(200 * time.Millisecond)
            }
        }()
    }

    for i := 0; i < 1000; i++ {
        ch <- i
    }
    close(ch)

    wg.Wait()

}

// mが素数かどうかを判定する
func checkPrimeNumber(m int) bool {
    for i := 2; i < m; i++ {
        if m%i == 0 {
            return false
        }
    }
    return true
}

// n以下の素数を全て見つける
func findPrimeNumbers(n int) []int {
    primeList := make([]int, 0)
    for m := 2; m < n; m++ {
        if checkPrimeNumber(m) {
            primeList = append(primeList, m)
        }
    }
    return primeList
}

実行

go test -bench=. -cpu 8

実行結果

goos: darwin
goarch: amd64
pkg: go-routine
cpu: VirtualApple @ 2.50GHz
BenchmarkFn-8                  1        100473714792 ns/op
PASS
ok      go-routine      100.923s

コードの補足

sync.WaitGroupを使用しています。
WaitGroupは複数の並行処理があったとき、それらの処理の完了を待つ手段を簡単に提供してくれます。
処理の実行順序の保証はありません。

パフォーマンス計測

並行処理の中身が 1. CPUリソースに依存する処理2. CPUリソースに依存しない処理 のどちらかによって最適化の方針が変わります。

1. CPUリソースに依存する処理

CPUリソースに依存する処理を並行処理で実行したときの処理時間をプロットしました。横軸に並列数、縦軸に処理時間をとっています。

並列数=CPUコア数となるときに最速になりました。CPUリソースがボトルネックになるので、CPUコア数を超える並列数にしてもパフォーマンスは向上しませんでした。

こちらのケースでは、パフォーマンス向上のためには実行環境のCPUコア数と並列数の両方をチューニングすることが必要です。

2.CPUリソースに依存しない処理

続いて、CPUリソースに依存しない処理を並行処理で実行したときの処理時間をプロットしました。

こちらは、実行環境のCPUコア数に関わらず、並列数だけ増やせばパフォーマンスが上がって行きました。

まとめ

この記事では、goの並行処理のコードのサンプルを示し、実行環境のCPUコア数と並列数の最適値の関係を調べました。

わかったこと

  • CPUリソースに依存する処理の場合、並列数=実行環境のCPUコア数付近のときに最もパフォーマンスが良くなる。それ以上並列数を増やしてもパフォーマンスは向上しない。
  • CPUリソースに依存しない処理の場合、実行環境のCPUコア数に関わらず、並列数を増やせばパフォーマンスが向上する。
  • CPUリソースに依存する処理とCPUリソースに依存しない処理が合わさっている場合、どちらが優位かを念頭に置きながら実行環境のCPUコア数と並列数をチューニングする。

株式会社 Ginco ではブロックチェーンを学びたい方、ウォレットについて詳しくなりたい方を募集していますので下記リンクから是非ご応募ください。

株式会社Ginco の求人一覧

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です