多コアCPUのコアを使い切るにはどうするか?とここ数年考えていたのですが、そういえばコラッツ予想(3n+1問題)を確認するプログラムはちょうどよい例だと思いプログラムを作成してみました。
CollatzAsmについて
せっかくなので64ビットアセンブラで作成し、128ビット(2の128乗)までの数を扱えるようにしました。ちなみに64ビットだと入力が数百億程度(35ビット程度)で内部の計算が桁あふれを起こします。
Visual Studio 2022(C++/Asm)で作成しています。ここからプロジェクトファイル一式をダウンロードできます。
Visual C++ですが32ビットバージョンはインラインアセンブラが使えるので、お手軽にアセンブラを使えたのですが、64ビットになりなぜかインラインアセンブラをサポートしなくなりました。ということで約30年ぶりにアセンブラのソースコードを書きました。
ちなみに、16ビット時代はアセンブラプログラミングの参考書が豊富にあったのですが、64ビットになりあまり見当たらなくなりました。昔はミックスドランゲージといって、Cからアセンブラを呼び出す方法もよく解説をされていたのですが、今では、ここに資料があるくらいで、基本的なことが分かっている人じゃないと意味不明かと思われます。
詳しい解説はご希望があればやりますが、このプロジェクトをサンプルとしてもらえればと思います。
また、このサンプルはC++14のマルチスレッドのサンプルにもなっています。長い間マルチスレッドプログラムと言えばOSのAPIかランタイム関数を使って作っていたのですが、C++14からプログラミング言語にサポートされたということで作成してみました。
実行例は以下のとおりとなります。
最初の引数で何処までの数を確認するかを入れ、2つ目の数は並列度(スレッド数)になります。
サンプルでは10になっていますが、当然コア数以上の値をいれます。32論理コアに対して100とかにしてもパフォーマンスが上がります(後述)。
CollatzAsmBenchについて
アセンブラでのプログラミングに限った話ではないのですが、プログラムの最適化の過程で試行錯誤を行うことがあります。特にアセンブラでプログラムすると様々な命令を使うことができるのでそのバリエーションが増えるかと思います。
ということで試行錯誤の記録として10個程アセンブラのコードのパフォーマンスを比較するプログラムを書いてみました。
以下、実行結果になります。
ChatGPTの出力コードとの比較
いわゆるバイブコーディングということで専用のツールも出てきていますが、コラッツ問題を扱うプログラムに関していうと、どこにでもあるのでChatGPTでも簡単なプロンプトでかなりいい感じのコードを出力しています。ということでChatGPTでプログラムを出力させてみました。、実際に試してみたところ可能でしたがあまり速度が変わらなかったので、今回はアセンブラでの出力はしていません。ChatGPTが作成したマルチスレッドのものを掲載します。
私が作ったコードと比較するとマルチスレッドの初期化の取り扱いがうまいです(emplace_backを使っている)。一方で、データ長は64ビット止まりで、並列性も論理コア数に従ってスレッドを作成していますが(hardware_concurrencyメソッドを呼んでコア数を取得している)、このプログラムの場合、各スレッドの実行時間が必ずしも同じではないので、スレッド数をより多くして各スレッドのタスクを細かくした方が、実行時間のばらつきの減少が期待できます。一方で、一般論になるのですが、論理コア数以上のスレッドを実行させると各スレッドがCPUのリソースを食い合いすることになるので、実行スレッド数を論理コア数に合わせるのも一つの手になります。
今回はアセンブラでは比較をしませんでしたが、CやC++のコードを単純にアセンブラにしてもあまり早くならないということもあります。一方で128ビットのような桁数の多い計算をさせる場合、アセンブラには桁あふれを処理する命令があり、CやC++で組むよりはるかに効率的なプログラムが記述できます。機会があればChatGPTでアセンブラプログラムの最適化を行いたいですが、↑の例にあるようにAIに任せるより、自分で工夫をした方が手っ取り早い面があります。もちろんですがアイデア出しをAIに頼ることもできますので、こういうことではあまりAIと人間の比較は意味がない(人間からしたらAIも利用する)ということになりますが、2025年9月現在、このあたりのチューニングはまだ人間の方に一日の長があるかと思います(もっともClaudeなどの専用のツールをつかったらどうなるかというのはありますが・・・)。
最後に実行結果を
ということで、倍以上のパフォーマンスを示しています。逆にいうと倍程度にしかならないのですが、ある処理時間が半分になるということは2020年代のCPUの進化でいうとほぼ10年に相当します(この場合シングルスレッド性能の比較になる)。つまり上手くアセンブラでプログラムを書き直すことができればCPUの進化を10年先取りできるとも言えます。CPUのシングルスレッド性能の向上が顕著だった90年代ですと概ね1,2年でパフォーマンスが倍になっていました。
余談ですが、アセンブラでのプログラミングは8ビットや16ビットの時代は割と一般的でした。90年代以降ではCPU自体の進化が早かった為、アセンブラでのプログラミングがエンコード等、いやゆるSIMD命令を使うため等、ニッチになった感がありました。CPUのシングルスレッド性の向上が見込めなくなった昨今、アセンブラでのプログラミングが見直されるかもしれません。
話を戻すと、コラッツ予想の確認プログラムの場合、スレッド数を100にしても性能が伸びていることを確認できます。これは、前述のとおり値により処理ステップにばらつきがあるためで、区間を細かくした方が(スレッド数を多くし多方が)、CPUから見た場合のトータル処理時間が平均化される為です。
Javaでメソッドを呼び出すときにはクラスからインスタンスを生成してインスタンスのメソッドを呼び出すのが普通です。一方、staticメソッドはインスタンスを生成しなくてもクラスから直接呼び出せます。このため、オブジェクト指向プログラミングを理解していない古いタイプのプログラマは、Javaでもstaticメソッドを多用します。これを揶揄して「staticおじさん」と呼ぶのです。これは、
クラスのメンバへのアクセスを制限するもう一つの方法は、メソッドを出来るだけstatic にすることだこのReadable codeという本は私は英語版を購入したのですがそこでも同様のことが書かれています。
ただ、現実に年齢を重ねると、どうしても守りに入りがちなのは事実です。「自分はstaticおじさんなのではないか」という問いは、常に忘れてはならないのでしょう。というヒマがあったら自身が思わぬ誤謬をしていないか記事の検証を行うことを勧めます。
ただ、現実に年齢を重ねると、どうしても守りに入りがちなのは事実です。「自分はstaticおじさんなのではないか」という問いは、常に忘れてはならないのでしょう。
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <math.h>
#include <ctype.h>
#include <time.h>
using namespace std;
class HABReferee {
vector<int> answer;
vector<bool> blowtable;
public:
bool prepareAnswer(const vector<int> &answer_) {
blowtable.assign(10, false);
for ( size_t i = 0; i < answer_.size(); i++ ) {
if ( blowtable[answer_[i]] ) {
return false;
}
blowtable[answer_[i]] = true;
}
answer = answer_;
return true;
}
bool submitAnswer( const vector<int> submit, int &hit, int &blow) {
if ( answer.size() != submit.size() ) return false;
hit = 0;
blow = 0;
for ( size_t i = 0; i < submit.size(); i++ ) {
if ( answer[i] == submit[i] ) {
hit++;
} else if ( blowtable[submit[i]] ) {
blow++;
}
}
return hit == submit.size();
}
};
static vector<int> inputNumbers(int N) { // N桁の数値の入力を行う(違った場合はやり直し)
vector<int> result;
string str;
do {
result.clear();
cin >> str;
for ( size_t i = 0; i < str.size(); i++ ) {
if ( isdigit(str[i]) && str[i] != '0' )
result.push_back(str[i] - '0');
else
break;
}
} while ( result.size() != N );
return result;
}
class HABContributor { // 出題者(人間)
public:
virtual vector<int> prepareAnswser(int N) {
cout << "各桁が1~9である" << N << "桁の数を入力してほしい。各桁で数が重複するのは避けてくれ" << endl;
return inputNumbers(N);
}
};
class HABContriburerComputer : public HABContributor { // 出題者(乱数生成)
public:
virtual vector<int> prepareAnswser(int N) {
vector<int> digits;
for ( int i = 1; i < 10; i++ ) {
digits.push_back(i);
}
vector<int> result;
srand((unsigned int)time(0));
for ( int i = 0; i < N; i++ ) {
vector<int>::iterator itor = digits.begin() + rand() % digits.size();
result.push_back(*itor);
digits.erase(itor);
}
return result;
}
};
class HABSolver { // 回答者(人間)
public:
virtual void prepare(int N) {}
virtual vector<int> getAnswer(int N) {
cout << "答えを予想してくれ" << N << "桁の数だ。" << endl;
return inputNumbers(N);
}
virtual void giveHint( int hit, int blow) {
cout << hit << "Hit" << " / " << blow << "blow" << endl;
}
};
class HABSolverComputer : public HABSolver { // 回答者(コンピューター)
HABReferee checker;
vector<vector<int>> candidate;
public:
void recur(vector<int> &answer, int N) {
if ( N == 0 ) {
candidate.push_back(answer);
} else {
for ( int i = 1; i < 10; i++ ) {
if ( find( answer.begin(), answer.end(), i) == answer.end() ) {
answer.push_back(i);
recur( answer, N-1);
answer.pop_back();
}
}
}
}
virtual void prepare(int N) {
vector<int> answer;
recur( answer, N);
}
virtual vector<int> getAnswer(int N) {
cout << "答えは";
for ( int i = 0; i < N; i++ ) {
cout << candidate.back()[i];
}
cout << "かな?";
return candidate.back();
}
virtual void giveHint( int hit, int blow) {
HABSolver::giveHint( hit, blow);
checker.prepareAnswer(candidate.back());
for ( vector<vector<int>>::iterator i = candidate.begin(); i < candidate.end(); ) {
int ahit, ablow;
checker.submitAnswer( *i, ahit, ablow);
if ( ahit != hit || ablow != blow ) {
i = candidate.erase(i);
} else {
i++;
}
}
}
};
class HABGame {
int N;
HABReferee referee;
HABContributor *c;
HABSolver *s;
public:
HABGame(int N_, HABContributor *c_, HABSolver *s_) : N(N_), c(c_), s(s_) {};
void play() {
// 出題者から問題をもらいレフリーに渡す。
while ( !referee.prepareAnswer(c->prepareAnswser(N)) )
; // 規格にあったものが出てくるまでループする
bool endflag = false;
int hit;
int blow;
s->prepare(N); // 回答者に準備をさせる
while ( endflag == false ) {
// 回答者から回答をもらい判定する。
endflag = referee.submitAnswer( s->getAnswer(N), hit, blow);
// 回答者にヒントを言う。
s->giveHint( hit, blow);
}
}
};
int main()
{
HABContributor hc;
HABContriburerComputer cc;
HABSolver hs;
HABSolverComputer cs;
HABContributor *c;
HABSolver *s;
string str;
while ( true ) {
cout << "メニュー" << endl
<< "1:出題者(Human) vs 回答者(Human)" << endl
<< "2:出題者(Human) vs 回答者(Computer)" << endl
<< "3:出題者(Computer) vs 回答者(Human)" << endl
<< "4:出題者(Computer) vs 回答者(Computer)" << endl
<< "0:終了" << endl;
cin >> str;
switch( str[0] ) {
case '0' :
return 0;
case '1' :
c = &hc;
s = &hs;
break;
case '2' :
c = &hc;
s = &cs;
break;
case '3' :
c = &cc;
s = &hs;
break;
case '4' :
c = &cc;
s = &cs;
break;
}
HABGame g(3, c, s);
g.play();
}
return 0;
}