この記事は「Go Advent Calendar 2022 3」の3日の記事です!
前説
LambdaをGoを使って何かをしたいときにarm64 アーキテクチャを使うと早くて安くなるという情報を得ていたので調べてみた。
結論
- CPUバウンドな処理じゃなければある気軽に乗り換えることで安く、早いを実現できそう
- ただし何でもかんでもx86_64->arm64にすれば全てに置いて早くなるわけではない(遅くなったり同一性能を確保するにはスペックアップして高くなるケースもありそう)
- ワークロードごとに性能は千差万別になるため、ベンチマークしてみよう!!!!!!1
- Graviton3で大分改善するようなレポートが多いのでLambdaでGraviton3が使えるようになってから再度検討でもよいかも
テスト用コード
とりあえずインクリメントするだけのプログラムです。後でアセンブリを出力するのでLambdaに関係する処理の部分は省いています。(importとかHandlerあたりの処理)。コンパイルはGOOS=linux GOARCH=arm64 go build -o bootstrap -gcflags=-m -tags lambda.norpc -ldflags="-w -s"
でGOARCHの部分を書き換えて実行しています。
package main func main() { var sum int for i := 0; i < 100000000; i++ { sum += i } }
測定結果
メモリは1024MBで検証。「Custom runtime on Amazon Linux 2」×「arm64」が一番遅かった。そうなの?
ランタイム | アーキテクチャ | 結果 |
---|---|---|
Go 1.x | x86_64 | 545.46[ms] |
Custom runtime on Amazon Linux 2 | x86_64 | 442.96[ms] |
Custom runtime | x86_64 | 448.71[ms] |
Custom runtime on Amazon Linux 2 | arm64 | 968.35[ms] |
Custom runtime | arm64 | 非サポート |
ちなみにLambdaは与えたメモリ量でCPUのコア数だけじゃなく周波数も変わってくるらしく倍の2048MBをarm64に設定したところ実行時間が半分になることがわかりました。(CPUクロック周波数だけじゃなくNW帯域も変わるらしい)
なぜ遅いのかの考察
いろいろ調べてみたが根本的な要因はわからなかったので考察を書いてみる。今回の検証プログラムはシングルスレッドで大量に計算させるプログラムとなっておりarm64のシングルスレッド性能がx86_64のものより低いのが原因ではないかと思われる。arm64とx86_64で使われているCPUについて調べてみると1vCPUあたりの物理コアの数が違うが周波数は同じであるということがわかりました。周波数同じでもアーキテクチャの違いでここまで差が出るものと言うのはちょっと意外でした(全然わからないですがこの辺はCISCとかRISCとかの設計思想の違いによったりするのでしょうか?)
HyperThreadingがないというのはとてもおもしろいなと思いました。
他の性能検証記事を見てみる
上記の記事の検証ではIntel/AMDと比べると多スレッド数(32以上とか)ほど性能を発揮となっている。ベンチマーク結果の所見ると今回私がした検証以上に劣化している箇所もあったりして「まじか...」となった。ただソートや小数点演算はx86_64のものより数値がよく出ていてこの辺は前評判通りの内容だなと思いました。あと暗号化だけ極端に遅いのは何故なんでしょうか?(zlibについては最適化がデフォルトだと効いていないとのことでこの辺のビルドオプションをつけてビルドしたら変わってくるのだろうか?)
暗号化はアルゴリズムによって性能差がすごいようでtlsを終端するNginxとかだと移行する際はきちんと検証しないと事故が起きそうですね。
Percona社が行った検証の結果。EC2 インスタンスにMySQLを入れて負荷テストツールを使って検証しているものでテスト中に I/O アクティビティはなしとしている。スレッド数が vCPU の以下の場合ほとんどの場合でx86_64の方がスループットもレイテンシも良い数値が出ていました。やはりシングルスレッド性能ではx86_64の方がよいみたいです。マルチスレッドでvCPU以上になってくるとarm64の方が性能が良くなっていくようです。これはx86_64が2vCPU = 1物理コアであるという点からみても納得の結果でした。(RDSをGravitonにするかは同時実行数が多いワークロードであればGravitonを同一スペックで選択することでコストカットができそうですね。ただ1クエリあたりの実行時間が伸びてしまう可能性があるのでそのへんは検証しないとですかねぇ)
こちらは早くなってさらに安くなったデータが有りました。ロードバランサーやアプリケーションサーバを乗り換えてたとのことですが(内容はわからないですが)これらはマルチコアを使うようなワークロードだと思うので結果がこのようになったのかなと思います。
上記はRedisの検証を行っている記事。m6g.largeとm5.largeの比較で若干m6g.largeの方が劣っているがほぼ誤差程度の模様でした。Redisは6.0からマルチスレッド対応が入ったのでスループットについてはこの辺をチューニングしていくことで良い数値が出るのでは無いでしょうか?同時接続数が多いならこのケースでもレイテンシは低くなりそうですね。
気軽に始めるGraviton2マネージドサービスによるコスト最適化 / Amazon Game Tech Night #23 - Speaker Deck
Redisあたりはレイテンシが低くなりつつ安くなったとのこと。Lambdaでarm64でもCPUバウンドな処理が少なければ安くパフォーマンス劣化もなしのようです。なるほどなぁ。
どのへんで使えるんだろう?
CPUバウンドじゃないかつマルチコアを活かすようなケースで効果を発揮するのであれば、MySQL, memcached, nginx(sslオフロードなし), Redis(マルチスレッド有効)あたりだろうか。あとは処理時間を気にしないバッチ処理とかは単純に同一スペックで置き換えることでコストダウンになるのでそれはそれでとてもよい。
まとめ
導入しても思ったような結果にならないケースもあるようなので事前にきちんと検証が必要そうですね。
おまけ1 Graviton3もあるらしい
周波数やメモリIOあたりでだいぶ改善が行われているようで今回やった検証も全然違う結果になる可能性がありそうですね。暗号化なんかや大分改善したとの話もあるようです。汎用処理25%、浮動小数点2倍、機械学習処理3倍、暗号処理3倍、エネルギー効率60%向上。これがどのくらいなのかはとても気になる。まだLambdaには来てないので来るのを待つしかとりあえず無いっぽいです。これは楽しみ。
おまけ2 アセンブリ
私自身アセンブリは1mmも理解していないがせっかくなので見てみました。ビルドオプションを変えたバイナリをそれぞれ出力して行数を見て見ると以下のようになっていました。CISC(シスク)、RISC(リスク)とは、命令セットアーキテクチャの設計手法を指す言葉でCISCは「複雑な命令、複雑な体系複雑なことを1つの命令で行う設計手法」に対してRISCは「簡単な命令、簡単な体系複雑なことを命令の組合せで行う設計手法」となっています。x86_64はCISCでARMはRISCなのでそのへんの違いが出てくるのかなと思いました。ただ命令数が少ない=高速ではない(複雑な命令であればそれに対しては時間がかかる)のでここから何かを言うことは出来ないという感じでした。CPU難しい。
# arm64 $ wc -l a.s 518875 # amd64 $ wc -l x.s 96403
おまけ3 ループ最適化
コンパイラはループの最適化を行う場合がありGolangでもそれが起きていたのではと思ったのが最初の一手目でした。以下のコードを見てもらうとわかるのですがsumに対して演算してしてもprintすらもしてないので実はこのループ自体は不要な処理です。x86_64でビルドすると最適化されてしまうのでは?と思ったのでした。ただ上記で見たアセンブリでは特にそのようなことは起きていないことがわかりました。
package main func main() { var sum int for i := 0; i < 100000000; i++ { sum += i } }
ちなみにgccで-O3をつけてコンパイルするとループ部分が最適化により取り除かれます。(以下はobjdumpの結果)
a.o: file format elf64-x86-64 Disassembly of section .text.startup: 0000000000000000 <main>: int main(int argc, char **argv) { 0: f3 0f 1e fa endbr64 int sum; for(int i; i<1000000000;i++) { sum+=i; } } 4: 31 c0 xor %eax,%eax 6: c3 ret