C言語でNESアプリを作成(技術的な話題)

大したネタではありませんが、技術的な話題を少しだけ。
コンパイラとハードウェアの特徴を押さえれば、C言語でもそこそこの速度が出せます。

ISO標準との違いとコーディングヒント

cc65のサイトに記載されている内容からざっくりと紹介。

ISO標準との違い
cc65 Users Guide - Differences to the ISO standard <http://cc65.github.io/doc/cc65.html#s4>
  1. 「double」と「float」は使用できません。
    The datatypes "float" and "double" are not available.

    使用した場合はコンパイルエラーになります。整数を固定小数値として扱うことで回避します。

  2. 構造体(または共用体)の値を関数の引数として渡せないし、戻り値として返せない。
    C Functions may not return structs (or unions), and structs may not be passed as parameters by value.
    However,struct assignment *is* possible.

    関数呼び出し時のスタックに積めるサイズが限定されています。
    ポインタ変数であれば構造体や共用体を引数や戻り値で受け渡しが可能です。

  3. 呼び出し規則の互換性がないため、fastcall関数ポインタとcdecl関数ポインタを混在させない。
    Most of the C library is available with only the fastcall calling convention.
    It means that you must not mix pointers to those functions with pointers to user-written,
    cdecl functions (the calling conventions are incompatible).

    原文は標準ライブラリ関数の話題となっていますが、ユーザが作成した関数についても同様です。
    関数ポインタで処理を切り替える場合は要注意です。

    v2.15よりfastcall呼び出しがデフォルトになりました。
    __fastcall__もしくは__cdecl__修飾子が記述されていない場合、関数呼び出しはfastcallになります。
    旧バージョンのライブラリは、リンクはできても正常に呼び出しができない場合があります。

  4. volatileキーワードは効果がありません。
    The volatile keyword doesn't have an effect.
    This is not as bad as it sounds,
    since the 6502 has so few registers that it isn't possible to keep values in registers anyway.

    キャッシュはありませんし、アキュムレータと2本のインデックスレジスタでメモリをガシガシアクセスしてレジスタの値を維持しない(同一変数をポーリングする処理でも毎回メモリから読み込む)コードが出力されるため、もともと影響がないようです。

コーディングヒント
cc65 coding hints <http://cc65.github.io/doc/coding.html>
  1. プロトタイプ宣言を使う。
    Use prototypes
  2. ネストしたブロック内で自動変数を使わない。
    Don't declare auto variables in nested function blocks

    今時のCPUであれば自動変数領域を確保、解放するコストは微々たるものですが、6502上では馬鹿になりません。
    特にループ中で使用する変数は、ループ前に準備しましょう。

  3. コンパイラはハイレベルな最適化ができません。
    Remember that the compiler does no high level optimizations

    素直に機械語コードへ落とし込むこととなります。

  4. long関連は遅い!
    Longs are slow!
  5. できるだけ符号なしの型を使う。
    Use unsigned types wherever possible
  6. int型よりchar型を使う。
    Use chars instead of ints if possible

    8ビットCPUですので扱う値が0〜255で収まる場合はunsigned char型で扱うと高速化につながりますが、オーバーフローには注意が必要です。
    画面停止中の計算など高速性がさほど求められない処理であれば、素直にint型を使った方が良いかもしれません。

  7. 配列の要素型のサイズは1, 2, 4, 8のいずれかで。
    Make the size of your array elements one of 1, 2, 4, 8

    要素型のサイズが2, 4, 8の場合、インデックスからバイトオフセット値を計算する際に乗算ではなくシフト演算が使え、より高速なアドレス計算ができます。
    要素型のサイズがコンパクトな計算結果を収めたテーブルから値を取り出すような処理で効果が期待されます。

  8. 式評価は左から右です。
    Expressions are evaluated from left to right

    演算子の優先順位に従い左から右へ評価を行った際の一時的な値は、その都度変数に格納されます。
    特に定数同士の演算は最初に置くか()で括ると、変数と定数の演算回数を減らすことができます。

  9. プリインクリメントかプリデクリメントを使う。
    Use the preincrement and predecrement operators

    i++やi--の書き方の場合は、演算のために変数の値をメモリから再取得します。
    変数の値を参照しないのであれば、++iや--iを使用することで少しだけ高速化できます。

  10. 絶対アドレスにアクセスする場合は定数を使う。
    Use constants to access absolute memory locations

    定数であれば高速で省メモリなコードが出力されます。
    PPUのコントロールレジスタ等の固定アドレスに配置されたものは予め定数を用意しておくと便利です。
    大域変数は絶対アドレスで参照可能ですので、ポインタ変数へアドレスを代入して参照した場合より、直接参照した方が高速です。
    当然ながらアドレス固定のため、汎用性は低下します。

  11. 初期値が設定されたローカル変数を使う。
    Use initialized local variables

    変数宣言と初期化を分けず、int i = 1;の様に一度に行うと高速で省メモリなコードが出力されます。
    初期値なしの変数はまとめて領域確保されるためブロックの先頭に、初期値付き変数はその後でまとめて宣言します。
    まぜると遅くなります。

  12. ポインタ操作は[]演算子を使う。
    Use the array operator [] even for pointers

    ポインタ変数をインデックス値と合わせて使用する場合、*(a + b)よりa[b]の方が良いコードを出力します。アドレス定数値とインデックス値を格納した変数の組み合わせも同様です。
    アドレス定数とインデックス値の組み合わせは、アドレス演算の方が絶対アドレスに変換するためコンパクトなコードが出力されます。
    ポインタ変数とインデックス用変数の組み合わせではあまり変わらず、インデックス用変数のサイズに応じて16ビットと8ビットあるいは16ビットと16ビットの整数演算でアドレスを求めるため、コード量が一気に増えます。

  13. レジスター変数は注意!
    Use register variables with care

    register修飾子を付加したレジスター変数はゼロページ領域から確保されますが、局所変数のため領域確保時に以前の値をスタックへ退避し、解放時に復帰させます。
    通常の変数のアクセスと比較してアドレス指定が1バイトで済むため速いのですが、2バイトで約70サイクルのオーバーヘッドがかかるため、複数回使わないと処理全体としては遅くなります。
    C言語の書き方にこだわらなければ、アセンブラ側で用意したゼロページ配置の領域を大域変数として扱った方がオーバーヘッドを気にせずに使えます。

  14. 0x7FFFより大きい10進数値はlong int扱いです。
    Decimal constants greater than 0x7FFF are actually long ints

    int型の範囲が-32768〜32767であるため、32768以上はlong型扱いとなります。
    サフィックスなしで-32768以下もしくは32768以上の定数にはコンパイル時に警告が出ますので、定数値の後にU、LやULを付加することで解決します。
    8進数か16進数であればサフィックスなしで記述できます。
    ただし、いずれの場合でも変数の代入可能な範囲を超えた分は破棄されます。

  15. 可変長引数へのアクセスは処理コストが高い。
    Access to parameters in variadic functions is expensive

    プロトタイプ宣言なしの場合を含め、スタックに詰められた引数を操作する処理は処理コストがかかります。
    利用する場合は引数の受け渡しに細心の注意を払い、限定的な利用に留めた方が良さそうです。

ゼロページ

大域変数としてゼロページの領域を使用すると、出力されるコードサイズが小さくなり速度が向上します。
注意点はゼロページのサイズは256バイトしかないことと、大域変数のため関数間で共有してしまうことです。

以下は変数を構造体にまとめ、ゼロページで確保した領域を参照する場合の一例です。

アセンブラソース
  1. C言語ソースとサイズが同じになるよう構造体を定義
  2. ゼロページに構造体のタグでメモリ領域を確保
  3. メモリ領域の先頭に"_"付きのラベルを付加
  4. ラベル名を外部参照可能となるよう宣言
.setcpu		"6502"
.autoimport	on

; ゼロページ配置の変数
.exportzp _zpw
.zeropage

; 構造体の定義
.struct Work
	i		.byte
	j		.byte

	ptr		.word

	array		.res 4
.endstruct

_zpw:
	.tag Work
C言語ソース
  1. アセンブラソースとサイズが同じになるよう構造体を定義
  2. ゼロページに確保した領域のラベルを"_"なしで大域変数名としてextern宣言
  3. 「#pragma zpsym ("変数名");」で上記の変数名をゼロページのシンボルと結びつけ
  4. 常に大域変数として参照
typedef struct Work {
	char i, j;
	char *ptr;
	char array[4];
} Work;

extern Work zpw;
#pragma zpsym ("zpw");  /* zpw is in the zeropage */

void test04_memory_zeropage(void) {
	zpw.i = 0;
	zpw.j = 1;

	zpw.ptr = (char*)0x5000;

	for (zpw.i = 0; zpw.i < 4; ++zpw.i) {
		zpw.array[zpw.i] = zpw.j;
	}
}
マップファイル

ld65やcl65コマンドでリンクを行う際に「-m ファイル名」を指定すると、大域変数や関数の配置状況が分かるマップファイルが出力されます。
これまでのMakefileをそのまま利用した場合は、/bin内の「プロジェクト名.map」として出力します。

C言語で用意した変数名には"_"が付きます。
例えば、"padinfo"変数の場所を探したい場合は、「Exports list by value」内の"_padinfo"を探します。

Exports list by value:
----------------------
sp                        000002 RLZ    sreg                      000004 RLZ    
regsave                   000006 RLZ    ptr1                      00000A RLZ    
ptr4                      000010 RLZ    tmp1                      000012 RLZ    
nsd_work_zp               00001C RLZ    __STACKSIZE__             000200 REA    
_chara                    000400 RLA    _ball                     000450 RLA    
_game                     000458 RLA    _gamemap                  00045E RLA    
_rand_x                   000522 RLA    _rand_y                   000523 RLA    
_rand_z                   000524 RLA    _sync                     000525 RLA    
_padinfo                  000526 RLA    _padother                 00052C RLA    
_dma_cnt                  00052D RLA    _dma_cnt_old              00052E RLA    
_cmd_cnt                  00052F RLA    _screen_adr               000530 RLA    
_scroll_x                 000532 RLA    _scroll_y                 000533 RLA    
上記の例では、0x0526のアドレスに配置されています。
アセンブラリスト

cl65のコマンドは、cc65、ca65、co65及びld65のフロントエンドです。
このコマンドを使ってコンパイルを行う際に「-l ファイル名」を指定すると、コンパイル結果をアセンブラソースリストとして出力します。

先のMakefileでは以下のように修正を加えると、/obj内の「Cソースファイル名.o.asm」として出力します。

$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
	$(CC) $(CFLAGS) -l $@.asm --create-dep $@.dep -c -o $@ $<

いくつかの処理のコンパイル結果を確認してみました。

なおコンパイル結果はコンパイラ自身のバージョンやオプション指定によって変わる可能性があります。
以下のソースは「-Or -Os」のオプション指定にてコンパイルした結果です。

関数呼び出し:通常の関数呼び出しと__cdecl__および__fastcall__をつけた場合の違いを確認
  1. 通常の関数(Ver2.14まで)
    char test01_funcA(char a) {                 000000r 1               .proc   _test01_funcA: near
        return a;                               000000r 1  A2 00            ldx     #$00
    }                                           000002r 1  A1 rr            lda     (sp,x)
                                                000004r 1  4C rr rr         jmp     incsp1
                                                000007r 1               .endproc
    
  2. 通常の関数(Ver2.15以降)
    char test01_funcA(char a) {                 000000r 1               .proc   _test01_funcA: near
        return a;                               000000r 1  20 rr rr         jsr     pusha
    }                                           000003r 1  A2 00            ldx     #$00
                                                000005r 1  A1 rr            lda     (sp,x)
                                                000007r 1  4C rr rr         jmp     incsp1
                                                00000Ar 1               .endproc
    
    
  3. __cdecl__関数:Ver2.14は無指定
    char __cdecl__ test01_funcB1(char a) {      00000Ar 1               .proc   _test01_funcB1: near
        return a;                               00000Ar 1  A2 00            ldx     #$00
    }                                           00000Cr 1  A1 rr            lda     (sp,x)
                                                00000Er 1  4C rr rr         jmp     incsp1
                                                000011r 1               .endproc
    
  4. __fastcall__関数
    char __fastcall__ test01_funcB2(char a) {   000011r 1               .proc   _test01_funcB2: near
        return a;                               000011r 1  20 rr rr         jsr     pusha
    }                                           000014r 1  A2 00            ldx     #$00
                                                000016r 1  A1 rr            lda     (sp,x)
                                                000018r 1  4C rr rr         jmp     incsp1
                                                00001Br 1               .endproc
    
  5. __cdecl__関数(戻り値なし):Ver2.14は無指定
    void __cdecl__ test01_funcC1(char a) {      00001Br 1               .proc   _test01_funcC1: near
    }                                           00001Br 1  4C rr rr         jmp     incsp1
                                                00001Er 1               .endproc
    
  6. __fastcall__関数(戻り値なし)
    void __fastcall__ test01_funcC2(char a) {   00001Er 1               .proc   _test01_funcC2: near
    }                                           00001Er 1  20 rr rr         jsr     pusha
                                                000021r 1  4C rr rr         jmp     incsp1
                                                000024r 1               .endproc
    
  7. 関数の呼び出し元(Ver2.14まで)
    void test01_call_function(void) {           000021r 1               .proc   _test01_call_function: near
        char r;                                 000021r 1  20 rr rr         jsr     decsp1
                                                000024r 1  A9 0A            lda     #$0A
        r = test01_funcA(10);                   000026r 1  20 rr rr         jsr     pusha
        r = test01_funcB1(10);                  000029r 1  20 rr rr         jsr     _test01_funcA
        r = test01_funcB2(10);                  00002Cr 1  A0 00            ldy     #$00
        test01_funcC1(10);                      00002Er 1  91 rr            sta     (sp),y
        test01_funcC2(10);                      000030r 1  A9 0A            lda     #$0A
    }                                           000032r 1  20 rr rr         jsr     pusha
                                                000035r 1  20 rr rr         jsr     _test01_funcB1
                                                000038r 1  A0 00            ldy     #$00
                                                00003Ar 1  91 rr            sta     (sp),y
                                                00003Cr 1  A9 0A            lda     #$0A
                                                00003Er 1  20 rr rr         jsr     _test01_funcB2
                                                000041r 1  A0 00            ldy     #$00
                                                000043r 1  91 rr            sta     (sp),y
                                                000045r 1  A9 0A            lda     #$0A
                                                000047r 1  20 rr rr         jsr     pusha
                                                00004Ar 1  20 rr rr         jsr     _test01_funcC1
                                                00004Dr 1  A9 0A            lda     #$0A
                                                00004Fr 1  20 rr rr         jsr     _test01_funcC2
                                                000052r 1  4C rr rr         jmp     incsp1
                                                000055r 1               .endproc
    
  8. 関数の呼び出し元(Ver2.15以降)
    void test01_call_function(void) {           000024r 1               .proc   _test01_call_function: near
        char r;                                 000024r 1  20 rr rr         jsr     decsp1
                                                000027r 1  A9 0A            lda     #$0A
        r = test01_funcA(10);                   000029r 1  20 rr rr         jsr     _test01_funcA
        r = test01_funcB1(10);                  00002Cr 1  A0 00            ldy     #$00
        r = test01_funcB2(10);                  00002Er 1  91 rr            sta     (sp),y
        test01_funcC1(10);                      000030r 1  A9 0A            lda     #$0A
        test01_funcC2(10);                      000032r 1  20 rr rr         jsr     pusha
    }                                           000035r 1  20 rr rr         jsr     _test01_funcB1
                                                000038r 1  A0 00            ldy     #$00
                                                00003Ar 1  91 rr            sta     (sp),y
                                                00003Cr 1  A9 0A            lda     #$0A
                                                00003Er 1  20 rr rr         jsr     _test01_funcB2
                                                000041r 1  A0 00            ldy     #$00
                                                000043r 1  91 rr            sta     (sp),y
                                                000045r 1  A9 0A            lda     #$0A
                                                000047r 1  20 rr rr         jsr     pusha
                                                00004Ar 1  20 rr rr         jsr     _test01_funcC1
                                                00004Dr 1  A9 0A            lda     #$0A
                                                00004Fr 1  20 rr rr         jsr     _test01_funcC2
                                                000052r 1  4C rr rr         jmp     incsp1
                                                000055r 1               .endproc
    

引数や戻り値は、C言語用のスタック領域やAレジスタを介して受け渡しを行います。

通常の関数呼び出しは、コンパイラのバージョンによって異なります。
Ver2.14以前はcdecl呼び出し、Ver2.15以降はfastcall呼び出しになっています。

呼び出し方法を明示するために__cdecl__と__fastcall__修飾子がありますが、__cdecl__修飾子はVer2.15より利用可能です。

cdecl呼び出しは、全ての引数をスタックに積んだ後に関数が呼び出されます。

fastcall呼び出しは、一番最後の引数はスタックに積まれず、Aレジスタに設定した状態で呼びだされます。
が、呼びだされた関数側では引数領域の解放手順が共通のためスタックに積んでしまいます。
呼び出し先をアセンブラで書き、引数を1つまでに抑えればスタックを使わずに呼び出すことが可能です。

戻り値サイズが1バイトの場合は、Aレジスタにセットされた状態で呼び出し元に戻ります。

ポインタ操作:ポインタ値のアドレス演算と[]による配列演算の違いを確認

コーディングヒントの話題にあったポインタ操作を少し試してみました。

  1. 定数値を使う

    定数同士のアドレス演算の場合、計算はコンパイル時に済ませてしまうため絶対アドレス参照になります。

    #define RAM ((char *)0x6000)
    void test02_pointer_addr_1(void) {     000037r 1               .proc   _test02_pointer_addr_1: near
        *(RAM +  0) = 1;                   000037r 1  A9 01                lda     #$01
        *(RAM + 10) = 2;                   000039r 1  8D 00 60             sta     $6000
        *(RAM +255) = 3;                   00003Cr 1  A9 02                lda     #$02
        *(RAM +256) = 4;                   00003Er 1  8D 0A 60             sta     $600A
    }                                      000041r 1  A9 03                lda     #$03
                                           000043r 1  8D FF 60             sta     $60FF
                                           000046r 1  A9 04                lda     #$04
                                           000048r 1  8D 00 61             sta     $6100
                                           00004Br 1  60                   rts
                                           00004Cr 1               .endproc
    

    配列演算の場合、インデックスレジスタを使用するため若干コードが長くなります。

    void test02_pointer_array_1(void) {    00035Dr 1               .proc   _test02_pointer_array_1: near
        RAM[  0] = 1;                      00035Dr 1  A2 60                ldx     #$60
        RAM[ 10] = 2;                      00035Fr 1  A9 00                lda     #$00
        RAM[255] = 3;                      000361r 1  85 rr                sta     ptr1
        RAM[256] = 4;                      000363r 1  86 rr                stx     ptr1+1
    }                                      000365r 1  A9 01                lda     #$01
                                           000367r 1  A0 00                ldy     #$00
                                           000369r 1  91 rr                sta     (ptr1),y
                                           00036Br 1  A9 02                lda     #$02
                                           00036Dr 1  A0 0A                ldy     #$0A
                                           00036Fr 1  91 rr                sta     (ptr1),y
                                           000371r 1  A9 03                lda     #$03
                                           000373r 1  A0 FF                ldy     #$FF
                                           000375r 1  91 rr                sta     (ptr1),y
                                           000377r 1  A9 00                lda     #$00
                                           000379r 1  20 rr rr             jsr     pushax
                                           00037Cr 1  A9 04                lda     #$04
                                           00037Er 1  A0 01                ldy     #$01
                                           000380r 1  18                   clc
                                           000381r 1  48                   pha
                                           000382r 1  98                   tya
                                           000383r 1  71 rr                adc     (sp),y
                                           000385r 1  91 rr                sta     (sp),y
                                           000387r 1  68                   pla
                                           000388r 1  88                   dey
                                           000389r 1  4C rr rr             jmp     staspidx
                                           00038Cr 1               .endproc
    

  2. ポインタ定数とインデックス用変数を使う

    アドレス演算の場合、周囲の定数値も交えて結果を求めます。
    インデックス用変数がunsigned charの場合は256以上の値がオーバーフローするため注意が必要です。
    intやunsigned intの変数の場合は256バイトの境界はありませんが、アドレス計算のコード量が一気に増えます。

    void test02_pointer_addr_2(void) {     00004Cr 1               .proc   _test02_pointer_addr_2: near
        unsigned char i;                   00004Cr 1  20 rr rr             jsr     decsp1
                                           00004Fr 1  A9 00                lda     #$00
        i =   0; *(RAM + i) = 1;           000051r 1  A8                   tay
        i =  10; *(RAM + i) = 2;           000052r 1  91 rr                sta     (sp),y
        i = 255; *(RAM + i) = 3;           000054r 1  A2 60                ldx     #$60
        i = 256; *(RAM + i) = 4;           000056r 1  18                   clc
    }                                      000057r 1  71 rr                adc     (sp),y
                                           000059r 1  90 01                bcc     L0242
                                           00005Br 1  E8                   inx
                                           00005Cr 1  85 rr        L0242:  sta     ptr1
                                           00005Er 1  86 rr                stx     ptr1+1
                                           000060r 1  A9 01                lda     #$01
                                           000062r 1  91 rr                sta     (ptr1),y
                                           000064r 1  A9 0A                lda     #$0A
                                           000066r 1  91 rr                sta     (sp),y
                                           000068r 1  A2 60                ldx     #$60
                                           00006Ar 1  98                   tya
                                           00006Br 1  18                   clc
                                           00006Cr 1  71 rr                adc     (sp),y
                                           00006Er 1  90 01                bcc     L0243
                                           000070r 1  E8                   inx
                                           000071r 1  85 rr        L0243:  sta     ptr1
                                           000073r 1  86 rr                stx     ptr1+1
                                           000075r 1  A9 02                lda     #$02
                                           000077r 1  91 rr                sta     (ptr1),y
                                           000079r 1  A9 FF                lda     #$FF
                                           00007Br 1  91 rr                sta     (sp),y
                                           00007Dr 1  A2 60                ldx     #$60
                                           00007Fr 1  98                   tya
                                           000080r 1  18                   clc
                                           000081r 1  71 rr                adc     (sp),y
                                           000083r 1  90 01                bcc     L0244
                                           000085r 1  E8                   inx
                                           000086r 1  85 rr        L0244:  sta     ptr1
                                           000088r 1  86 rr                stx     ptr1+1
                                           00008Ar 1  A9 03                lda     #$03
                                           00008Cr 1  91 rr                sta     (ptr1),y
                                           00008Er 1  98                   tya
                                           00008Fr 1  91 rr                sta     (sp),y
                                           000091r 1  A2 60                ldx     #$60
                                           000093r 1  18                   clc
                                           000094r 1  71 rr                adc     (sp),y
                                           000096r 1  90 01                bcc     L0245
                                           000098r 1  E8                   inx
                                           000099r 1  85 rr        L0245:  sta     ptr1
                                           00009Br 1  86 rr                stx     ptr1+1
                                           00009Dr 1  A9 04                lda     #$04
                                           00009Fr 1  91 rr                sta     (ptr1),y
                                           0000A1r 1  4C rr rr             jmp     incsp1
                                           0000A4r 1               .endproc
    

    unsigned char型のインデックス用変数で配列演算を行った場合、先頭アドレスとインデックスレジスタによるアクセスが可能になるためコード量がコンパクトにまとまります。
    intやunsigned int型の変数も利用できますが、アドレス計算が発生するためコード量が増えます。

    void test02_pointer_array_2(void) {    00038Cr 1               .proc   _test02_pointer_array_2: near
        unsigned char i;                   00038Cr 1  20 rr rr             jsr     decsp1
                                           00038Fr 1  A9 00                lda     #$00
        i =   0; RAM[i] = 1;               000391r 1  A8                   tay
        i =  10; RAM[i] = 2;               000392r 1  91 rr                sta     (sp),y
        i = 255; RAM[i] = 3;               000394r 1  A9 01                lda     #$01
        i = 256; RAM[i] = 4;               000396r 1  99 00 60             sta     $6000,y
    }                                      000399r 1  A9 0A                lda     #$0A
                                           00039Br 1  91 rr                sta     (sp),y
                                           00039Dr 1  A8                   tay
                                           00039Er 1  A9 02                lda     #$02
                                           0003A0r 1  99 00 60             sta     $6000,y
                                           0003A3r 1  A9 FF                lda     #$FF
                                           0003A5r 1  A0 00                ldy     #$00
                                           0003A7r 1  91 rr                sta     (sp),y
                                           0003A9r 1  A8                   tay
                                           0003AAr 1  A9 03                lda     #$03
                                           0003ACr 1  99 00 60             sta     $6000,y
                                           0003AFr 1  A9 00                lda     #$00
                                           0003B1r 1  C8                   iny
                                           0003B2r 1  91 rr                sta     (sp),y
                                           0003B4r 1  A9 04                lda     #$04
                                           0003B6r 1  99 00 60             sta     $6000,y
                                           0003B9r 1  4C rr rr             jmp     incsp1
                                           0003BCr 1               .endproc
    

  3. ポインタ変数とインデックス用変数を使う

    unsigned char型のインデックス用変数の場合でもかなりコード量が増えます。

    void test02_pointer_addr_5(void) {     000158r 1               .proc   _test02_pointer_addr_5: near
        unsigned char i;                   000158r 1  20 rr rr             jsr     decsp1
        char *ptr = RAM;                   00015Br 1  A2 60                ldx     #$60
                                           00015Dr 1  A9 00                lda     #$00
        i =   0; *(ptr + i) = 1;           00015Fr 1  20 rr rr             jsr     pushax
        i =  10; *(ptr + i) = 2;           000162r 1  A0 02                ldy     #$02
        i = 255; *(ptr + i) = 3;           000164r 1  91 rr                sta     (sp),y
        i = 256; *(ptr + i) = 4;           000166r 1  AA                   tax
    }                                      000167r 1  B1 rr                lda     (sp),y
                                           000169r 1  18                   clc
                                           00016Ar 1  A0 00                ldy     #$00
                                           00016Cr 1  71 rr                adc     (sp),y
                                           00016Er 1  85 rr                sta     ptr1
                                           000170r 1  8A                   txa
                                           000171r 1  C8                   iny
                                           000172r 1  71 rr                adc     (sp),y
                                           000174r 1  85 rr                sta     ptr1+1
                                           000176r 1  98                   tya
                                           000177r 1  88                   dey
                                           000178r 1  91 rr                sta     (ptr1),y
                                           00017Ar 1  A9 0A                lda     #$0A
                                           00017Cr 1  A0 02                ldy     #$02
                                           00017Er 1  91 rr                sta     (sp),y
                                           000180r 1  18                   clc
                                           000181r 1  A0 00                ldy     #$00
                                           000183r 1  71 rr                adc     (sp),y
                                           000185r 1  85 rr                sta     ptr1
                                           000187r 1  8A                   txa
                                           000188r 1  C8                   iny
                                           000189r 1  71 rr                adc     (sp),y
                                           00018Br 1  85 rr                sta     ptr1+1
                                           00018Dr 1  A9 02                lda     #$02
                                           00018Fr 1  88                   dey
                                           000190r 1  91 rr                sta     (ptr1),y
                                           000192r 1  A9 FF                lda     #$FF
                                           000194r 1  A0 02                ldy     #$02
                                           000196r 1  91 rr                sta     (sp),y
                                           000198r 1  18                   clc
                                           000199r 1  A0 00                ldy     #$00
                                           00019Br 1  71 rr                adc     (sp),y
                                           00019Dr 1  85 rr                sta     ptr1
                                           00019Fr 1  8A                   txa
                                           0001A0r 1  C8                   iny
                                           0001A1r 1  71 rr                adc     (sp),y
                                           0001A3r 1  85 rr                sta     ptr1+1
                                           0001A5r 1  A9 03                lda     #$03
                                           0001A7r 1  88                   dey
                                           0001A8r 1  91 rr                sta     (ptr1),y
                                           0001AAr 1  8A                   txa
                                           0001ABr 1  A0 02                ldy     #$02
                                           0001ADr 1  91 rr                sta     (sp),y
                                           0001AFr 1  18                   clc
                                           0001B0r 1  A8                   tay
                                           0001B1r 1  71 rr                adc     (sp),y
                                           0001B3r 1  85 rr                sta     ptr1
                                           0001B5r 1  8A                   txa
                                           0001B6r 1  C8                   iny
                                           0001B7r 1  71 rr                adc     (sp),y
                                           0001B9r 1  85 rr                sta     ptr1+1
                                           0001BBr 1  A9 04                lda     #$04
                                           0001BDr 1  88                   dey
                                           0001BEr 1  91 rr                sta     (ptr1),y
                                           0001C0r 1  4C rr rr             jmp     incsp3
                                           0001C3r 1               .endproc
    

    []演算子を使った場合でもコード量が一気に増えました。

    void test02_pointer_array_5(void) {    000464r 1               .proc   _test02_pointer_array_5: near
        unsigned char i;                   000464r 1  20 rr rr             jsr     decsp1
        char *ptr = RAM;                   000467r 1  A2 60                ldx     #$60
                                           000469r 1  A9 00                lda     #$00
        i =   0; ptr[i] = 1;               00046Br 1  20 rr rr             jsr     pushax
        i =  10; ptr[i] = 2;               00046Er 1  A0 02                ldy     #$02
        i = 255; ptr[i] = 3;               000470r 1  91 rr                sta     (sp),y
        i = 256; ptr[i] = 4;               000472r 1  AA                   tax
    }                                      000473r 1  B1 rr                lda     (sp),y
                                           000475r 1  18                   clc
                                           000476r 1  A0 00                ldy     #$00
                                           000478r 1  71 rr                adc     (sp),y
                                           00047Ar 1  85 rr                sta     ptr1
                                           00047Cr 1  8A                   txa
                                           00047Dr 1  C8                   iny
                                           00047Er 1  71 rr                adc     (sp),y
                                           000480r 1  85 rr                sta     ptr1+1
                                           000482r 1  98                   tya
                                           000483r 1  88                   dey
                                           000484r 1  91 rr                sta     (ptr1),y
                                           000486r 1  A9 0A                lda     #$0A
                                           000488r 1  A0 02                ldy     #$02
                                           00048Ar 1  91 rr                sta     (sp),y
                                           00048Cr 1  18                   clc
                                           00048Dr 1  A0 00                ldy     #$00
                                           00048Fr 1  71 rr                adc     (sp),y
                                           000491r 1  85 rr                sta     ptr1
                                           000493r 1  8A                   txa
                                           000494r 1  C8                   iny
                                           000495r 1  71 rr                adc     (sp),y
                                           000497r 1  85 rr                sta     ptr1+1
                                           000499r 1  A9 02                lda     #$02
                                           00049Br 1  88                   dey
                                           00049Cr 1  91 rr                sta     (ptr1),y
                                           00049Er 1  A9 FF                lda     #$FF
                                           0004A0r 1  A0 02                ldy     #$02
                                           0004A2r 1  91 rr                sta     (sp),y
                                           0004A4r 1  18                   clc
                                           0004A5r 1  A0 00                ldy     #$00
                                           0004A7r 1  71 rr                adc     (sp),y
                                           0004A9r 1  85 rr                sta     ptr1
                                           0004ABr 1  8A                   txa
                                           0004ACr 1  C8                   iny
                                           0004ADr 1  71 rr                adc     (sp),y
                                           0004AFr 1  85 rr                sta     ptr1+1
                                           0004B1r 1  A9 03                lda     #$03
                                           0004B3r 1  88                   dey
                                           0004B4r 1  91 rr                sta     (ptr1),y
                                           0004B6r 1  8A                   txa
                                           0004B7r 1  A0 02                ldy     #$02
                                           0004B9r 1  91 rr                sta     (sp),y
                                           0004BBr 1  18                   clc
                                           0004BCr 1  A8                   tay
                                           0004BDr 1  71 rr                adc     (sp),y
                                           0004BFr 1  85 rr                sta     ptr1
                                           0004C1r 1  8A                   txa
                                           0004C2r 1  C8                   iny
                                           0004C3r 1  71 rr                adc     (sp),y
                                           0004C5r 1  85 rr                sta     ptr1+1
                                           0004C7r 1  A9 04                lda     #$04
                                           0004C9r 1  88                   dey
                                           0004CAr 1  91 rr                sta     (ptr1),y
                                           0004CCr 1  4C rr rr             jmp     incsp3
                                           0004CFr 1               .endproc
    

  4. 静的変数を使う

    静的なポインタ変数を利用するだけで、先の例と比較すると結構コード量が減ります。

    static char *static_ptr;
    void test02_pointer_addr_8(void) {     000280r 1               .proc   _test02_pointer_addr_8: near
        unsigned char i;                   000280r 1  20 rr rr             jsr     decsp1
        static_ptr = RAM;                  000283r 1  A2 60                ldx     #$60
                                           000285r 1  A9 00                lda     #$00
        i =   0; *(static_ptr + i) = 1;    000287r 1  8D rr rr             sta     _static_ptr
        i =  10; *(static_ptr + i) = 2;    00028Ar 1  8E rr rr             stx     _static_ptr+1
        i = 255; *(static_ptr + i) = 3;    00028Dr 1  A8                   tay
        i = 256; *(static_ptr + i) = 4;    00028Er 1  91 rr                sta     (sp),y
    }                                      000290r 1  18                   clc
                                           000291r 1  6D rr rr             adc     _static_ptr
                                           000294r 1  AE rr rr             ldx     _static_ptr+1
                                           000297r 1  90 01                bcc     L0275
                                           000299r 1  E8                   inx
                                           00029Ar 1  85 rr        L0275:  sta     ptr1
                                           00029Cr 1  86 rr                stx     ptr1+1
                                           00029Er 1  A9 01                lda     #$01
                                           0002A0r 1  91 rr                sta     (ptr1),y
                                           0002A2r 1  A9 0A                lda     #$0A
                                           0002A4r 1  91 rr                sta     (sp),y
                                           0002A6r 1  18                   clc
                                           0002A7r 1  6D rr rr             adc     _static_ptr
                                           0002AAr 1  AE rr rr             ldx     _static_ptr+1
                                           0002ADr 1  90 01                bcc     L0276
                                           0002AFr 1  E8                   inx
                                           0002B0r 1  85 rr        L0276:  sta     ptr1
                                           0002B2r 1  86 rr                stx     ptr1+1
                                           0002B4r 1  A9 02                lda     #$02
                                           0002B6r 1  91 rr                sta     (ptr1),y
                                           0002B8r 1  A9 FF                lda     #$FF
                                           0002BAr 1  91 rr                sta     (sp),y
                                           0002BCr 1  18                   clc
                                           0002BDr 1  6D rr rr             adc     _static_ptr
                                           0002C0r 1  AE rr rr             ldx     _static_ptr+1
                                           0002C3r 1  90 01                bcc     L0277
                                           0002C5r 1  E8                   inx
                                           0002C6r 1  85 rr        L0277:  sta     ptr1
                                           0002C8r 1  86 rr                stx     ptr1+1
                                           0002CAr 1  A9 03                lda     #$03
                                           0002CCr 1  91 rr                sta     (ptr1),y
                                           0002CEr 1  98                   tya
                                           0002CFr 1  91 rr                sta     (sp),y
                                           0002D1r 1  18                   clc
                                           0002D2r 1  6D rr rr             adc     _static_ptr
                                           0002D5r 1  AE rr rr             ldx     _static_ptr+1
                                           0002D8r 1  90 01                bcc     L0278
                                           0002DAr 1  E8                   inx
                                           0002DBr 1  85 rr        L0278:  sta     ptr1
                                           0002DDr 1  86 rr                stx     ptr1+1
                                           0002DFr 1  A9 04                lda     #$04
                                           0002E1r 1  91 rr                sta     (ptr1),y
                                           0002E3r 1  4C rr rr             jmp     incsp1
                                           0002E6r 1               .endproc
    

    []演算子を使った場合も、自動変数よりコンパクトです。

    void test02_pointer_array_8(void) {    0005E2r 1               .proc   _test02_pointer_array_8: near
        unsigned char i;                   0005E2r 1  20 rr rr             jsr     decsp1
        static_ptr = RAM;                  0005E5r 1  A2 60                ldx     #$60
                                           0005E7r 1  A9 00                lda     #$00
        i =   0; static_ptr[i] = 1;        0005E9r 1  8D rr rr             sta     _static_ptr
        i =  10; static_ptr[i] = 2;        0005ECr 1  8E rr rr             stx     _static_ptr+1
        i = 255; static_ptr[i] = 3;        0005EFr 1  A8                   tay
        i = 256; static_ptr[i] = 4;        0005F0r 1  91 rr                sta     (sp),y
    }                                      0005F2r 1  18                   clc
                                           0005F3r 1  6D rr rr             adc     _static_ptr
                                           0005F6r 1  AE rr rr             ldx     _static_ptr+1
                                           0005F9r 1  90 01                bcc     L027D
                                           0005FBr 1  E8                   inx
                                           0005FCr 1  85 rr        L027D:  sta     ptr1
                                           0005FEr 1  86 rr                stx     ptr1+1
                                           000600r 1  A9 01                lda     #$01
                                           000602r 1  91 rr                sta     (ptr1),y
                                           000604r 1  A9 0A                lda     #$0A
                                           000606r 1  91 rr                sta     (sp),y
                                           000608r 1  18                   clc
                                           000609r 1  6D rr rr             adc     _static_ptr
                                           00060Cr 1  AE rr rr             ldx     _static_ptr+1
                                           00060Fr 1  90 01                bcc     L027E
                                           000611r 1  E8                   inx
                                           000612r 1  85 rr        L027E:  sta     ptr1
                                           000614r 1  86 rr                stx     ptr1+1
                                           000616r 1  A9 02                lda     #$02
                                           000618r 1  91 rr                sta     (ptr1),y
                                           00061Ar 1  A9 FF                lda     #$FF
                                           00061Cr 1  91 rr                sta     (sp),y
                                           00061Er 1  18                   clc
                                           00061Fr 1  6D rr rr             adc     _static_ptr
                                           000622r 1  AE rr rr             ldx     _static_ptr+1
                                           000625r 1  90 01                bcc     L027F
                                           000627r 1  E8                   inx
                                           000628r 1  85 rr        L027F:  sta     ptr1
                                           00062Ar 1  86 rr                stx     ptr1+1
                                           00062Cr 1  A9 03                lda     #$03
                                           00062Er 1  91 rr                sta     (ptr1),y
                                           000630r 1  98                   tya
                                           000631r 1  91 rr                sta     (sp),y
                                           000633r 1  18                   clc
                                           000634r 1  6D rr rr             adc     _static_ptr
                                           000637r 1  AE rr rr             ldx     _static_ptr+1
                                           00063Ar 1  90 01                bcc     L0280
                                           00063Cr 1  E8                   inx
                                           00063Dr 1  85 rr        L0280:  sta     ptr1
                                           00063Fr 1  86 rr                stx     ptr1+1
                                           000641r 1  A9 04                lda     #$04
                                           000643r 1  91 rr                sta     (ptr1),y
                                           000645r 1  4C rr rr             jmp     incsp1
                                           000648r 1               .endproc
    
  5. ゼロページ変数を使う

    基本的には静的変数を利用する場合と変わりません。
    メモリアドレッシングの違いから若干コンパクトにまとまります。

    extern Work zpw;
    #pragma zpsym ("zpw");  /* zpw is in the zeropage */
    
    void test02_pointer_addr_z(void) {       00035Dr 1               .proc   _test02_pointer_addr_z: near
        zpw.i =   0; *(zpw.ptr + zpw.i) = 1; 00035Dr 1  A9 00                lda     #$00
        zpw.i =  10; *(zpw.ptr + zpw.i) = 2; 00035Fr 1  85 rr                sta     _zpw
        zpw.i = 255; *(zpw.ptr + zpw.i) = 3; 000361r 1  A5 rr                lda     _zpw+2
        zpw.i = 256; *(zpw.ptr + zpw.i) = 4; 000363r 1  A6 rr                ldx     _zpw+2+1
    }                                        000365r 1  18                   clc
                                             000366r 1  65 rr                adc     _zpw
                                             000368r 1  90 01                bcc     L0279
                                             00036Ar 1  E8                   inx
                                             00036Br 1  85 rr        L0279:  sta     ptr1
                                             00036Dr 1  86 rr                stx     ptr1+1
                                             00036Fr 1  A9 01                lda     #$01
                                             000371r 1  A0 00                ldy     #$00
                                             000373r 1  91 rr                sta     (ptr1),y
                                             000375r 1  A9 0A                lda     #$0A
                                             000377r 1  85 rr                sta     _zpw
                                             000379r 1  A5 rr                lda     _zpw+2
                                             00037Br 1  A6 rr                ldx     _zpw+2+1
                                             00037Dr 1  18                   clc
                                             00037Er 1  65 rr                adc     _zpw
                                             000380r 1  90 01                bcc     L027A
                                             000382r 1  E8                   inx
                                             000383r 1  85 rr        L027A:  sta     ptr1
                                             000385r 1  86 rr                stx     ptr1+1
                                             000387r 1  A9 02                lda     #$02
                                             000389r 1  91 rr                sta     (ptr1),y
                                             00038Br 1  A9 FF                lda     #$FF
                                             00038Dr 1  85 rr                sta     _zpw
                                             00038Fr 1  A5 rr                lda     _zpw+2
                                             000391r 1  A6 rr                ldx     _zpw+2+1
                                             000393r 1  18                   clc
                                             000394r 1  65 rr                adc     _zpw
                                             000396r 1  90 01                bcc     L027B
                                             000398r 1  E8                   inx
                                             000399r 1  85 rr        L027B:  sta     ptr1
                                             00039Br 1  86 rr                stx     ptr1+1
                                             00039Dr 1  A9 03                lda     #$03
                                             00039Fr 1  91 rr                sta     (ptr1),y
                                             0003A1r 1  84 rr                sty     _zpw
                                             0003A3r 1  A5 rr                lda     _zpw+2
                                             0003A5r 1  A6 rr                ldx     _zpw+2+1
                                             0003A7r 1  18                   clc
                                             0003A8r 1  65 rr                adc     _zpw
                                             0003AAr 1  90 01                bcc     L027C
                                             0003ACr 1  E8                   inx
                                             0003ADr 1  85 rr        L027C:  sta     ptr1
                                             0003AFr 1  86 rr                stx     ptr1+1
                                             0003B1r 1  A9 04                lda     #$04
                                             0003B3r 1  91 rr                sta     (ptr1),y
                                             0003B5r 1  60                   rts
                                             0003B6r 1               .endproc
    

    変数を使う場合は、[]演算子でゼロページ変数を使った場合が一番コンパクトになります。

    void test02_pointer_array_z(void) {    0006BFr 1               .proc   _test02_pointer_array_z: near
        zpw.i =   0; zpw.ptr[zpw.i] = 1;   0006BFr 1  A9 00                lda     #$00
        zpw.i =  10; zpw.ptr[zpw.i] = 2;   0006C1r 1  85 rr                sta     _zpw
        zpw.i = 255; zpw.ptr[zpw.i] = 3;   0006C3r 1  A5 rr                lda     _zpw+2
        zpw.i = 256; zpw.ptr[zpw.i] = 4;   0006C5r 1  A6 rr                ldx     _zpw+2+1
    }                                      0006C7r 1  18                   clc
                                           0006C8r 1  65 rr                adc     _zpw
                                           0006CAr 1  90 01                bcc     L0281
                                           0006CCr 1  E8                   inx
                                           0006CDr 1  85 rr        L0281:  sta     ptr1
                                           0006CFr 1  86 rr                stx     ptr1+1
                                           0006D1r 1  A9 01                lda     #$01
                                           0006D3r 1  A0 00                ldy     #$00
                                           0006D5r 1  91 rr                sta     (ptr1),y
                                           0006D7r 1  A9 0A                lda     #$0A
                                           0006D9r 1  85 rr                sta     _zpw
                                           0006DBr 1  A5 rr                lda     _zpw+2
                                           0006DDr 1  A6 rr                ldx     _zpw+2+1
                                           0006DFr 1  18                   clc
                                           0006E0r 1  65 rr                adc     _zpw
                                           0006E2r 1  90 01                bcc     L0282
                                           0006E4r 1  E8                   inx
                                           0006E5r 1  85 rr        L0282:  sta     ptr1
                                           0006E7r 1  86 rr                stx     ptr1+1
                                           0006E9r 1  A9 02                lda     #$02
                                           0006EBr 1  91 rr                sta     (ptr1),y
                                           0006EDr 1  A9 FF                lda     #$FF
                                           0006EFr 1  85 rr                sta     _zpw
                                           0006F1r 1  A5 rr                lda     _zpw+2
                                           0006F3r 1  A6 rr                ldx     _zpw+2+1
                                           0006F5r 1  18                   clc
                                           0006F6r 1  65 rr                adc     _zpw
                                           0006F8r 1  90 01                bcc     L0283
                                           0006FAr 1  E8                   inx
                                           0006FBr 1  85 rr        L0283:  sta     ptr1
                                           0006FDr 1  86 rr                stx     ptr1+1
                                           0006FFr 1  A9 03                lda     #$03
                                           000701r 1  91 rr                sta     (ptr1),y
                                           000703r 1  84 rr                sty     _zpw
                                           000705r 1  A5 rr                lda     _zpw+2
                                           000707r 1  A6 rr                ldx     _zpw+2+1
                                           000709r 1  18                   clc
                                           00070Ar 1  65 rr                adc     _zpw
                                           00070Cr 1  90 01                bcc     L0284
                                           00070Er 1  E8                   inx
                                           00070Fr 1  85 rr        L0284:  sta     ptr1
                                           000711r 1  86 rr                stx     ptr1+1
                                           000713r 1  A9 04                lda     #$04
                                           000715r 1  91 rr                sta     (ptr1),y
                                           000717r 1  60                   rts
                                           000718r 1               .endproc
    


上記を含めたテストコードをまとめたものはこちら。
なお、コンパイル結果を楽しむものです。実行はできません。

  1. nesapp-asmtest.zipアセンブラソースリスト出力テスト用プロジェクト

ついでにcc65@wiki様の「kihon.h」のパッドの変数をゼロページに配置してみたサンプルです。
動作確認用のプロジェクトも作業用の変数とコサインテーブルをゼロページに配置したバージョンです。
テーブルコピーの関数が一つ増えたにも関わらず、プログラムサイズは数バイトですが減りました。

  1. nesapp-libkihon2.zip「ライブラリ本体」
  2. nesapp-test_libkihon2.zip「ライブラリの動作確認用プロジェクト」