先頭へ戻る

最適化に関する機能 | Programming Place Plus C言語編 第57章

Programming Place Plus トップページ -- C言語編

先頭へ戻る

この章の概要

この章の概要です。


register

register (register記憶域クラス指定子) は、対象のオブジェクトへのアクセスを可能な限り高速に行えるように最適化する指示を与えます。ただし、処理系(用語集)はその要求を無視することができますし、どの程度の効力を持つかについても処理系定義です。

register は、ローカル変数の宣言時、あるいは関数の仮引数に使用します。

register int 変数名;
void f(register int n);

register と同じ、記憶域クラス指定子という種類に属する指定子と同時に使うことはできません。具体的には、extern、static、auto、typedef と同時には使えません。

register static int 変数名;  // コンパイルエラー

register指定子を使って宣言された変数や仮引数は、そのメモリアドレスを取得することができません。たとえば、アドレス演算子を適用できませんし、配列からポインタへの変換も起こりません。

int main(void)
{
    register int v = 10;
    register int a[] = { 0, 1, 2 };

    int* pv = &v;  // コンパイルエラー
    int* pa = a;   // コンパイルエラー

    return 0;
}

メモリアドレスが取得できない理由は、register に対する最適化の具体的な方法の1つとして、コンピュータに内蔵されている、レジスタと呼ばれる記憶領域を使うことがあるからです。レジスタはメモリではないので、メモリアドレスという概念自体が存在しません。

実際のところ、register にはそれほど期待が持てません。register を付けても無視されるかもしれないし、付けなくても最適化を施してくれるかもしれません。現代のコンパイラは優秀なので、基本的には任せておいたほうが無難です。

もし register を使うのなら、本当に効果が出ているのか実測したり、マニュアルを読むなどして、どのような効果が期待できるのか確かめたりするべきでしょう。

inline

register がオブジェクトへのアクセスを最適化する指示であるのに対し、inline (inline関数指定子) は関数呼び出しの速度を最適化する指示を与えるものです。

inline は、関数宣言の先頭に付加して使用します。

inline 戻り値の型 関数名(仮引数の並び);

inline が付加された関数を、インライン関数と呼びます。

inline を使うことで、関数の呼び出しを可能な限り高速に行えるように最適化する指示を与えます。ただし、処理系はその要求を無視することができますし、どの程度の効力を持つかについても処理系定義です。

具体的な方法は決まっていませんが、関数定義の内容を、呼び出し元のところに展開する方法が一般的です。この手法は、インライン置換(インライン展開)と呼ばれています。この手法では、関数呼び出しのコストが無くなりますが、呼び出し箇所が多ければプログラムサイズが大きくなる可能性があります。要するに、関数形式マクロが展開される場合と同じような結果を生む訳ですが、マクロがプリプロセスで展開しているのに対し、インライン置換はコンパイル時に行われます。また、あくまでも関数ですので、きちんと型を持った仮引数や戻り値を使えます。

インライン関数の仕様は複雑なので、順番に見ていきましょう。

まず、関数が内部結合の場合は簡単で、関数宣言と関数定義に、inline を付加すれば、インライン関数になります。関数が内部結合であるとは、つまり、static を付けた関数であるということです(第24章)。

#include <stdio.h>
#include <string.h>

inline static int str_eq(const char* s1, const char* s2);

int main(void)
{
    if( str_eq( "abc", "abc" ) ){
        puts( "OK" );
    }

    if( !str_eq( "abc", "ab" ) ){
        puts( "OK" );
    }

    return 0;
}

inline static int str_eq(const char* s1, const char* s2)
{
    return strcmp( s1, s2 ) == 0;
}

実行結果:

OK
OK

一方、関数が外部結合の場合には難しくなります。この難しさの原因は、インライン展開の手法が使われる場合のことをイメージすれば分かります。

インライン関数の中身のコードを、その関数を呼び出している箇所に展開するためには、呼び出し箇所から、関数の中身を知ることができなければなりません。内部結合の関数であれば、この要件は必ず満たせます。

しかし、外部結合ではそうはいかないかもしれません。当サイトが徹底しているような、外部関数の定義をソースファイルに、その宣言をヘッダファイルに書くスタイルを想定すると、たとえば、main.c から sub.c の中身は見えていません。そのため、関数呼び出しが main.c にあり、呼び出される インライン関数が sub.c にあったら、その関数のコードを展開できないのです。

「main.c から sub.c の中身が見えていない」という点が飲み込めない方は、各ソースファイルが別個にコンパイルされ、別個にオブジェクトファイル(たとえば、main.o と sub.o) が生成されることを思い出してください(第24章)。main.c をコンパイルしているときに sub.o の中身は使えませんし、sub.c をコンパイルしているときに main.o の中身は使えません。

たとえば、次のプログラムはコンパイルエラーになります。

// sub.h
#ifndef SUB_H_INCLUDED
#define SUB_H_INCLUDED

inline int str_eq(const char* s1, const char* s2);

#endif
// sub.c

#include "sub.h"
#include <string.h>

inline int str_eq(const char* s1, const char* s2)
{
    return strcmp( s1, s2 ) == 0;
}
// main.c

#include <stdio.h>
#include "sub.h"

int main(void)
{
    if( str_eq( "abc", "abc" ) ){  // コンパイルエラー
        puts( "OK" );
    }

    if( !str_eq( "abc", "ab" ) ){  // コンパイルエラー
        puts( "OK" );
    }

    return 0;
}

main.c は sub.h をインクルードしており、sub.h に str_eq関数の宣言があるので、main.c から宣言はみえています。また、そこに inline が付いているので、インライン関数であることも認識できます。

インライン関数なので、インライン展開をするとすれば、main.c の中で str_eq関数を呼び出している箇所に、str_eq関数の中身のコードを展開できる必要があります。しかし、str_eq関数の定義は sub.c にあるので、main.c からは見えておらず、インライン展開することができません。そのため、これはコンパイルエラーになります。

解決方法としては、ヘッダファイル(.h) に inline付きで関数定義を書き、ソースファイル(.c) に extern inline が付いた関数宣言を書くことです。直感に反して、ヘッダファイル側に"定義"、ソースファイル側に"宣言" であることに注意してください。

// sub.h
#ifndef SUB_H_INCLUDED
#define SUB_H_INCLUDED

#include <string.h>

inline int str_eq(const char* s1, const char* s2)
{
    return strcmp( s1, s2 ) == 0;
}

#endif
// sub.c

#include "sub.h"

extern inline int str_eq(const char* s1, const char* s2);
// main.c

#include <stdio.h>
#include "sub.h"

int main(void)
{
    if( str_eq( "abc", "abc" ) ){
        puts( "OK" );
    }

    if( !str_eq( "abc", "ab" ) ){
        puts( "OK" );
    }

    return 0;
}

実行結果:

OK
OK

1つの翻訳単位(ここではたとえば、stdio.h と sub.h を取り込んだ main.c)の中で、inline付きで extern が付かない関数宣言があるとき(つまり、インライン関数にしたい関数の宣言)、その関数の定義(つまり、関数の内容を記述したもの)をインライン定義と呼びます。サンプルプログラムでは、sub.h にある定義がインライン定義です。

インライン展開を行う際、展開するコードの中身を、インライン定義から持ってくるか、ほかの箇所に記述した関数定義から持ってくるかは未規定です。そのため、sub.h のインライン定義さえあればコンパイルが可能になる実装もあります(Visual Studio はこちら)。インライン定義ではなく、関数定義が必要な実装もあります(clang はこちら)。

どちらを選ぶ処理系でも動作させるには、このサンプルプログラムの sub.c のように、外部定義を記述すると良いです。extern付きで、中身を持たない関数 "宣言" のようにみえるので混乱しますが、これがあることで、外部定義が生成されます。

restrict

restrict修飾子は、ポインタ型に対してのみ適用できます。

Visual Studio 2017 は restrict に対応おらず、拡張機能として __restrict があります。

int* restrict p;
void f(int* restrict p);

restrict を置く位置に注意してください。「restrict int* p」のような記述はコンパイルエラーになります。このような書き方だと、「restrict 付きの int型」へのポインタという意味になってしまいます。

restrict の有無によって、プログラムの意味に変化が起きることはなく、単に最適化のヒントとして使われるだけです。

restrict は C99規格で追加されました。同時に、標準ライブラリ関数のいくつかについて、仮引数に restrict を付加する変更が加えられましたが、これによって関数の挙動が変わっているということはありません。

restrict は、restrict付きポインタが指し示す先のオブジェクトは、この restrict付きポインタを経由する以外の方法では変更されないことを、処理系に教える効果があります。処理系はこの情報を利用して、最適化を施せる可能性があります。

#include <stdio.h>

int main(void)
{
    int n = 10;
    int* restrict pn = &n;  // n の変更は pn経由に限られる

    n = 20;        // 違反(pn 以外の方法で変更)
    *pn = 20;      // OK

    return 0;
}

わざわざ restrict という仕組みがあるのは、どのような方法でオブジェクトの変更があり得るかどうかを、処理系に判断できないことがあるからです。そのため、代わりに、プログラムの内容を熟知しているプログラマーが教えるということです。

ですから、プログラマーは「この restrict付きポインタを経由する以外の方法では変更されない」ことを間違いなく保証しなければなりません。restrict を使っておきながら、この要件を満たしていなかった場合の動作は未定義です。先ほどのサンプルプログラムも未定義の動作です。

この保証を満たさなければならないコード上の範囲は、restrict付きポインタを宣言した位置によって異なります。

#include <stdio.h>

int g = 10;

int main(void)
{
    {
        int* restrict pg = &g;  // このブロック内でのみ効果がある

        g++;      // 未定義の動作
        (*pg)++;  // OK
    }

    g++;  // OK。pg が宣言されたブロック外なので問題ない

    return 0;
}

restrict付きポインタが指し示す先が配列や構造体の場合、その各要素、各メンバも対象になります。そのため「*p」のような直接的なアクセスのほか、「*(p+1)」や「p[1]」のような間接的なアクセスも含みます。

たとえば、次のサンプルプログラムでは、仮引数 p1、p2 が restrict 付きポインタになっており、それぞれをインクリメントしながら、間接参照によってオブジェクトを変更しています。この場合、p1、p2 が指し示す先の配列に重なり合いがあると、restrict の要件に違反していることになります。

#include <stdio.h>

void f(int* restrict p1, int* restrict p2, size_t size)
{
    for( size_t i = 0; i < size; ++i ){
        *p1 = *p2;
        p1++;
        p2++;
    }
}

int main(void)
{
    int a[10] = { 0 };

    f( a, &a[5], 5 );  // OK
    f( a, &a[2], 5 );  // 違反。a[2]~a[4] の各要素が p1, p2 の両方から変更される

    return 0;
}

このように、関数の仮引数で restrict が使われているケースでは、関数の呼び出し元が注意は払わなければなりません。実際に関数の中で行われている処理をみるのではなく(当然、見られないこともありますし)、仮引数に restrict が付いているかどうかで判断します。実引数の指定を誤ると、未定義の動作になってしまいます。

この事例としてより身近なのは、標準ライブラリ関数の memcpy関数memmove関数です。

第34章で説明したように、コピー元の範囲と、コピー先の範囲に重なり合いがあったとき、memcpy関数は未定義の動作になり、memmove関数はきちんと処理されるという違いがあります。memmove関数のほうが安全である反面、memcpy関数のほうが効率で勝る可能性があります。

この差が、それぞれの宣言に現れています。

void *memcpy(void* restrict s1, const void* restrict s2, size_t n);
void *memmove(void* s1, const void* s2, size_t n);

範囲の重なり合いを許さない memcpy関数は、s1 と s2 に restrict を付加することによって、その要件を表現しています。

このように、restrict を使うことによって要件を明確に表示できますが、実際には強制力がありません。要件を満たさない間違ったコードを書いてもコンパイルエラーにならず、単に未定義の動作を引き起こすため、危険性が高い機能でもあります。

コーディングガイドラインによっては、この危険性を嫌い、restrict の使用を禁止しているものもあります(たとえば、ESCR Ver3.0 の R1.3.4 および、MISRA C:2012 R8.14)


参考リンク


更新履歴



前の章へ (第56章 ビットフィールド)

C言語編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
Twitter でツイート Twitter をフォロー LINE で送る
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー