8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Golangでベクトル演算を高速化しようとしてみた話

Last updated at Posted at 2019-10-31

はじめに

Goでスライスのベクトル演算などを書いてたときに、今までは

for i := 0; i<len(slice); i++ {...}

のように素直にfor文で全要素にアクセスしてました。

もっと速い演算方法は無いかと考えていたところ、

for i := 0; i<cpus; i++ {
    go func{...}
}

のようにcpuコア数に合わせてsliceを分割し、goroutineで並行処理する方法を思いつきました。

ということで、実際に速度の検証をしてみました。

結論から言うと...

むしろ遅くなりました。

2019/11/01追記

要素数10万以上では、goroutineを使用したほうが速くなりました。(環境によります。)

検証コードはスライスに数字を代入しているだけなので、演算時間が逆転する要素数が多くなっています。 しかし、べき乗や平方根を求めるなどの重い処理をする場合、goroutineの旨みが増すことが予想できます。

以下は検証のコードなので、興味のある方は見て頂けると幸いです。

環境

  • MacBook Pro 2019
  • macOS Catalina
  • 2.4 GHz Quad-Core Intel Core i5
  • Go 1.13.1

検証

素直にfor文

func main() {
	n := 100000000
	is := make([]int, n)
	fmt.Println("==START==")

	start := time.Now()
	for i := 0; i < n; i++ {
		func(i int) { is[i] = i }(i)
	}
	end := time.Now()

	fmt.Println("==END==")
	fmt.Println(end.Sub(start))
}
$ go run main.go
==START==
==END==
309.95595ms

1億回で310msでした。

cpu数に合わせて並行処理

func main() {
	fmt.Printf("Number of cpus: %v\n", runtime.NumCPU())
	wg := &sync.WaitGroup{}
	cpus := runtime.NumCPU()
	n := 100000000
	is := make([]int, n)
	fmt.Println("==START==")

	start := time.Now()
	jMax := n / cpus
	for i := 0; i < cpus; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			/* 遅いコード
			   for j := 0; j < n/cpus; j++ {
			       is[i*n/cpus+j] = i*n/cpus + j
			   }
			*/
			offset := i * jMax
			for j := 0; j < jMax; j++ {
				is[offset+j] = offset + j
			}
		}(i)
	}
	wg.Wait()
	end := time.Now()

	fmt.Println("==END==")
	fmt.Println(end.Sub(start))
}
$ go run main.go
Number of cpus: 8
==START==
==END==
88.946429ms

1億回を8個のgoroutineで処理して、89msでした。
要素数が多い場合、goroutineを使用したほうがかなり速いです。

cgoで実行

/*
void vec(long *is, long n){
    for (int i=0; i<n; i++) {
        is[i] = i;
    }
}
*/
import "C"

func main() {
	fmt.Println("==START==")

	n := 100000000
	is := make([]int, n)

	start := time.Now()
	C.vec((*C.long)(unsafe.Pointer(&is[0])), (C.long)(n))
	end := time.Now()
	time.Sleep(time.Second)

	fmt.Println("==END==")
	fmt.Println(end.Sub(start))
}
$ go run main.go
==START==
==END==
271.969568ms

cgoはそこそこの結果に終わりました。

  • メモリセーフでない
  • コードが読みづらくなる

ことを考慮すると、よっぽどの理由がない限りgoroutineを使うのがよいと思います。

ちなみにpyhtonでも実行してみました。

import time
import numpy as np

print("==START==")
start = time.time()
iarr = np.arange(100000000)
duration = time.time() - start
print("==END==")

print(str(duration*1000) + "ms")
python main.py
==START==
==END==
304.9652576446533ms

書きやすさと速度を考えると、numpyは偉大ですね。

要素数を増減して検証

演算回数を1万回、10万回、100億回に変えて実行してみます。

演算回数 生go goroutine cgo
1万 0.035ms 0.087ms 0.038ms
10万 0.43ms 0.34ms 0.39ms
1億 309ms 89ms 272ms
100億 47s 38s 43s

演算回数10万回以上ではgoroutineが速くなりました。

cgoは演算回数が増えると生goよりも速くなりました。シングルコアの環境であれば、cgoを使うのも有効かもしれません。

おわりに

冒頭にも書きましたが、演算数が増えたり、内容が重くなったりするとgoroutineの効果が大きく出ると思います。

信号処理をgoで書いてるので、機会があればQiitaにも投稿しようと思います。

8
2
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?