プログラミング言語別マルチスレッドハッシュ処理(SHA-256)の処理時間比較
平文を総当たりでハッシュ化し、一致する元の平文を探すプログラム
☆HassingSpeedTest
https://github.com/NobuyukiInoue/HashingSpeedTest
を使って、平文3桁および4桁の総当たりにかかる所要時間を調べてみました。
検証はSHA256を使用しましたが、MD5/SHA1/SHA384/SHA512なども指定できます。
1-1. 言語別に比較しようとした動機
数年前に作成していた
C#(.NET Framework)で書いたマルチスレッド(並列処理)で平文をハッシュ化するプログラムを、
新たに購入したインテル第8世代 Core i7 のパソコンで動かしたところ、
インテル第7世代Core iシリーズ以前の処理時間よりも遅い結果となったので、
コア数の増えたインテル第8世代Core iシリーズは、
そんなにサーマルスロットリングの症状が酷いのかと不思議に思い、
ほかの言語にも移植し、処理時間を比較してみることにしました。
移植してびっくり!!!
同じC#でも、
.NET Framework のプログラムを
.NET Core のプロジェクトに移植するだけ(プログラムは一切変更なし)で、
(スレッド数=1の場合の)
処理時間が92.985秒から1.130秒に縮まりました。
Golangではさらに速く、なんと0.181秒です。
.NET Framework 遅すぎです...orz
先に結果を見たい方は、こちらへ
1-2. 使用した言語
検証のために移植した言語は、
・C#(.NET Framework4.72)
・C#(.NET Core 2.0/3.0)
・Java8
・Golang
・Python3
の4つです。
ハッシュ後の文字列の照合は、
・byte[]型での照合
・String型に変換して照合
の2つの方法で検証しています。
言語別の比較では、、先に結論をいうと、
Golangの圧勝となりました。
まさか、ここまで桁違いの差が開くとは思いませんでした。
最近のCPUは、CPU内部のキャッシュメモリも数MByte搭載しており、
Windows3.1/95時代のパソコンに搭載されていたメインメモリ容量に匹敵します。
8bit/16bit/32bitのOS(当時としては超巨大プログラム)が丸ごとCPUキャッシュに乗せられる容量です。
Golangが高速な要因としては、コンパイル後のバイナリコードが省メモリで、
CPUキャッシュ内でコードが実行できたことが大きいものと思われます。
それに対して、C# では、
スレッド数を増やすとサーマルスロットリングが発生し、
逆に処理時間が長くなるといった症状が発生しました。
1-3. 平文に使用する文字
平文で使用する文字は、0x00~0xffのすべての値を使用するわけではなく、
キーボードから入力できる通常文字(0x20:半角スペース)~
~
(0x7e:チルダ) の
95通りに限定しています。
平文3桁、4桁それぞれの組み合わせの数は以下のようになります。
平文3桁の組み合わせ数 = 95の3乗 = 857,375 = 85万7375通り
平文4桁の組み合わせ数 = 95の4乗 = 81,450,625 = 8145万625通り
1-4. 使用マシンのスペック
CPU: Intel Core-i7 8559U 2.7GHz(4.5GHz) 4コア/8スレッド
Memory: 16GB(LPDDR3 2,133MHz)
いわゆる、Macbook Pro 13インチ 2018モデルですね。
2. Windows 10 の場合
2-1-0. C#(.NET Framework 4.72) on Windows 10
桁数 | スレッド数=1 | スレッド数=2 | スレッド数=4 |
---|---|---|---|
平文3桁 | 92.985 秒 | 51.110 秒 | 32.734 秒 |
平文4桁 | 計測断念 | 計測断念 | 計測断念 |
平文4桁は計測していませんが、おおむね平文3桁の95倍の処理時間だと考えられます。
2-1-1. C#(.NET Core 2.0(ServerGC=false)) on Windows 10
桁数 | スレッド数=1 | スレッド数=2 | スレッド数=4 |
---|---|---|---|
平文3桁 | 1.130 秒 | 1.109 秒 | 1.131 秒 |
平文4桁 | 86.727 秒 | 68.876 秒 | 939.002 秒 |
平文3桁は、処理時間が短すぎるため、スレッド数1,2,4 でとほとんど違いがありません。
平文4桁では、スレッド数=2までは順調に処理時間が短くなっていますが、
スレッド数=4では逆に時間が長くなっています。
サーマルスロットリングが発生しているものと思われます。
2-1-2. C#(.NET Core 3.0(ServerGC=false)) on Windows 10
桁数 | スレッド数=1 | スレッド数=2 | スレッド数=4 |
---|---|---|---|
平文3桁 | 0.884 秒 | 0.641 秒 | 0.808 秒 |
平文4桁 | 80.457 秒 | 54.480 秒 | 210.184 秒 |
平文4桁では、スレッド数=2までは順調に処理時間が短くなっていますが、
他のC#の結果と同様に、スレッド数=4では逆に時間が長くなっています。
全体的に、.NET Core 2.0より速いですね。
2-1-3. C#(.NET Core 3.0(ServerGC=True)) on Windows 10
桁数 | スレッド数=1 | スレッド数=2 | スレッド数=4 |
---|---|---|---|
平文3桁 | 0.889 秒 | 0.566 秒 | 0.481 秒 |
平文4桁 | 74.025 秒 | 44.737 秒 | 129.15 秒 |
プロジェクトのプロパティでServeGC=Trueにすると、処理時間が短くなりました。
平文4桁では、スレッド数=2までは順調に処理時間が短くなっていますが、
他のC#の結果と同様に、スレッド数=4では逆に時間が長くなっています。
2-2. Java8 on Windows 10
桁数 | スレッド数=1 | スレッド数=2 | スレッド数=4 |
---|---|---|---|
平文3桁 | 0.406 秒 | 0.329 秒 | 0.281 秒 |
平文4桁 | 23.040 秒 | 13.984 秒 | 10.078 秒 |
スレッド数を増やすほど、順調に処理時間が短くなっています。
2-3. Golang version 1.11.5 : Windows 10
桁数 | スレッド数=1 | スレッド数=2 | スレッド数=4 |
---|---|---|---|
平文3桁 | 0.181 秒 | 0.108 秒 | 0.064 秒 |
平文4桁 | 16.414 秒 | 9.378 秒 | 5.610 秒 |
スレッド数を増やすほど、順調に処理時間が短くなっています。
Javaよりも2倍程度速いですね。
2-4. Python version 3.7.1 : Windows 10
桁数 | スレッド数=1 | スレッド数=2 | スレッド数=4 |
---|---|---|---|
平文3桁 | 1.828 秒 | 1.828 秒 | 1.843 秒 |
平文4桁 | 209.743 秒 | 198.856 秒 | 185.472 秒 |
Pythonにはプログラム実行の一貫性を保証する仕組みとして「Global Interpreter Lock(GIL)」を持っています。
同時に1つのスレッドしか実行されないため、マルチスレッド化しても並列実行とはなりません。
3. macOS の場合
3-1-1. C#(.NET Core 2.0(ServerGC=false)) on macOS 10.14 Mojave
桁数 | スレッド数=1 | スレッド数=2 | スレッド数=4 |
---|---|---|---|
平文3桁 | 0.827 秒 | 0.598 秒 | 1.418 秒 |
平文4桁 | 73.877 秒 | 52.735 秒 | 1800秒以上(計測断念) |
Windows10より、10%程度遅くなりました。
3-1-2. C#(.NET Core 3.0(ServerGC=false)) on macOS 10.14 Mojave
桁数 | スレッド数=1 | スレッド数=2 | スレッド数=4 |
---|---|---|---|
平文3桁 | 0.725 秒 | 0.528 秒 | 0.770 秒 |
平文4桁 | 63.571 秒 | 43.214 秒 | 1000.931 秒 |
平文4桁、スレッド数=4の処理時間が、Windows10よりも酷い結果になりました。
3-1-3. C#(.NET Core 3.0(ServerGC=True)) on macOS 10.14 Mojave
ごめんなさい...
まだ未計測です。
3-2. Java8 on macOS 10.14 Mojave
桁数 | スレッド数=1 | スレッド数=2 | スレッド数=4 |
---|---|---|---|
平文3桁 | 0.488 秒 | 0.370 秒 | 0.304 秒 |
平文4桁 | 32.155 秒 | 18.600 秒 | 11.190 秒 |
Windows10よりもわずかに処理時間が短いですが、
ほぼ同等のパフォーマンスと言って良いかもしれません。
3-3. Golang version 1.11.5 on macOS 10.14 Mojave
桁数 | スレッド数=1 | スレッド数=2 | スレッド数=4 |
---|---|---|---|
平文3桁 | 0.180 秒 | 0.102 秒 | 0.062 秒 |
平文4桁 | 16.325 秒 | 9.731 秒 | 5.519 秒 |
Golangの場合も、Java同様、Windows10よりもわずかに処理時間が短いですが、
ほぼ同等のパフォーマンスと言って良いかもしれません。
3-4. Python version 3.7.1 on macOS 10.14 Mojave
桁数 | スレッド数=1 | スレッド数=2 | スレッド数=4 |
---|---|---|---|
平文3桁 | 1.955 秒 | 1.926 秒 | 1.948 秒 |
平文4桁 | 202.687 秒 | 192.723 秒 | 193.321 秒 |
Windows10よりもわずかに遅い程度でしょうか。
同時に1つのスレッドしか実行されないため、マルチスレッド化しても並列実行とはなりません。
4. 各言語の結果比較
同じ平文の桁数で各言語の結果を並べると、以下のようになります。
(といっても、掲載はWindows10の結果のみですが...)
4-1. 平文3桁
4-1-1. 平文3桁(byte[]型で照合)on Windows10
言語 | スレッド数=1 | スレッド数=2 | スレッド数=4 |
---|---|---|---|
C# .NET Framework 4.72 | 92.985 秒 | 51.110 秒 | 32.734 秒 |
C# .NET Core 2.0 | 1.130 秒 | 1.109 秒 | 1.131 秒 |
C# .NET Core 3.0(ServerGC false) | 0.884 秒 | 0.641 秒 | 0.808 秒 |
C# .NET Core 3.0(ServerGC true) | 0.889 秒 | 0.566 秒 | 0.481 秒 |
Java8 | 0.406 秒 | 0.329 秒 | 0.281 秒 |
Golang | 0.181 秒 | 0.107 秒 | 0.064 秒 |
Python3 | 1.828 秒 | 1.828 秒 | 1.843 秒 |
4-1-2. 平文3桁(String型に変換して照合)on Windows10
言語 | スレッド数=1 | スレッド数=2 | スレッド数=4 |
---|---|---|---|
C# .NET Framework 4.72 | 未計測 | 未計測 | 未計測 |
C# .NET Core 2.0 | 2.486 秒 | 1.783 秒 | 1.039 秒 |
C# .NET Core 3.0(ServerGC false) | 1.916 秒 | 1.293 秒 | 1.123 秒 |
C# .NET Core 3.0(ServerGC true) | 2.136 秒 | 1.385 秒 | 1.189 秒 |
Java8 | 11.659 秒 | 6.078 秒 | 4.156 秒 |
Golang | 3.736 秒 | 2.133 秒 | 1.128 秒 |
Python3 | 1.859 秒 | 1.859 秒 | 1.812 秒 |
4-2. 平文4桁
4-2-1. 平文4桁(byte[]型で照合)on Windows10
言語 | スレッド数=1 | スレッド数=2 | スレッド数=4 |
---|---|---|---|
C# .NET Framework 4.72 | 未計測 | 未計測 | 未計測 |
C# .NET Core 2.0 | 86.727 秒 | 68.876 秒 | 939.002 秒 |
C# .NET Core 3.0(ServerGC false) | 80.457 秒 | 54.480 秒 | 210.184 秒 |
C# .NET Core 3.0(ServerGC true) | 84.025 秒 | 44.737 秒 | 129.150 秒 |
Java8 | 23.040 秒 | 13.984 秒 | 10.078 秒 |
Golang | 16.414 秒 | 9.378 秒 | 5.610 秒 |
Python3 | 209.743 秒 | 198.856 秒 | 185.472 秒 |
4-2-2. 平文4桁(String型に変換して照合)on Windows10
言語 | スレッド数=1 | スレッド数=2 | スレッド数=4 |
---|---|---|---|
C# .NET Framework 4.72 | 未計測 | 未計測 | 未計測 |
C# .NET Core 2.0 | 219.537 秒 | 139.804 秒 | 105.649 秒 |
C# .NET Core 3.0(ServerGC false) | 179.296 秒 | 118.178 秒 | 92.185 秒 |
C# .NET Core 3.0(ServerGC true) | 194.337 秒 | 130.760 秒 | 93.532 秒 |
Java8 | 1063.626 秒 | 558.925 秒 | 369.657 秒 |
Golang | 376.401 秒 | 202.287 秒 | 129.462 秒 |
Python3 | 181.329 秒 | 197.700 秒 | 185.268 秒 |
4-3. 結果に関する所感
冒頭でも述べたとおり、Golangの圧勝です。
byte[]型での照合と、String型に変換してからの照合では、
変換処理のないbyte[]型の方が処理時間は短くなるのは当然ですが、
C#のスレッド数=4は、スレッド数=2よりも遅くなる結果となりました。
サーマルスロットリングによるクロックダウンが発生しているものと思われます。
Pythonの場合、ハッシュ後にbyte[]型とString型の両方が格納されているようで、
処理時間に明確な差は見当たりませんでした。
5. 感想
1つの言語やプラットフォームに固執するのではなく、
少し視点(言語)を変えてみることで、また違った側面が見えてきますね。
C/C++でも計測してみたいが、、まだ作ってない...
いまさら移植する気が起きない...