★記事更新しました (2022/09/24 23:00更新)
比較ソースがGoとC#とで差異があったため、再測定を行い、記事更新しました。
修正前ソースは、for文のループ変数の型がintになっており、内部的にはGoはint641、C#はint32で差異がありました。
記事本文は、以下3つのソースの測定結果を並記しております。(③が更新前の記載内容)
→変数の型が、①int64 ②int32 ③int (Goはint64、C#はint32)
以下、本文です。
GoとC#の速度比較を行いました。
普段C#を扱っているのですが、Goを使う機会があり、Goは早いと噂なので実際に測ってみたのが動機です。
計測内容
2~Nの整数が素数かどうか、愚直にループと判定を行う処理をGoとC#で実行しました。
- 変数の型が以下3パターンのソースで実施。
- int64 →ソース:Github(Int64ブランチ)
- int32 →ソース:Github(Int32ブランチ)
- int (Goはint641、C#はint32) →ソース:Github(Intブランチ)
- 測定パターン(各ソースに対して実施)
- 論理コア数8 Windows 直列処理
- 論理コア数8 Windows 並列処理
- 論理コア数1 Windows 直列処理
- 論理コア数1 Linux 直列処理
- 論理コア複数のパターンは、パフォーマンスモニターで各論理コアのCPU使用率を確認。
- 処理時間は5回計測した平均を算出。
- 求める素数の最大値Nは、100,000, 500,000, 1,000,000 の3パターンを測定。
(測定パターンについて補足)
・論理コア複数 直列で測定時に、CPU負荷のかかり方にGoとC#で差異があったため、
1コアの環境で測定も行いました。
・Linuxでも実施しているのは興味本位です。
動作環境
ソフト環境
- Go
- Go 1.18.4
- C#
- .NET6 (6.0.302)
- Releaseビルド
ハード環境
- 論理コア数8 Windows
- CPU: Intel(R) Core(TM) i7-8550U CPU @1.80GHz 1.99 GHz
- Windows10
- 論理コア数1 Windows
- AWS EC2 t2.micro ※バーストクレジット残数あり(CPUを100%使用可能)で測定
- Windows Server 2022 (AMI: Windows_Server-2022-English-Full-Base-2022.08.10)
- 論理コア数1 Linux
- AWS EC2 t2.micro ※バーストクレジット残数あり(CPUを100%使用可能)で測定
- Amazon Linux 2 (AMI: amzn2-ami-kernel-5.10-hvm-2.0.20220805.0-x86_64-gp2)
結果
処理時間
変数の型:int64
いずれの測定パターンにおいても、C#がGoより若干だけ速い 結果となりました。
変数の型:int32
いずれの測定パターンにおいても、C#がGoより1.5倍ほど速い 結果となりました。
変数の型:int (Goはint64、C#はint32)
いずれの測定パターンにおいても、C#がGoより3~4倍速い結果となりました。
CPU使用率
論理コア数8における、各論理コアの直列処理、並列処理時のCPU使用率です。
いずれのソース(int64、int32、int)も同傾向であったためint32の結果のみ載せています。
直列処理
CPU使用率合計(_Total)はGoとC#に差異は見られませんが、
Goは一つの論理コアに負荷が集中しています。
C#はいくつかのコアの負荷が上がったり下がったりしています。
Go
C#
並列処理
GoとC#共にCPU使用率100%となり、差異は見られません。
Go
C#
int32でGoが遅いのが不思議なのでアセンブリコード見てみる
Compiler explorerというサイトを使って、Goのアセンブリコードを見てみました。
見てみた結果
int64以外の型を指定する場合、int64の場合と比べてアセンブリコードの命令が増えていました。
具体的に何をしてるのかまでは見れてないのですが、これがint32の処理速度差の一因なのかなと思いました。
amd64を指定して解析したので、上記は64bit環境の場合と思われます。(32bit環境ならint32以外?)
確認した型は、int、int16、int32、int64、uint64です。
→(2022/09/26追記)コメントにてアセンブリコードの差異について記載くださっている方がいます。そちらも是非ご確認ください。
アセンブリコード抜粋
int64とint32のアセンブリコード変換前後のコードを載せます。
アセンブリコードを見やすくするため、直列処理のみをmain関数に記載してます。
intとint64(varで宣言)の変換結果は全く同じでした。
また、変数の宣言方法による差異もありませんでした(:= と var)。
変数の型:int (int64)
変換前
package main
// 求める素数の最大値
const MAX = 1000000
// アセンブリを単純にするため、直列処理のみmain()に記載
func main() {
for i := 2; i <= MAX; i++ {
for j := 2; j <= i; j++ {
if i == j {
// fmt.Println(target) // 計測時はコメントアウト
} else if i%j == 0 {
break
}
}
}
}
変換後
TEXT "".main(SB), NOSPLIT|ABIInternal, $0-0
FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
MOVL $2, AX
JMP main_pc11
main_pc7:
LEAQ 1(BX), AX
main_pc11:
CMPQ AX, $1000000
JGT main_pc32
MOVL $2, CX
JMP main_pc39
main_pc26:
MOVQ AX, BX
JMP main_pc7
NOP
main_pc32:
RET
main_pc33:
INCQ CX
MOVQ BX, AX
main_pc39:
CMPQ CX, AX
JGT main_pc26
JNE main_pc51
MOVQ AX, BX
JMP main_pc33
main_pc51:
MOVQ AX, DX
MOVQ DX, BX
CQO
IDIVQ CX
NOP
TESTQ DX, DX
JNE main_pc33
JMP main_pc7
変数の型:int32
変換前
package main
// 求める素数の最大値
const MAX int32 = 1000000
// アセンブリを単純にするため、直列処理のみmain()に記載
func main() {
var i int32
for i = 2; i <= MAX; i++ {
var j int32
for j = 2; j <= i; j++ {
if i == j {
// fmt.Println(target) // 計測時はコメントアウト
} else if i%j == 0 {
break
}
}
}
}
変換後
TEXT "".main(SB), NOSPLIT|ABIInternal, $8-0
SUBQ $8, SP
MOVQ BP, (SP)
LEAQ (SP), BP
FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
MOVL $2, AX
JMP main_pc22
main_pc19:
LEAL 1(BX), AX
main_pc22:
CMPL AX, $1000000
JGT main_pc40
MOVL $2, CX
JMP main_pc53
main_pc36:
MOVL AX, BX
JMP main_pc19
main_pc40:
MOVQ (SP), BP
ADDQ $8, SP
RET
main_pc49:
INCL CX
MOVL BX, AX
main_pc53:
CMPL CX, AX
JGT main_pc36
JNE main_pc64
MOVL AX, BX
JMP main_pc49
NOP
main_pc64:
TESTL CX, CX
JEQ main_pc92
MOVL AX, DX
MOVL DX, BX
CMPL CX, $-1
JEQ main_pc82
CDQ
IDIVL CX
JMP main_pc86
main_pc82:
NEGL AX
XORL DX, DX
main_pc86:
TESTL DX, DX
JNE main_pc49
JMP main_pc19
main_pc92:
PCDATA $1, $0
NOP
CALL runtime.panicdivide(SB)
XCHGL AX, AX
結論
単純なループ処理は、直列/並列処理ともにC#がGoより3~4倍速い。- 変数の型がint64の場合、C#がGoより若干だけ速い。
- 変数の型がint32の場合、C#がGoより1.5倍ほど速い。
→Goが遅いのは、int64以外の型を指定した場合にアセンブリコード上、処理が増えるのが原因?
→(2022/09/26追記)コメントにて考察くださっている方がいます。そちらも是非ご覧ください。 - Go、C#ともに、変数の型のサイズを小さくすると処理が速くなる。
- 複数コアの場合にCPU負荷のかかり方に差異があるようだが、1コアでも処理時間が同傾向であることから、CPU負荷差異の処理速度差異への寄与は小さいと思われる。
感想
Goの方がだいぶ遅いという予想外の結果となりました。
せめてgoroutineを使った並列処理では多少差が縮まるとも思いましたがそうでもなく。
C#もスレッドプールが優秀なのかもしれないですが。
なぜGoの方が遅いのか腑に落ちないところがあるので、機会があれば比較言語を増やしたり
色々な処理で比較したりしてみたく思います。
GoとC#の比較が本題でしたが、変数の型によってこんなに速度に違いが出てくることに驚きました。
Goはintが実行環境によるというのは、完全に盲点でした。。C#しか触ってなかった弊害ですね。
また、アセンブリコードを見てみるとGoは型の違いだけなのに差異があることがわかってこれも以外な結果でした。
色々発見あって楽しかったです。コメントでご指摘いただけた方、本当にありがとうございました。