GoとC#を速度比較してみた

# ★記事更新しました (2022/09/24 23:00更新)
比較ソースがGoとC#とで差異があったため、再測定を行い、記事更新しました。
修正前ソースは、for文のループ変数の型がintになっており、内部的にはGoはint64[^1]、C#はint32で差異がありました。
記事本文は、以下3つのソースの測定結果を並記しております。（③が更新前の記載内容）
　→変数の型が、①int64　②int32　③int (Goはint64、C#はint32)

以下、本文です。

[^1]:Goのint型は、環境依存でint64またはint32になります。

----------
GoとC#の速度比較を行いました。
普段C#を扱っているのですが、Goを使う機会があり、Goは早いと噂なので実際に測ってみたのが動機です。


# 計測内容
2~Nの整数が素数かどうか、愚直にループと判定を行う処理をGoとC#で実行しました。

* 変数の型が以下3パターンのソースで実施。
    * int64　→ソース：[Github(Int64ブランチ)](https://github.com/takeITeasy7/CountPrime/tree/Int64)
    * int32　→ソース：[Github(Int32ブランチ)](https://github.com/takeITeasy7/CountPrime/tree/Int32)
    * int (Goはint64[^1]、C#はint32) 　→ソース：[Github(Intブランチ)](https://github.com/takeITeasy7/CountPrime/tree/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より若干だけ速い** 結果となりました。
![結果Int64.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572307/73532e6a-f5ea-0fbd-ef4a-ff0ca3ed2186.png)

### 変数の型：int32
いずれの測定パターンにおいても、**C#がGoより1.5倍ほど速い** 結果となりました。
![結果Int32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572307/96cae140-5937-76c3-d853-a24217222ced.png)


### 変数の型：int (Goはint64、C#はint32)
いずれの測定パターンにおいても、**C#がGoより3~4倍速い**結果となりました。
![結果.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572307/6b6f4287-b29b-e746-2809-59f78931282c.png)


## CPU使用率
論理コア数8における、各論理コアの直列処理、並列処理時のCPU使用率です。
いずれのソース（int64、int32、int）も同傾向であったためint32の結果のみ載せています。

### 直列処理
CPU使用率合計(_Total)はGoとC#に差異は見られませんが、
Goは一つの論理コアに負荷が集中しています。
C#はいくつかのコアの負荷が上がったり下がったりしています。

#### Go
![Go直列.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572307/3671467c-b288-b5db-7c74-25c35f44fa42.png)

#### C#
![C#直列.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572307/4ad75b88-4292-d4b8-a087-93c4a6b151c1.png)

### 並列処理
GoとC#共にCPU使用率100%となり、差異は見られません。

#### Go
![Go並列.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572307/81d2f9e1-d933-4da8-1db5-06017e02e12b.png)

#### C#
![C#並列.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572307/be9fe031-a3c4-5ba1-e669-8c2d6bca6f08.png)

# int32でGoが遅いのが不思議なのでアセンブリコード見てみる
[Compiler explorer](https://godbolt.org/)というサイトを使って、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)
#### 変換前
```go
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
#### 変換前
```go
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は型の違いだけなのに差異があることがわかってこれも以外な結果でした。
色々発見あって楽しかったです。コメントでご指摘いただけた方、本当にありがとうございました。