そういえば以前、rust と pythonの比較をしたけれど、golangではあの計算をやっていなかったなと思い、ベタで計算するのと array に入れて参照するのと、ループを振り分けて goroutine を使って計算するのが、それぞれどれくらいになるかを見てみようかと、4つにわけて試してみました。
owiewowe.hatenablog.com
package main import ( "fmt""math""sync""time" ) func x5(i int) float64 { f := float64(i) return math.Pow(f, 5) } func CalcWithoutGoroutine() { start := time.Now() L: for a := 1; a <= limit; a++ { for b := a; b <= limit; b++ { for c := b; c <= limit; c++ { for d := c; d <= limit; d++ { for e := d; e <= limit; e++ { if x5(a)+x5(b)+x5(c)+x5(d) == x5(e) { fmt.Println(a, b, c, d, e) fmt.Println("CalcWithoutGoroutine() ", time.Since(start)) break L } } } } } } } func SilceWithoutGoroutine() { start := time.Now() s := [limit]float64{} for i := 0; i < limit; i++ { s[i] = x5(i + 1) } L: for a := 0; a < limit; a++ { for b := a; b < limit; b++ { for c := b; c < limit; c++ { for d := c; d < limit; d++ { for e := d; e < limit; e++ { if s[a]+s[b]+s[c]+s[d] == s[e] { fmt.Println(a, b, c, d, e) fmt.Println("SilceWithoutGoroutine()", time.Since(start)) break L } } } } } } } func CalcWithGoroutine() { start := time.Now() var wg sync.WaitGroup ch := make(chanstruct{}) LO: for a := 1; a <= limit; a++ { select { case<-ch: break LO default: } wg.Add(1) t := func(a int, ch chanstruct{}) { L: for b := a; b <= limit; b++ { for c := b; c <= limit; c++ { select { case<-ch: break L default: } for d := c; d <= limit; d++ { for e := d; e <= limit; e++ { if x5(a)+x5(b)+x5(c)+x5(d) == x5(e) { fmt.Println(a, b, c, d, e) fmt.Println("CalcWithGoroutine()", time.Since(start)) close(ch) // ここでgoroutineの数を見ると結構な数が走っている// fmt.Println(runtime.NumGoroutine())break L } } } } } wg.Done() } go t(a, ch) } wg.Wait() // goroutine終了確認用// fmt.Println(time.Since(start)) } func SliceWithGoroutine() { start := time.Now() s := [limit + 1]float64{} for i := 0; i < limit; i++ { s[i] = x5(i + 1) } var wg sync.WaitGroup ch := make(chanstruct{}) LO: for a := 0; a < limit; a++ { select { case<-ch: break LO default: } wg.Add(1) t := func(a int, ch chanstruct{}) { L: for b := a; b < limit; b++ { for c := b; c < limit; c++ { select { case<-ch: break L default: } for d := c; d < limit; d++ { for e := d; e < limit; e++ { if s[a]+s[b]+s[c]+s[d] == s[e] { fmt.Println(a, b, c, d, e) fmt.Println("SliceWithGoroutine()", time.Since(start)) close(ch) // ここでgoroutineの数を見ると結構な数が走っている// fmt.Println(runtime.NumGoroutine())break L } } } } } wg.Done() } go t(a, ch) } wg.Wait() // goroutine終了確認用// fmt.Println(time.Since(start)) } const limit = 200func main() { CalcWithoutGoroutine() SilceWithoutGoroutine() CalcWithGoroutine() SliceWithGoroutine() }
2784110133144 CalcWithoutGoroutine() 2m22.8086113s 2784110133144 SilceWithoutGoroutine()1.0819965s 2784110133144 CalcWithGoroutine()43.4378386s 2784110133144 SliceWithGoroutine()370.383ms
見にくいですが、SliceWithGoroutine() < SilceWithoutGoroutine() < CalcWithGoroutine() < CalcWithoutGoroutine() となっておりますので、 math.Pow をいちいち計算せずに計算結果の入った array を参照すると 100 倍程度、goroutine を使うと 6 コア 12 スレッドで windows11 上で golangに丸投げしても 2 - 3 倍速くなりました。正確性を問うなら benchmark を使うべきなのでしょうけれど、ここでのネタ程度ですし、桁が違うので benchmark 使わなくてもいいかなと。
goroutine 使うと 2 - 3 倍速くなるのは意外でした、とんとんか良くても 1.5 倍くらいだと。
ただ、これ主題から外れるのですが、 x5 の関数を
func x5(i int) float64 { f := float64(i) return f * f * f * f * f }
にすると、
2784110133144 CalcWithoutGoroutine()5.552252s 2784110133144 SilceWithoutGoroutine()1.0677033s 2784110133144 CalcWithGoroutine()1.9848244s 2784110133144 SliceWithGoroutine()365.7034ms
となったので array 参照は 5 倍強、goroutine を使うと 2倍強 というところでしょうか。こちらは正確に計測したいなら benchmark 使ったほうが良さそうです。
負荷掛けるのが目的でしたら、math.Pow が適切?
ここでの runtime.NumGoroutine() は一桁のこともあれば、200 近いこともあったので割と楽しめます。