インライン関数の基本
概要
最終更新日:2020/03/02
C++で機能であるインライン関数の基本的な内容、使い方について書いた記事です。
この記事は以下の内容を知りたい方に向けて書いています。
- インライン関数とは何か知りたい
- インラインのメリットとデメリットをを知りたい
- インラインの確認方法が知りたい
- インラインが展開される条件の情報が欲しい
概要
インライン関数とは処理の高速化で使用される手法の一つです。
通常の関数の呼び出しはアドレスに登録されている関数のポインタを使用して
その関数先に移動し、処理を実行しますがインラインでは移動をせずに
呼び出されたソースに関数がコンパイル時に展開されます。
// 書式例:
inline 戻り値の型 関数名(引数)
{
処理内容
}
// 具体例:
inline int AddSum(int a, int b)
{
retrun (a + b);
}
上のコードのように戻り値の前に「inline」を書くことで
インライン関数として扱われます。
通常関数とインライン関数のコンパイル時のソース内容
通常関数はコンパイル時でも通常通りincludeファイルが展開され
objファイルが作成されますが、インライン関数の場合
インライン関数を使用している箇所が関数ではなく、
その関数で定義している内容に置き換えられます。
関数呼び出し部分がインライン関数の内容に置換されることを
インライン展開と呼びます。
// インライン無し
コンパイル前:
Test.h
#ifndef TEST_H_
#define TEST_H_
int AddSum(int a, int b)
{
return (a + b);
}
#endif
Main.cpp
#include "Test.h"
void main(void)
{
int a = AddSum(1, 3);
}
コンパイル後:
Main.cpp
#ifndef TEST_H_
#define TEST_H_
int AddSum(int a, int b)
{
return (a + b);
}
#endif
void main(void)
{
int a = AddSum(1, 3);
}
通常のコードではコンパイル後の関数呼び出しに変化はありません。
次はインラインを使ったコードです。
// インライン有り
コンパイル前:
InlineTest.h
#ifndef INLINE_TEST_H_
#define INLINE_TEST_H_
inline int AddSum(int a, int b)
{
return (a + b);
}
#endif
Main.cpp
#include "InlineTest.h"
void main(void)
{
int a = AddSum(1, 3);
}
コンパイル後:
Main.cpp
#ifndef INLINE_TEST_H_
#define INLINE_TEST_H_
inline int AddSum(int a, int b)
{
return (a + b);
}
#endif
void main(void)
{
int a = (a + b);
}
このように関数のインライン展開されると関数呼び出しに
関数の定義内容がそのまま展開されます。
クラス内でのメンバ関数の定義
C++では以下のようにクラス内で関数を定義することが可能です。
class Test
{
public:
int AddSum(int a, int b)
{
return (a + b);
}
};
このようなクラスの中でメンバ関数を定義した場合
自動的にインライン関数として扱われます。
もちろん、関数の規模が大きい場合は展開はされません。
なので、ゲッターやセッターなどのコード量が少ない関数は
クラス内に記述することが多いです。
インラインの注意
インラインの注意点として、関数の規模が大きい判断された場合、
インライン展開されず、通常の関数呼び出しと同様に扱われます。
なので、インライン関数の処理は小規模にしないといけません。
メリット、デメリット
インラインを使用した場合のメリットとデメリットの一例を以下に書きます。
メリット
インラインのメリットは高速化を見込めるという点です。
関数呼び出しの負荷がなくなり、そのソース内で処理が
展開、実行されるので高速化が見込めます。
インライン関数が活きる例
forやwhileなどのループ処理内での関数呼び出しはそのループ回数分
関数が呼び出されることになります。
なので、その関数呼び出しをインライン関数にした場合、
ループ回数が多ければ多いほど高速化につながります。
#include <iostream>
#include <Windows.h>
#include <stdio.h>
#define USE_INLINE
#ifdef USE_INLINE
inline unsigned int Pow(int x1, int x2)
{
return (x1 * x2);
}
#else
unsigned int Pow(int x1, int x2)
{
return (x1 * x2);
}
#endif
int main()
{
LARGE_INTEGER f;
if (!QueryPerformanceFrequency(&f))
{
return;
}
LARGE_INTEGER s, e;
QueryPerformanceCounter(&s);
unsigned int pow = 0;
for (int i = 0; i < 10000; i++)
{
for (int j = 0; j < 10000; j++)
{
pow = Pow(i, j);
}
}
printf("pow%d\n", pow);
QueryPerformanceCounter(&e);
double t = (double)(e.QuadPart - s.QuadPart) / f.QuadPart;
printf("time = %fsec\n", t);
return 0;
}
実行結果:
インライン展開なし:
0.40806sec
インライン展開有り:
0.24090sec
実行結果から分かるようにインラインを使用した方が処理が早く終わっています。
デメリット
インラインのデメリットは「ファイルサイズの増大」「隠蔽性の低下」
「ビルドファイルの増加」の3つがあります。
ファイルサイズの増大
インライン関数は関数呼び出しではなく、
ソースファイルに関数が展開されることになるので、
インライン関数の呼び出しが多いほどファイルサイズが増大します。
ファイルサイズ増加に伴いビルドにかかる時間も増えてしまいます。
隠蔽性の低下
ヘッダファイルに宣言されているクラスに直接コードを書くことになるので、
コードの隠蔽性が低下します。
ビルドファイルの増加
インライン関数を修正することで、そのヘッダをインクルードしている
全てのcppファイルに対して再度ビルドの必要が発生するので、
頻繁にインライン関数を変更していると、毎回複数のファイルをビルドすることになり、
その分だけ時間がかかります。
インライン展開の確認方法
確認で使用したVisualStudioのバージョンはVisualStudio2019です。
確認の流れ
① |
上段メニュー => デバッグ => プロパティ => 構成プロパティ =>C/C++ => 全般 => デバッグ情報の形式を「Zi」にする |
② |
上段メニュー => デバッグ => プロパティ => 構成プロパティ =>C/C++ => 最適化 => 最適化を「カスタム」にする |
③ |
上段メニュー => デバッグ => プロパティ => 構成プロパティ => C/C++ => 最適化 =>インライン関数の展開を「拡張可能な関数すべて」か「__inlineのみ」にする |
④ |
展開を確認したい関数の呼び出しでブレークポイントを設定して実行する |
④ |
ブレークポイントが止まったら上段メニュー => デバッグ => ウィンドウ => 逆アセンブルをクリック |
まずはインライン展開有りのアセンブラを確認してみます。
// インライン展開有り
inline int Add(int a, int b)
{
return (a + b);
}
int main()
{
int sum = Add(1, 3); // ここにブレークポイント
return 0;
}
インライン展開されているアセンブラ:
int a = AddSum(1, 1);
01391027 mov eax,1
0139102C add eax,3
0139102F mov dword ptr [a],eax <= 展開された処理
次はインライン展開無しのアセンブラを確認します。
// インライン展開有り
int Add(int a, int b)
{
return (a + b);
}
int main()
{
int sum = Add(1, 3); // ここにブレークポイント
return 0;
}
インライン展開されていないアセンブラ:
int a = AddSum(1, 3);
01071037 push 1
01071039 push 3
0107103B call AddSum (0107100Ah) <= 関数呼び出し
展開有りのアセンブラでは直接足し算を行っていますが、
展開無しの方は「call」によって関数呼び出しが行われています。
このようにインライン展開が行われると関数呼び出しによる
オーバーヘッドがなくなるので、高速化につながります。
inline宣言の調査結果
以下は通常の関数やクラスのメンバ変数に対してinline宣言を行った結果です。
まずは、クラスのメンバ関数の検証結果です。
メンバ関数
宣言方法 |
inline宣言の場所 |
展開結果 |
クラス内関数定義(.h) |
関数宣言でinline宣言 |
展開 |
inline宣言無し |
展開 |
クラス外関数宣言(.cpp) |
関数宣言にinline宣言 |
エラー |
関数定義にinline宣言 |
エラー |
宣言、定義にinline宣言 |
エラー |
inline宣言無し |
非展開 |
メンバ関数の検証の結果、宣言と定義を別にした関数で
inline宣言を行った場合、エラーが発生しました。
インライン関数を使用する場合はヘッダファイルにコーディングしましょう。
次は通常関数の検証結果です。
通常関数
宣言方法 |
inline宣言の場所 |
結果 |
宣言 => .h、定義 => .cpp |
関数宣言にinline宣言 |
エラー |
定義にinline宣言 |
エラー |
宣言、定義にinline宣言 |
エラー |
inline宣言無し |
非展開 |
宣言、定義 => .h |
宣言にinline宣言 |
展開 |
定義にinline宣言 |
展開 |
宣言、定義にinline宣言 |
展開 |
inline宣言無し |
非展開 |
宣言、定義 => .cpp |
宣言にinline宣言 |
展開 |
定義にinline宣言 |
展開 |
宣言、定義にinline宣言 |
展開 |
inline宣言無し |
非展開 |
通常関数では、cppとhの宣言を分けた時のみエラーになり、
それ以外ではインライン展開されるという結果になりました。
ただ、cppとhの宣言には例外がありますので紹介したいと思います。
// test.h
inline int Add(int a, int b);
// main.cpp
#include "test.h"
int Add(int a, int b)
{
return (a + b);
}
int main()
{
int sum = Add(1, 3);
return 0;
}
上のコードのように宣言と定義が別でも関数を定義したcppで実行する場合は
問題なくコンパイルは通り、インライン展開もされます。
ただ、定義したcppでしか実行できないのならばhに書く意味がありません。