トップC-Tips > register指定子


register指定子

1.register指定子

C言語の関数では仮引数やローカル変数は通常、主記憶上のスタックエリアに割り当てられる。 コンピュータにはメインメモリやキャッシュメモリよりももっと 高速にアクセスできるレジスタと呼ばれるメモリがある[1]。

register指定子は仮引数やローカル変数をなるべくレジスタに割り当てるように指定するものである。

レジスタの数には限りがあるため、レジスタに割り当てられない場合もある。 コンパイラによっては、register指定子を無視するコンパイラもある。

レジスタには名前が付けられており、メインメモリのようにアドレスがあるわけではない。 したがって、レジスタ変数(register指定子を付けた変数)のアドレスを求めることはできない。 例えば、

  register int n;
のように宣言した場合、int *p = &n; のようなプログラムは書けなくなる。

2.使用事例

register指定子の使用事例を下に示す。

void Reverse(register int *b, register int *e) {
    while (b < e) {
        register int tmp = *b;
        *b++ = *e;
        *e-- = tmp;
    }
}

VC++ ではregister指定子なしのときのプログラムと実行時間は全く変わらなかった。 コンパイル結果(機械語コード)を確認したが全く変化がなかった。

GCC では大幅な高速化が図れた。b から e までのデータ数が 333個の場合、 約3倍も速くなった[2]。

一般的にはコンパイラには色んな最適化処理が含まれているため、 コンパイラに任せればいい。

しかし、このプログラムも b から e までのデータ数が数個であれば、 レジスタ変数を使わない方が速いかも知れない。

つまり、最適化といっても、実際に引数の値がいくらかになるかは、 コンパイラには分からないため、その関数の使われ方が関係するような最適化は行われない。

コンパイラや機械語に詳しい人間が見れば、どの変数をレジスタ変数に割り当てれば良さそうか が分かるが、コンパイラ単独の最適化ではそこまでのことはせず、プログラマの協力を得て、 このようなスリムなコードが生成できたのであろう。

上の Reverse関数は3変数だけのシンプルなプログラムであったが、 もし変数が6つならどうするか、全てに register指定子を付けるとどうなるか? 多分、最初の2つか3つの変数がレジスタに割り付けられるだろう。 ベストな3つの変数が選べるわけではない。なぜなら、どれらの変数をレジスタに割り当てるのがいいか、 コンパイラには分からない。 それは実引数の値などに依存するわけであり、 プログラマがこの関数がどのような使われ方をするかをよく考えて、 2、3の変数に限定して register指定子をつけなければならない。 つまり、一般的には、プログラマの協力があって初めて register指定子が有効に機能する。

register変数は余り使われないし、サポートしていないコンパイラもあるが、 特にプログラムの高速化が求められる場合には、検討項目の一つになる。

GCCがどのような命令コードを生成しているか調べてみた。結果を下に示す。 これは MASM32の逆アセンブル機能を使って求めたものである。 ;; から始まるコメントは筆者が後から追加したものである。

tmp, b, e はそれぞれレジスタ ebx, edx, eax に割り当てられている。 ecx、esi がワークレジスタとして使われている。 アセンブリ言語で考えればさらなる高速化が可能かも知れないが、 非常にスリムなコードである。

004016B0                    fn_004016B0:                ; Xref 0040172F
004016B0 55                     push    ebp
004016B1 89E5                   mov     ebp,esp
004016B3 56                     push    esi
004016B4 53                     push    ebx                 ;; tmp
004016B5 8B5508                 mov     edx,[ebp+8]         ;; b
004016B8 8B450C                 mov     eax,[ebp+0Ch]       ;; e
004016BB EB12                   jmp     loc_004016CF 

004016BD                    loc_004016BD:               ; Xref 004016D1
004016BD 8B1A                   mov     ebx,[edx]           ;; tmp = *b
004016BF 89D1                   mov     ecx,edx
004016C1 8D5104                 lea     edx,[ecx+4]         ;; b++
004016C4 8B30                   mov     esi,[eax]
004016C6 8931                   mov     [ecx],esi           ;; *b = *e (bはインクリメント前の値)
004016C8 89C1                   mov     ecx,eax             ;; eax <- eax - 4   e--
004016CA 8D41FC                 lea     eax,[ecx-4]         ; 
004016CD 8919                   mov     [ecx],ebx           ;; *e = tmp
004016CF                    loc_004016CF:               ; Xref 004016BB
004016CF 39C2                   cmp     edx,eax             ;; b : e
004016D1 72EA                   jb      loc_004016BD
004016D3 5B                     pop     ebx
004016D4 5E                     pop     esi
004016D5 5D                     pop     ebp
004016D6 C3                     ret

ReverseP関数を次のように浮動小数点演算関数に変更して計測した。 register指定子が無い時は 4.2秒、register指定子を付けた時は 1.2秒となり、3倍以上高速となった。

double Sum(register double *b, register double *e) {
    register double sum = 0;
    while (b < e) {
        sum += *b++;
    }
    return sum;
}

Reference

[1] IA-32
[2] 配列をポインタで走査する