04 August 2012

インライン関数についてややこしいところをまとめておきます。 結論から言うと、常に static inline を使え、ってことになります。

結果だけ知りたい人は以下は読む必要なし。

最初に

inline キーワードは関数callを高速化せよ、という指定であって、inline を付けたからといって、本当にインライン展開されるかはわかりません。

実際のところ、最適化オプションをつけていない場合、inline を付けても、一切インライン展開してくれません。 最適化オプションによらず、インライン展開を強制させるには __attribute__((always_inline)) をつけるというやり方があります。

-O1 オプションをつけた場合、明示的に inline がついている場合はなるべくインライン展開しようとするが、inline指定がないものについては インライン展開しないようだ。

-O2 オプションをつけた場合、inline指定がなくても、インライン展開をする場合がある。static 関数や、非常に短い関数など。

続いて重要なのは、インライン展開された結果、展開された関数の定義コードが残るのか、消えるのかという点について。 ここを間違えると、リンク時に関数の多重定義でエラーになったり、逆に関数が未定義でエラーになったりします。

これについては gcc のマニュアルに説明があるので、ほとんどここに書いてあるとおりなのですが。

-std= の指定によって挙動が違ってくる部分もあるのだが、まず全体通して共通している部分から。

ケース1

static inline int inc(int *a)
{
  return (*a)++;
}

のように static 関数に inline がついた場合。 この場合、(static inline の方が static よりもスピード重視であることを除き)基本的に inline を付けなかった時と同じ扱いになる。

どういうことかと言うと、すべての関数 call がインライン展開されたときは関数定義は残らない。 インライン展開されないcall があったり、関数のアドレス参照があったりすると関数の定義は残ります。

ちなみに、以下のように inline をつけずに、__attribute__((always_inline))をつけ、最適化オプションなしでコンパイルした場合、インライン展開はされたが、関数定義は残った。普通はこういう使い方はしないだろうが。。

static int __attribute__((always_inline)) inc(int *a)
{
  return (*a)++;
}

ケース2

extern int inc(int *a);

inline int inc(int *a)
{
  return (*a)++;
}

のように、 inline なしで宣言された後、 inline 付きで定義された場合。 (この例では、宣言の直後に定義を書いているが、普通は宣言の部分はヘッダーに書くはずです。) この時も inline がなかった時と同じような挙動になる。 すべての関数callがインライン展開されたとしても、必ず関数定義は残る。

ここからは -std= の指定によって挙動が異なる。

ケース3

inline int inc(int *a)
{
  return (*a)++;
}

のように宣言はなくて、 inline 付きで定義された場合。 もしくは、宣言と定義ともに inline が付いている時。

-std=gnu90 (gnu89) または -fgnu89-inline を指定しているときは すべての関数callがインライン展開されたとしても、他のファイルからの関数callに備えて、関数定義は残ります。

一方、-std=gnu99 または -std=c99 が指定された時は、全く逆で、この関数定義はインライン展開用にしか使われません。そのため、inline 指定の関数定義は必ず消える。 最適化オプションをつけていないときはインライン展開されていないにも関わらず、関数定義は消えてしまう。

ちなみに、-std=c90 のときは inline キーワードを認識しません。

これを書いている時点 (gcc version 4.7) では -std=gnu90 がデフォルトです。 将来的には -std=gnu99 がデフォルトになるそうですが。

ケース4

extern inline int inc(int *a)
{
  return (*a)++;
}

のように externinline が同時に指定された場合。

-std=gnu90 (gnu89) または -fgnu89-inline の時は、この関数定義はインライン展開のみに使われ、関数定義は残らない。

-std=gnu99 または -std=c99 の時は、関数定義が必ず残る。

まとめ

デフォルト (-std=gnu90)では、以下のような挙動になる。

  1. static inline

    すべての関数call がインライン展開されると関数定義が消える

  2. inline なしの宣言 + inline 付きの定義

    関数定義は必ず残る

  3. inline

    関数定義は必ず残る

  4. extern inline

    関数定義は必ず消える。

ただし、 -std=gnu99 または -std=c99 オプションをつけると 3 と 4 の扱いが逆転します。

以上のことからわかるように、ヘッダーファイルに書いてマクロ関数の代わりにできるのは 1 と 4 です。 2 と 3 はヘッダーに書いてしまうと、(そのヘッダーが複数のファイルからインクルードされていると)multiple definition でエラーになります。

1 をヘッダーに記述し、複数のソースからインクルードさせる場合、 -O1 以上の最適化オプションか、__attribute__((always_inline)) が必要。 そうしないと、やはり関数の multiple definition でリンクエラーになる。

4 の使い方としてはヘッダーファイルにインライン展開用のコードを書いておき、一つのCソースに、インライン展開されなかった時用の定義を(extern inline を外して)書いておく。 ただし、記述が重複する上に、 -std= オプションによって挙動が逆転するので使いにくいです。

以上のことから、 1 が最も使い勝手がよく、インライン関数は static inline を使え、ということになります。

Linux Kernelのコードをみてもほとんど static inline になっています。 たまに extern inline がありますが。。。



blog comments powered by Disqus