自作TTLコンピュータで円周率を計算する(第4話)

人間プリプロセッサでマクロを妙訳しながら展開する

本サイトは1970年代にオリジナルCPUの設計と、そのCPUを使ったコンピュータの自作を夢見ていた青年(当時)が40年の月日を経て、完成させたTTLコンピュータ、RETROF-8のソフトウェア作成編です

円周率を求める自作TTLコンピュータ
 2011年8月12日公開

マクロを駆使して16bit変数を8bit化することには成功はしました(第3話参照)。でも最終的にTTLコンピューターの機械語に変換するためには、せっかく定義したマクロも通常のコードに展開する必要があります。もちろんこの処理はプリプロセッサに委ねてもよいのですが、少しでも効率の良いコードを得るために、手動でのプリプロセスに挑戦してみました。かなりマニアックな話ですので、結論のみを知りたい方は本頁末尾の「変換後のソースコード」を御覧ください。

マクロの展開における留意点

マクロの多用は諸刃の剣

C系言語では、マクロは上手に使うとプログラムの可読性を飛躍的に高めることができます。しかし多用しすぎると、一見簡潔に見えるコードが、効率が著しく悪い複雑怪奇なオブジェクトに化けている危険もあります。ここでは、その点に注意しながら、前述のプログラムに含まれるマクロを手動で展開することを試みます。

以下は、前回全ソースを公開した円周率プログラムの冒頭5行です。

SET(I,0);
do{
    SETARRAY(I,2);
    INC(I);
}while(VAL(I) < ARRAYMAX);

これを、#defineに定義されている通りに機械的にマクロ展開してしまうとこうなります。

I[0]=0/256; I[1]=0%256 ;
do{
    if(I[0]==0){H0[I[1]]=2/256;L0[I[1]]=2%256;}
    else       {H1[I[1]]=2/256;L1[I[1]]=2%256;} 
    ++I[1]; if(I[1]==  0) ++I[0] ;
}while(I[0]*256+I[1] < 512); 

しかし、人間が意識しながら展開すると、以下の様に簡潔な表現にすることも可能なのです。

C=0 ;              //最大255なのでインデクスは単純変数で十分
do{
    H0[C] = H1[C] = 0;   //円周率算出用配列の上位8bitを0にする
    L0[C] = L1[C] = 2;   //円周率算出用配列の下位8bitを2にする
    ++C;
}while(!C);         //Cが一周して0に戻ったらループを終える 

マクロ展開の実例

VALマクロとその展開

前述の考察に従いプログラムの効率(この場合の効率とは、手作りTTLコンピュータに移植した時の実行速度)を意識しながら他のマクロも展開します。まずは全てのVAL()マクロを展開します。

#define VAL(x)   (x[0]*256+x[1])

VAL()は上記に示すの様に擬似16bit変数(実体は要素数2の8bit配列変数)の値を取り出すマクロです。

while(VAL(I) < ARRAYMAX)

直訳すると while(I[0]*256+I[1]<512) です。
しかし、I[]は8bitの変数ですから、I[0]*256+ I[1] が512以上になるのはI[1]の値に関わらず、I[0]が3以上の場合のみです。
 従ってこの文は単に while(I[0]<3) と書き換えることができます。

SET(J,VAL(I))

SETマクロは下記の様に定義されています。

#define SET(x,y) x[0]=y/256;x[1]=y%256

マクロが2重になっているので、単純に直訳すると、
J[0]=(I[0] * 256 + I[1]) /256;J[1]=(I[0] * 256 + I[1]) %256
という複雑怪奇な表現になります。しかし落ちついて考えると、VALは2つの8bit変数を16bit値に変換するマクロで、SETは16bit値を2つの8bit変数に格納するマクロです。つまり16bitに変換せずに直接8bit同士で代入すれば済む話であり、単に、J[0]=I[0]; J[1]=I[1]と書くことができます。

if(VAL(J) == 0)

擬似16bit変数の値が0になるのは、上位8bitも下位8bitも0の場合のみですので、これは
if(J[0] == 0 && J[1]==0) と表せます。

while(VAL(K) < VAL(J))

2つの擬似16bit変数の大小比較です。これはまず上位8bitを大小比較し同じなら下位を比較する、という常套手段が使え、while(K[0]<J[0] || K[0]==J[0] && K[1]<J[1]) となります。
演算子の優先順位は&&が||より上ですが、
while(K[0]<J[0] || (K[0]==J[0] && K[1]<J[1])) と書くほうが親切かもしれません。

while(VAL(K) < 10)

定数値との比較です。直訳は while(K[0]==0 && K[1]<10) ですが、プログラムの前後関係をみると、この文の実行時は常にK[0]が0であることが明白ですので、単にwhile(K[1]<10)とできます。

if(VAL(M) < VAL(N))

while(VAL(K) < VAL(J))と同様ですので説明は割愛します。

SETARRAY(J,VAL(M));

配列への変数代入です。配列のインデクスが256以上ならH1[]とL1[]の配列に代入し、そうでなければH0[]とL0[]の配列に代入するという振り分けが発生する以外は変数同士の代入と同じです。
少し長くなりますが、変換結果はこうなります。

if(J[0]) {H1[J[1]]=M[0];L1[J[1]]=M[1];}
else {H1[J[1]]=M[0];L1[J[1]]=M[1];}

dig[VAL(I)] = VAL(M);

digは最終結果を格納する配列です。1要素に1桁が対応しますので幅は8bitで十分です。また要素数は512桁分を用意していますが、小数点152桁以降は誤差が生じることは既知ですので、実際の格納は256番目までとしロジックの簡略化を図りました。変換後のコードは  dig[I[1]]=M[0]; です。

while(VAL(I)>0) ;

if(VAL(J) == 0)と同様ですので説明は割愛します。

人間プリプロセッサの結果

他のマクロも同様に工夫しながら展開した結果が以下です。今回から結果表示は160桁固定とし、見やすくするために20桁毎に改行を挟みました。またデバッグ用の関数を撤廃し、main()だけのプログラムとしました。いずれにせよ、もはや人間用のソースというよりCPU用のオブジェクトであり、可読性は最悪です。

8bit変数と加減算のみで円周率を小数点152桁まで正しく表示するプログラム

VC++ 2008(express edition)にて、コピペで動くことを確認済みです。
他の処理系(GCC等)でもヘッダーを<stdio.h>等に変えれば動くかとは思いますが、確認はしておりません。

//================================================================
// 8bit変数と加減算のみで円周率を小数点152桁まで正しく表示する
// TTLコンピュータ、RETROF-8への移植用。Aug2011 T.GATARO as duo6750
//================================================================
#include "stdafx.h" // ヘッダは処理系に依存する
void main(void) {
          
unsigned char C,I[2],J[2],K[2],M[2],N[2];
unsigned char H0[256],L0[256],H1[256],L1[256],dig[256];
        
C=0 ;                     
do{
    H0[C]=H1[C]=0; 
    L0[C]=L1[C]=2; 
    ++C;
}while(C);                
I[0]=1;I[1]=255; 
do{
    M[0]=M[1]=0; 
    J[0]=I[0];J[1]=I[1]; 
    if(!J[1]) --J[0]; --J[1]; 
    do{
        if(!J[0] && !J[1]) break; 
        N[0]=N[1]=0;
        K[0]=K[1]=0;
        do{
           C=N[1];N[0]+=M[0];N[1]+=M[1];if(N[1]<C)++N[0]; 
           ++K[1]; if(!K[1]) ++K[0] ;
        }while(K[0]<J[0] || (K[0]==J[0] && K[1]<J[1])); 
        M[0]=M[1]=0; 
        K[0]=K[1]=0; 
        do{
            if(J[0]==0) {C=M[1];M[0]+=H0[J[1]];
                         M[1]+=L0[J[1]];if(M[1]<C)++M[0];}
            else        {C=M[1];M[0]+=H1[J[1]];
                         M[1]+=L1[J[1]];if(M[1]<C)++M[0];}
            ++K[1]; if(!K[1]) ++K[0] ;
        }while(K[1]<10); 
        C=M[1];M[0]+=N[0];M[1]+=N[1];if(M[1]<C)++M[0]; 
        N[0]=J[0];N[1]=J[1]; 
        C=N[1];N[0]+=J[0];N[1]+=J[1];if(N[1]<C)++N[0]; 
        if(!N[1]) --N[0]; --N[1];
        K[0]=K[1]=0; 
        do{
            if(M[0]<N[0] || (M[0]==N[0] && M[1]<N[1])) break; 
            C=M[1];M[0]-=N[0];M[1]-=N[1];if(M[1]>C)--M[0]; 
            ++K[1]; if(!K[1]) ++K[0] ;
        }while(1);
        if(J[0]) {H1[J[1]]=M[0];L1[J[1]]=M[1];}
        else     {H0[J[1]]=M[0];L0[J[1]]=M[1];}
        M[0]=K[0];M[1]=K[1]; 
        if(!J[1]) --J[0]; --J[1]; 
    }while(1) ;
    if(I[0]==1) dig[I[1]] = M[1]; 
    --I[1]; if(I[1]==255) --I[0];
}while(I[0] != 0 || I[1] !=0) ;  
C=0;
do {
if (dig[C]>9) dig[C]-=10,dig[C+1]++ ;
++C;
}while(C<255) ;

/****///試験用コード。実機では結果は配列に格納に付、以下は不要 
/****/  for(C=255; C >= 56; --C) {
/****/      if (C%20==15) printf("\n") ;
/****/     printf("%d",dig[C]) ; 
/****/  }
/****/  getchar(); //一時停止(VC++コンソールアプリ用)
}

実行結果

8bit変数と加減算のみでの円周率算出結果


おわりに

RETROF-8のロータリースイッチここまで来るとRETROF-8用の機械語に変換するのも比較的容易です。しかしRETROF-8の基板上のロータリースイッチのみで、これだけの長さのプログラムを入れるのは大作業(数時間かかる)です。しかも一度電源を落としたら、最初からやりなおし。
何か新たな手段が必要です。


TTLコンピュータ

円周率編

  1. C言語でのアルゴリズム検証
  2. 乗除算を加減算に書き換えた
  3. 8bitメモリにshort intを格納
  4. 人間プリプロセッサ
  5. 殆どアセンブラ