Z80 vs 6502
日記として書いた「6502は遅かったのか?」に、予想外の反響をいただきました。ありがとうございます。
コメントで多数のご意見が寄せられ、非常に面白いものでした、
しかし、このサイトで使用しているシステムでは、コメントはスパム避けもかねて最小限のことしか書けないようになっています。
知識がない人が後から見ても、面白さがさっぱりわからないでしょう。
そこで、こちらのページに、説明と共に記録を残したいと思います。
目次
掛け算(別ページ)
ブロック転送(別ページ)
概要
詳細は元記事を読んでもらうことにして、概要のみ説明しておきます。
ファミコンと MSX は、共に NTSC (家庭用テレビ)の信号出力を前提にしていたため、動作クロック周波数も NTSC を元にしていました。
ただし、ファミコンは MSX のちょうど半分。
このことを根拠に「ファミコンは MSX より遅かった」と思っている人が多いようなのですが、当時の CPU は今とは違って、1命令を数クロックで実行しました。
どちらの CPU にもある類似命令を比べると、ファミコンで使用された 6502 の方が、MSX で使用された Z80 の半分程度以下のクロック数で実行を終了します。
つまり、6502 は決して遅いとは言えません。
ただ、比較はこれだけでは不完全です。
「どちらにも類似した命令がある」場合に命令を比較していますが、Z80 の方が命令の種類は豊富でした。
Z80 の得意な部分を無視して比較すれば、6502 の方が良いように見えるのは当然でしょう。
6502 には存在せず、Z80 に存在する機能としては、16bit 演算命令や、ループ命令などがあります。
そこで、記事の最後に、「1~100を足すプログラム」を示しました。このプログラム自体がループ構造ですし、結果は 8bit に収まらないため 16bit 演算が必要です。
僕の作ったプログラムでは、結果は Z80 の勝ちだけど、ファミコンもそれほど遅くない…というものでした。
しかし、僕も 6502 のプログラムを組んだのは30年ぶりくらい。無駄だらけだ、という指摘がありました。
6502 の改良版を作ってくれる人、負けじと Z80 の改良版を作る人…と、コメント投稿が相次ぎます。
(みんな「名無し」を名乗ろうとするのでわかりませんが、2~3人はいると思います)
この改良合戦が面白かったので、ここで解説するとともに、改良とは別に寄せられた意見も考慮してみよう、というのがこのページの趣旨です。
なお、日記ページは僕の使っているシステム上の都合で編集しにくいのですが、こちらのページは編集時に履歴を残せます。
今後も新たな改良が寄せられたら、ページ上追記・改良していきたいと思います。
改良の投稿は、ページ下部の1行コメント欄でもいいですし、メールで送ってもらっても構いません。(SPAM対策アドレス表記使用)
まだ改良されそうな中で結論は出せないのですが、先に結論めいたことを書いておきます。
そもそもの比較意図は、「ファミコンは MSX より遅い」とした記述に異を唱えることでした。
クロック周波数が半分であっても、個別命令単位の比較ではファミコンはむしろ速いくらいですし、Z80 で有利になる例題を考えて比較しても、両者の速度差はそれほど大きくありません。
もちろん、6502 の有利になる例題を作れば、個別命令が速い 6502 が勝つと思っています。総体として、ファミコンと MSX で、CPU 速度の差はそれほどない、と考えています。
Dhrystone ベンチマークで MIPS 計測するのが筋かもしれませんが、Dhrystone は FOTRRAN や C言語のプログラムをコンパイルすることが前提で、コンパイラによる性能差も大きいですし、そもそも C 言語は 8bit CPU を想定した言語仕様ではありません。
そのため、個別の命令速度比較や、ある程度の規模のプログラム比較で、「同程度」と見なすのが現実的な解答だと思っています。
比較ルール
最後に比較時のルールを作りたいと思います。
今後も意見が寄せられるかもしれないけど、暴走しないために。
上に書いたように、16bit 演算を使うのは意図の一つでした。僕が当初書いた 6502 のプログラムでは、足す数を 16bit 拡張する形で演算を行っています。
でも、足す数は実際には 8bit に収まります。これを 16bit 演算するのは無駄なので、下位 8bit を加算して、繰り上がりが生じた時だけ上位 8bit を操作、というプログラムが寄せられました。
これは、OK とします。現状 Z80 は 16bit のまま加算するのが速いようですし、CPU の性格の違いが出て面白いです。
ループ変数と「足す数」を共用するために計算順序を入れ替える、というような変更も OK です。というか、僕のプログラムでも Z80 版はループ変数と足す数が別でしたが、6502 版は共有していました。
「n*(n+1)/2 の公式を使えば速い」という意見もありました。これは、目的は計算ではなく「機能比較」なので、無しです。
これを許すなら、結果は 5050 と判っているのだから最初から結果レジスタに放り込んでしまえ、となってしまいます。
ただし、別の比較として、Z80 と 6502 の両方でこの公式をプログラムしてみるのは面白そうです。というか、実際に基本プログラムのみ作ってみました。改良お待ちしております。
目的を明確化するためにも、プログラムはある程度の柔軟性を持つものとします。
1~100の加算であれば、100 ではなく任意の n (ただし 8bit)を足せるように考慮しなくてはならないことにします。
これは、最適化する際に、計算回数が 100 回であることを前提に条件判断などを省略してはならない、という意味です。これも、先に書いた「5050を放り込んでしまえ」を防ぐために重要です。
ただし、100回の時に最速となる局所最適化を施すのは構いません。
多少のループ展開や、テーブル参照はやっても構わないと思っていますが、速度・サイズのコスト問題は考慮する必要があります。
これも、目的は現実に即した速度を比較すること、だからです。何パーセントかの速度向上が、同割合程度のサイズ増大で実現できるのであれば、現実的かもしれません。
しかし、コードサイズが倍増しているのに速度は1割向上、等であれば、その改良は現実的ではない、と考えます。
しかしこれは、誰もが実現できないと思っていたことを実現する…などの非常に限定された条件でのみ許される稀有な例だと思います。
おおざっぱにまとめると、現実での利用状況を想定してくれ、ということです。
後ほど他の例も出そうと思っていますが、そうした例題に改良プログラムを考案される際も同じです。
Z80 の速度は、MSX を基準とします。本来の Z80 よりも1~2クロック遅いです。
(この理由説明は日記に書いた本記事参照)
ファミコンの 6502 は、Z80 の半分のクロック周波数で動きます。つまり、6502 の必要クロック数を2倍して Z80 の必要クロック数と比較した時に、少ないほうが速度が速いことになります。
参考:Z80 命令一覧
Z80+M1 が、MSX の速度(命令の実行クロック数)になります。
参考:6502 命令一覧
各命令の cycles で示される部分が速度(命令の実行クロック数)になります。
1~100を足すプログラム
さて、改めて、紹介されたプログラムから現状一番速いものを紹介します。