Z80(ゼッパチ)を4個つないで円周率を求めてみた — 並列で速くなる計算と、何台つないでも速くならない計算
- z80
- 並列計算
- 数値計算
- sdcc
- アセンブリ
はじめに — 1台で動いたなら、4個つないだら?
前回、ゼッパチを、いまの机に蘇らせる で、1970 年代の 8 ビット CPU Z80——私の世代でいう「ゼッパチ」——を、いまの机の上で printf まで動くところまで戻しました。
戻したら、計算させたくなります。円周率を出させてみたら、1 個の π に約 131 万クロックかかりました。8 ビットは、とにかく重い。
そこで素朴な疑問が湧きます。Z80 を 4 個つないで分担させたら、4 倍速くなるんだろうか?
やってみたら、答えはどれも予想と違いました。ある繋ぎ方は 4 台でほぼ理想線まで伸びて、しかも何回走らせても 1 クロックもブレない。別の繋ぎ方は 2 倍そこそこで頭打ち、おまけに走るたびに数字が踊る。そして極めつけ——何台つないでも、速度が 1.00 倍からびくとも動かない計算が、ひとつ混ざっていました。
これは、並列計算のいちばんおいしい所を、頭にまるごと収まる 8 ビットの小さな机の上で覗いた記録です。私もやりながら「えっ」と声が出た回なので、その「えっ」を一緒に楽しんでもらえたら嬉しいです。
題材 — 円周率を、足し算で
使ったのは、数値計算でも並列計算でも教科書のいちばん最初に出てくる定番です。
右辺がきっかり 。だから左辺を数値積分すれば円周率が出ます。区間を細かく刻んで台形で埋めて足す——いわゆる台形公式です。足し算の集まりなので、区間をいくつかに分けて別々の Z80 に配り、最後に足し合わせれば「分担」になります。
Z80 には浮動小数点の回路(FPU)がありません。3.14 のような小数を素では持てない。なので整数の下の方を小数とみなす固定小数点で計算しています(このあたりの苦労は後で少しだけ)。まず、1 台での基準値がこれです。
trapezoid: pi~=3.1414 (N=64)
Inst= 96075 約 1,311,070 clocks
π ひとつに約 131 万クロック。この数字を基準に、台数を増やすと何が起きるかを見ていきます。
驚き① — 「皆で1つのメモリを取り合う」式は、気持ちよく速くなる
ひとつ目の繋ぎ方は、共有メモリ型。複数の Z80 が 1 つの RAM をのぞき、そこへ部分和を足し込んでいく方式です(実機の Z80 はこれを単体では想定していないので、その「土台」ごと自分で作りました。これも後で)。
台数を 1 → 2 → 3 → 4 と増やした、1 台基準の速度向上です。

1台 : 1.00x
2台 : 1.94x
3台 : 2.86x
4台 : 3.73x (理想は 4.00x)
4 台で 3.73 倍。理想の 4 倍に肉薄しています。そして個人的にいちばん感心したのは、何回走らせても 1 クロックも変わらないこと。自作シミュレータは決定論的で、出てくる π も毎回ビット単位で同じ——しかも 1 台のときと完全に同じ値です(同じ計算カーネルを使っているから当然そうなる、というのが後でじわっと効きます)。「速い」だけでなく「毎回まったく同じ」。気持ちよさすらあります。
ただ、タダの速さではありません。台数が増えるほどコアどうしが「いま書いていいのは誰か」の札を奪い合い、待ち時間がじわっと増えます。速さの裏で、ちゃんと代償を払っています。
驚き② — 「送って持ち寄る」式は、速いのに読めない
ふたつ目は、メッセージパッシング型。各 Z80 は自分のメモリしか持たず、部分和を「送って」一台に持ち寄り合算する、スーパーコンピュータの定番の方式です。
同じ計算なのに、こちらは様子がまるで違いました。代表でならせば 2 倍そこそこ。けれど——ここが面白い——走らせ直すたびに大きく振れる。4 台で 1.8 倍のときもあれば 2.9 倍のときもあって、「だいたい何倍」と一言で言えない代物でした。速度向上が、ホストのご機嫌しだいで毎回変わるのです。
犯人は、まとめ役の 1 台です。各台の計算は台数で軽くなるのに、まとめ役が背負う「みんなの結果を受け取って合算する」仕事は台数を増やしても縮まない(むしろ増える)。この縮まない一本道が、全体の足を引っ張り続けます。
処理に「並列にできない一本道の部分」があると、いくらコアを足しても速度には上限がある——これがアムダールの法則です。名前は怖いけれど、起きていることは「みんなで作業しても、最後に 1 人がレジ打ちするなら、その行列は人を増やしても消えない」だけ。
共有メモリ型が「決定論的に速い」隣で、こちらは「速いけれど読めない」。同じ問題、同じ計算カーネルなのに、束ね方が違うだけでここまで性格が変わる。私はこの対比がいちばん面白いと感じました。
驚き③ — 何台つないでも、1.00倍から動かない計算
ここがこの回のオチです。
平方根を求める、有名な漸化式があります(ニュートン法、ヘロンの方法)。
実際に走らせると 1.0 → 1.5 → 1.41662 → 1.41418 と、きれいに √2 に近づきます。これを台形と同じノリで「区間をコアに配って分担」しようとして、手が止まりました。
x_{n+1} を出すには x_n が要る。x_{n+2} には x_{n+1} が要る。前の答えが無いと、次に進めない。 台形のように「ここからここはコア A、その先はコア B」と切れません。
flowchart TB
subgraph たば["束 — 台形(区間ごとに独立 → 配れる)"]
direction TB
B0["区間 0–15 → Z80 #1"]
B1["区間 16–31 → Z80 #2"]
B2["区間 32–47 → Z80 #3"]
B3["区間 48–63 → Z80 #4"]
B0 ~~~ B1 ~~~ B2 ~~~ B3
end
subgraph くさり["鎖 — ニュートン法(前の答えが要る → 配れない)"]
direction TB
C0["x₀ = 1.0"] --> C1["x₁"] --> C2["x₂"] --> C3["x₃ → √2"]
end
だから、Z80 を 4 台ぶら下げても、実際に働けるのは 1 台だけ。残りはただ待っています。速度向上は 1.00 倍から動きようがない。測るまでもなく、構造で決まっています。
(念のため:この 1.00 倍は、並列版を作って実測した数字ではありません。「鎖はそもそも分割できない」という依存関係から導かれる値です。実装が無いものを、実測のように描かないでおきます。)
ここで腑に落ちたのが、この回でいちばん持って帰ってほしいことでした。並列で速くなるかどうかは、計算が重いかどうかでは決まりません。計算の「形」——鎖か、束か——で決まります。 束(独立な区間の集まり=台形)なら素直に効く。鎖(前段に依存する反復=ニュートン法)なら、台数をいくら積んでも無駄で、その前に鎖をほどけないかを考えるのが先。
8 ビットの小さな実測だからこそ、この優先順位が頭に焼き付きました。
種明かし — 8ビットで、これをやる泥臭さ
「結果」だけ書くと格好いいですが、ここに着くまでに、8 ビットの石とはそれなりに泥臭く格闘しました。長く居座った所を、3 つだけ。
long long で、シミュレータが永遠に終わらない。 最初、精度を稼ごうと 64 ビット整数で書きました。Z80 には掛け算命令すら無く、64 ビットの四則演算はすべてライブラリ関数の呼び出しに化けます。重すぎて、シミュレータ上で計算が終わらない。すべて 32 ビットに収まる形に作り直して、ようやく走りました。「精度・語長・速度は綱引き」を、身体で覚えました。
volatile を付けたのに、出力が一切出ない。 結果を出す「出口」に書き込んでも、何も出てこない。生成されたアセンブリを読んで分かったのが、SDCC が「同じ番地への連続書き込み」を冗長と見て最後の 1 個以外を消していたこと。volatile が効いていなかったのです。「volatile を付けたから大丈夫」と思い込まず、コンパイラが実際に何を吐いたかを自分の目で読む。8 ビットの小さな世界だと、これが一発で腹に落ちました。
そもそも Z80 は「複数で協調する」設計ではない。 共有メモリ型をやるには、複数の Z80 が 1 本のバスを取り合う仕組みが要りますが、それは CPU 単体にはありません。当時も外付けハードで実現していた世界です。なので、検証済みの単一ヘッダ Z80 コアを再利用し、その周りにバス調停・ハードウェアセマフォ・バリア・本物のベクタ割り込み(IM2・手書き ISR)を自分で書いて、1 つのマルチコアにしました。計算の中身(pi_kernel.h)はたった 1 ファイルで、1 台版も「送って持ち寄る」版も「取り合う」版も、同じファイルを取り込んでいるだけ。だから π はどの方式・どの台数でもビット単位で同じ値になります。
ちなみに「取り合う」版でロックを外すと、答えは派手に 0 になるのではなく、それらしく見える過小な値に、静かにずれます(4 台で π≈0.94)。0 で落ちてくれた方が気づけるのに、と思う、いやな壊れ方も実物で踏めます——が、これ以上は repo の実験ノートに譲ります。
まとめ
- Z80 4 個で円周率を分担。共有メモリ型は 3.73 倍・毎回まったく同じ。送って持ち寄る型は 2 倍頭打ち・走るたびに踊る。
- ニュートン法は何台積んでも 1.00 倍。重いからではなく、鎖だから。
- 並列を考える前に、まず 「鎖か、束か」。束なら効く。鎖なら、台数を積む前に鎖をほどけないかを考える。
- 8 ビットは重い。けれど重いぶん、何が高くつくかが正直に全部見える。古い石は、いまでも良い先生だと思います。
動かしてみる
コードと手順は、公開リポジトリにまとめてあります。速くなる・ならないを、自分の机の上で実測できます。
📦 https://github.com/logicia32/z80-parallel-pi
SDCC(Z80 対応版)と ucsim_z80 が入っていれば、各ディレクトリで make run するだけ。1 台の基準、送って持ち寄る①、取り合う②、そして「鎖は分割できない」を、すべて自分の数字で確かめられます。固定小数点の桁の話、シンプソン公式との精度比べ、ロストアップデートの中身、IM2 割り込みの作法——細かい所が気になった方ぶんの実験ノートも、同じリポジトリに 4 本置いてあります。
並列計算の最初の一歩を「体で測る」小さな実験室として、ちょうどいいかもしれない、と個人的には思っています。
この記事は Zenn に初出したものを加筆・補足したものです ── Zenn の元記事を見る