2015年2月7日土曜日

C言語のinline

C言語のinlineが理解できていなかったので調べてみました.

この記事は個人的なノートであるため間違いが含まれている可能性があります. また,英語の解釈を間違っている部分がある可能性が高いです.

以下の内容はC11の規格に関するものです.古いコンパイラーで試す場合は標準をC11に切り替える必要があります。

何故かリンカーエラー

C言語でも関数にinlineを指定できると聞いて以下のコードを試してみたところリンカエラーになりました.

// 何故かリンカーエラー

inline int add( int l, int r )
{
    return l + r;
}

int main( void )
{
    add( 1, 1 );
}
/tmp/prog-aVTaGb.o: In function `main':
prog.c:(.text+0x19): undefined reference to `add'
clang: error: linker command failed with exit code 1 (use -v to see invocation)

明らかに目の前に関数が存在するにも関わらず,addが未定義だと言っています. しかも,最適化レベルによってエラーになったりならなかったりします.

困った時は規格書を,ということでCの規格書を辞書を片手に調べてみました. 参照した規格はWG14 N1570です.

inlineは関数指定子

inlineは文法上,関数指定子に分類されます.なんとなくstaticやexternと仲間かと思っていましたが,別物のようです. 関数指定子は関数の識別子の宣言の中にのみ現れることができます.

非常に厄介なのは,inlineによって何が起こるかは関数の宣言で識別子のリンケージがどのように指定されたかに依存しているという点です.

インライン関数

まず,どのような場合においても一度でもinlineをつけて宣言された関数はインライン関数になります(6.7.4-6). インライン関数として関数を作ると、その関数への呼び出しは可能な限り高速である(にする?)とコンパイラーに示唆することができます. あくまで示唆であるためコンパイラは無視することも従うこともできます.inline関数の宣言のスコープ範囲内でのみ従うかもしれません. また,インライン関数として宣言された関数は,必ずその翻訳単位で定義されなければなりません.

インライン関数の宣言・定義には以下の3種類があると思います.

  • 内部リンケージを持つ宣言・外部定義
  • 外部リンケージを持つ宣言・外部定義
  • 外部リンケージを持つ宣言・インライン定義

これらは、上から順に以下のような使い方ができると思います。

  • 内部リンケージを持つ関数をinline指定したい(関数マクロのような機能)
  • 外部リンケージを持つ関数を、関数の定義が存在する翻訳単位内でのみinline指定したい(翻訳単位外ではインライン化して欲しくない)
  • 関数を利用するすべての翻訳単位でinline指定される関数を作りたい(インライン化可能な関数を提供するモジュールを作る)

それぞれ順番に考えてみようと思います.

内部リンケージを持つ宣言・外部定義

すべての内部リンケージをもつ関数はインライン関数になることができます(6.7.4-7).しかし,インライン関数とは単に呼び出しが高速である関数という意味しかないため,それ以外に特別なことは何もありません.普通の内部リンケージを持つ関数と同じ扱いです

// 内部リンケージを持つインライン関数の例

inline static int add( int l, int r )
{
    return l + r;
}

static int sub( int l, int r );
int sub( int l, int r );
inline extern int sub( int l, int r )
{
    return l - r;
}

int main( void )
{
    add( 1, 1 );
    sub( 3, 2 );
}

上の例ではaddもsubも内部リンケージを持つ関数です.内部リンケージを持っているため,翻訳単位外から使うことはできません.コンパイラーは関数を削除してmainに埋め込むかもしれませんし,埋め込まないかもしれません.

まとめ

内部リンケージを持つ普通の関数です.インライン関数化することで,コンパイラーがその関数を高速に呼び出す努力をしてくれる可能性があります.

あるモジュールの中だけで頻繁に使う関数はもちろん,関数マクロの代わりに使えるかもしれません. (追記 : 型は固定なので関数マクロという表現は適切ではありませんが、ここでは「呼び出しのオーバーヘッドがない関数をヘッダで提供する」という意味で使用しています) 以下のような関数をヘッダファイルに定義ごと書き,インクルードして使うようにすれば型チェックのかかる安全な関数マクロのようなものになる可能性があります.(内部リンケージを持つ定義なので複数の翻訳単位に現れていいと思うのですが,規格でどのように説明されているかよくわかりませんでした)

// inline_util.h 関数マクロの代わりになるインライン関数
inline static int inline_fast_add( int l, int r )
{
    return __ultra_fast_add( l, r );
}

外部リンケージをもつ宣言・外部定義

inline指定された宣言に加えてinline指定なしのstaticが指定されていないファイルスコープの宣言がひとつでもあるか,inlineとexternの両方が指定されたファイルスコープの宣言がひとつでもある場合,関数の定義は外部リンケージを持つインライン関数の外部定義となります(6.7.4-7). この場合も,インライン関数であるということを除けば単に外部リンケージを持つ関数と同じ扱いです.

// 外部リンケージを持つインライン関数の例

inline extern int add( int l, int r )
{
    return l + r;
}

int sub( int l, int r );
inline int sub( int l, int r )
{
    return l - r;
}

int main( void )
{
    add( 1, 1 );
    sub( 3, 2 );
}

条件を文章にすると非常にわかりにくくなってしまいますが,一般的によく行うプロトタイプ宣言を関数定義とは別に行う場合を考えると,上のコードのsubのように定義にだけinlineを指定した状態などが当てはまります.

add,subのいずれも単に外部リンケージを持つ関数であるので他の翻訳単位から呼び出すこともできます.

まとめ

外部リンケージを持つ普通の関数です.インライン関数化することで,同じ翻訳単位ではコンパイラーがその関数を高速に呼び出す努力をしてくれる可能性があります.

外部に関数の定義を提供する必要があるが,モジュール内では可能な限り高速に実行して欲しい場合に使えるかもしれません. その場合,ヘッダーのプロトタイプ宣言は変更せず,モジュール内の定義のみinline指定します.

外部リンケージを持つ宣言・インライン定義

一番やっかいなのがインライン定義(inline definition)です(6.7.4-7).まず重要な点として,インライン定義は文法上外部定義ではありません(外部宣言ではあります)(6.9-5). 外部定義ではないので他の翻訳単位に定義を提供せず,他の翻訳単位での同名の関数の外部リンケージを持つ外部定義を禁止しません.

また,インライン定義は外部リンケージを持つ同名の関数の識別子の宣言でもあるという点も重要だと思います.

インライン定義になる条件は単純です.その翻訳単位上のすべてのファイルスコープを持つ宣言がinlineを含みexternを含まない場合,その関数のその翻訳単位での定義はインライン定義となります. また,その関数の識別子が式中で使われるのであればプログラムのどこかに必ずただひとつの外部定義が存在しなければなりません(6.9-5). (ただし,同じ翻訳単位に同じ識別子の複数のインライン定義が存在してはならないはずですが,規格のどこを読めばそう判断できるかはよくわかりませんでした)

これが一番最初に出てきたリンカーエラーとなったプログラムと同じ状況です.

// 外部リンケージを持つ外部宣言・インライン定義の例

inline int add( int l, int r )
{
    return l + r;
}

inline int sub( int l, int r )
{
    return l - r;
}

int main( void )
{
    int sub( int l, int r );
    
    add( 1, 1 );
    sub( 3, 2 );
}

明らかに目の前に関数の定義が存在しますが,存在するのはあくまでインライン定義です.従って,コンパイラーの最適化レベルによっては式中のaddとsubは外部定義を参照してしまい,リンカーエラーが発生します.

インライン定義の目的は,他の2つの定義とは若干異なるように感じます. インライン定義は,他のどこかに存在する外部定義の代替を提供するからです. 例えば,ライブラリなどで提供されるある関数の外部定義が存在するが,その関数の性能が良くない,またはコンパイラーの最適化によって(関数の呼び出しをなくすという意味での)インライン化を行いたい場合などに,インライン定義によって代替を用意することにより性能を向上させたりすることができます.

あまり適切な例ではないと思うのですが,このように外部の関数の代替を用意することができます.

// main.c
#include <stdio.h>

inline void func( void )
{
    puts( "inline" );
}

int main( void )
{
    func();
}
// func.c
#include <stdio.h>

void func( void )
{
    puts( "extern" );
}

関数funcのインライン定義をmain.cで提供し,外部定義をfunc.cで提供しています. 実験すると,最適化レベルによってmain.cのfuncが呼ばれたりfunc.cのfuncが呼ばれたりします.

このとき注意しなければならないのは,インライン定義の実行結果は外部定義の関数の実行結果と全く同じでなければなりません. さらに,インライン定義には静的記憶域期間を持つオブジェクトの定義を含んではいけないし,内部リンケージを持つ識別子への参照も含んではいけません(6.7.4-3). プログラマにはコンパイラがどちらの定義を用いるかわからないからです. 従って,上の例は規格に適合したプログラムではありません.

これで一番最初のプログラムがなぜリンカーエラーになったかがわかった気がします. 最初の例ではインライン定義になってしまうため,他の翻訳単位に外部定義が存在しなければならず,コンパイラーが外部定義を参照しリンカーエラーになったのだと思います.

ちなみに,最初の例に最適化(O2など)をかけるとコンパイルが通る場合があります. おそらく,コンパイラは外部定義でなくインライン定義を参照するようになり,リンカーの立場からすれば未解決の参照がないため問題なくリンクが完了するのだと思います. しかし,この状況では必ず外部定義が存在しなければならないので規格適合のプログラムではありません(6.9-5). 現行のコンパイラーでは警告してくれないようなので気をつける必要があります.

まとめ

インライン定義という特殊な関数です.他の翻訳単位に外部定義が存在する場合にのみ定義することができます.

外部定義された関数の高速な代替を作りたい場合に使えるかもしれません.ただし,ヘッダーからinline指定なしのプロトタイプ宣言を読み込んでしまうとインライン定義ではなく外部定義になってしまい,多重定義になってしまいます. 非常に扱いが難しい関数だと思います.

モジュールのヘッダー内のプロトタイプ宣言をインライン定義にしてしまうという使い方もありそうです。 モジュール外部に公開する必要があるが、可能な限り高速にしてほしい関数がある場合にインライン定義を使用すれば、その関数を使うそれぞれの翻訳単位でコンパイラが最適化してくれる可能性があります。コンパイラは外部定義を使っても良いしインライン定義を使っても良いので最適化の幅が広がりそうです。 ただし、モジュール内に外部定義を生成する必要があるので、モジュール内に宣言のみを書いて外部定義を生成させるという不思議な状況になります。

この方法が本来のインライン定義の使い方なのかもしれません。

// module.h
#include <stdio.h>

// インライン関数の外部定義またはインライン定義
inline void module_inline( void )
{
    puts( "inline" );
}

// 普通の関数のプロトタイプ宣言
void module_func( void );
// module.c
#include "module.h"

// インライン関数の外部定義を生成するための宣言
inline extern void module_inline( void );

// 普通の関数
void module_func( void )
{
    puts( "func" );
}
// main.c
#include "module.h"

int main( void )
{
    // モジュールの使用
    module_inline();
    module_func();
}

module.cでわざわざinline指定しているのは、インライン定義を使用していることがわかりやすくなるかなと思ったからです。

実際にコンパイルしてみると、main内でmodule_inlineだけがインライン化されることが確認できました。

# main.s
    movl    $.L.str, %edi
    callq   puts
    callq   module_func
    xorl    %eax, %eax
    popq    %rdx
    ret

C++のインライン関数

C++の規格はほとんど理解していないのですが,言語リンケージなどでinlineを含むCのプログラムを取り込むと何が起こるのか知るために少し調べてみました.規格を直接読むのは大変なので,日本語でC++11を解説してくださっているサイトを参考にしました.

http://ezoeryou.github.io/cpp-book/C++11-Syntax-and-Feature.xhtml#dcl.fct.spec.inline

私の理解では,C++のinline指定はリンケージの影響は全く受けないが,inline指定された宣言が現れる翻訳単位には定義がなければならないという,Cのインライン関数に似た制限があるみたいです.

インライン定義が存在しないインライン関数という感じでしょうか.

少し異なるところは,C++では外部リンケージを持つ外部定義であっても,まったく同じ文であれば異なる翻訳単位に複数存在できるというところだと思います. Cの外部リンケージを持つインライン関数の外部定義は,どこかの翻訳単位に一つしか存在できません.しかし,C++では許されます.

モジュール内(.cファイルなど)の定義にはinlineが指定されているが、ヘッダーファイル内のプロトタイプ宣言にはinline指定されない関数がヘッダーファイルを通してC++にC言語リンケージで取り込まれた場合,単に外部リンケージを持つ識別子の宣言となり問題なくC側のインライン関数にリンクできると思います.

staticとinlineが指定され,定義された関数が含まれるヘッダーをC言語リンケージでC++に取り込んでも,単純にC言語リンケージと内部リンケージを持つインライン関数が定義されるので問題なくC++側で使用できそうです.

インライン定義を含むヘッダーがC++にC言語リンケージで取り込まれた場合は少しむずかしそうです。 C++では外部定義が生成されてしまいます。従って多重定義になるはずですが、ヘッダーで提供されたインライン定義ということは必ず同じ文字並びであるのでC++では合法になりそうです。

インライン関数のまとめ

インライン関数を調べるために規格を追いかけてみましたが,とても難しかったです. 一つのことを確かめるために規格の様々な場所を参照する必要があったり,表現が独特だったり引っかかるところが多いです.

一番使い勝手が良いのはやっぱり内部リンケージを持つインライン関数ではないでしょうか. 安全な関数マクロにもなるし,組み込み用途でも移植性のある記述ができそうです.

インライン関数はあくまでコンパイラーへの最適化のヒントでしかないので,最適化レベルやコンパイラによってインライン化されたりされなかったりします. 確実なインライン化が必要な場合,コンパイラーオプションの調整やアセンブリの確認が必要だと思います.