これは何?
先日、go で巨大な slice を作ると遅いことに気がついたので、どれぐらい遅いのかを調べようと思い、今日も楽しいマイクロベンチマーク。
登場人物
コンピュータは
- MacBook Pro M1 (非Max)。
- Raspberry Pi 3B+ with 32bit Raspberry Pi OS
- Raspberry Pi 3B+ with 64bit Raspberry Pi OS
の三者。
go は、どこでも go 1.21。
C++ も使うんだけど、こちらは macOS では Apple Clang 15。ラズパイでは gcc-10。
調べたこと
make([]uint8, n)
、malloc(n)
、std::vector<char>(n);
などの方法でメモリ確保っぽい処理をおこない、その時間を測り、グラフにした。代表値は 5回やって中央値を採用。
メモリの確保っぽい処理
メモリの確保っぽい処理は、以下の通り。
-
make([]uint8, n)
on go -
make([]uint8, 0, n)
on go -
malloc
on C++ -
calloc
on C++ -
vector<char>(n)
on C++ -
vector<char>()
→reserve(n)
on C++ -
vector<char>()
→resize(n)
on C++ -
new char[n]
on C++
調査結果
縦軸は時間。単位はミリ秒。対数目盛注意。
横軸は確保したメモリの指数。例えば 10 だったら 2の10乗 バイト。
C++ on macOS
2の14乗 バイト前後で上に行っているのがゼロで埋める人たち。下に行っているのが 値が不定で良いと思っている人たち。
それはいいんだけど、2の18乗 バイト付近で calloc
が下に行く。これは OS に「初めからゼロで埋まっていることがわかっているメモリを提供する」という機能があるから。この機能はある程度大きなメモリブロックでしか使えないのでそれまではチマチマゼロ埋めするんだけど、ある大きさ以上はチマチマゼロ埋めしなくて良くなるので calloc
が急に速くなる。
vector
の reserve
は、メモリの確保するだけで値は未初期化で放置になる。
vector
は末尾以降へのアクセスが違法なのでこれでよい。
あと。 vector<char>(n)
が calloc
にならないのはちょっと残念。
Go on macOS
一方 go は、make([]uint8,0,n)
でも make([]uint8,n)
と同じだけ時間がかかる。
これは、メモリ確保済みであれば スライスの末尾より先のメモリへのアクセスが合法だから仕方ない。C/C++ の malloc
に相当するような「メモリ確保はするけど値の初期化はしないでね」に相当する処理は言語機能としては存在しないと思ってるんだけどどうだろう。
そして calloc
を使ってくれるということもないので、巨大スライスの確保はどうやっても遅い。
C++ on ラズパイ64bit
macOS ほど見やすくはないけど、同じ傾向。
Go on ラズパイ64bit
mac と同じ。サイズが大きいと時間はサイズに比例する感じ。
C++ on ラズパイ32bit
やはり mac と同じ。calloc
が途中で ゼロ初期化必要チーム から離脱して malloc
の仲間になる。
Go on ラズパイ32bit
こちらも mac と同じく、サイズゼロにしてもキャパシティが大きいと遅くなる。
まとめ
go で巨大スライスを確保する場合。
ゼロ埋めが要素数分走るので、どうやっても遅い。ということだと思う。
C/C++ だと malloc
の様に未初期化で放置という手段もあるし、calloc
の様に OS の不思議な力で一瞬で終わらせるという手段も(OSによっては)ある。go にはそういう選択肢はない。
こういうところで、go はスピード狂向けじゃないんだなと思う。
あと。
コードは
https://github.com/nabetani/allocbench
に置いた。