考えてみると初期のGo言語から触ってるが残念で放置していた部分を休日を利用して整理してみた。
そもそもGo言語はforループが遅かった。
初期の頃は遅い理由としてループ変数のリストを先に生成しているのだと説明を聞いたことがある。つまり,0~5のループを回すときに,メモリ上にリスト{0,1,2,3,4,5}を生成していたのである。
その後さすがに修正されたが,面白いことに1.21から1.22でスコープの問題で再度見直されることになった。これは並行計算を実際にコーディングしている人は経験しているだろう。例を挙げよう
for i := range 5 {
go func() { fmt.Println(i) }()
}
上記のプログラムでループ変数iをGoroutine内で参照しているが,この参照時に既にiがインクリメントされていることがある。
で,1.22以降ではこの参照されるiのアドレスをループ中で別のものになる実装となった。初期の思想に戻った感じでもある。速度面のペナルティより安全側をとったということだろう。
これはコンパイルオプションloopvarで選択できる。リストが巨大な場合など特殊なケースではパフォーマンスに大きな影響が出る。
次に境界チェックである。
これはループに限らないが主にループ内で多大な影響になる。
golangでBCEを意識してスライスへのアクセスを速くする #Go - Qiita
実はこれはコンパイルオプション-gcflags="-B"でチェックしないようにできる。
場合によると10%以上の高速化となる。
最後にループ展開である。
今のCPUはレジスタ数や演算器数が多く同時処理ができるものが多いので,コンパイラ側で自動ループ展開するのが一般的になっている。
が,何故かGo言語ではこれを未だに行わないので場合によっては手動でソースを弄りループ展開してやると高速化することがある。
これも議論には何度も上がっているので今後のバージョンで対応されるかもしれない。
手元で試したものでは4命令展開や8命令展開まで高速化したケースがあった。
実効環境毎に最適形が異なるのでソースを弄るのは美しくない。
低レイヤコーディングを人間が行うことは減っているらしいので,今後はどうなることやら
注意点ばかり挙げたので否定的な話に思えるが,実際のところはバージョンアップのたびに少しずつ性能改善があるので初期から比べるとループは随分と速くなっている。