2021年4月25日日曜日

20: C++ 'inline': 変更が無視される等の信頼できない振る舞い

<このシリーズの前の記事 | このシリーズの目次 | このシリーズの次の記事>

C++ 'inline'は、忌まわしいが、その理由は、それが、本当にインラインすることを保証しないことではなく、インライン化しているふりすらもしないことだ。

話題


About: C++

この記事の目次


開始コンテキスト



ターゲットコンテキスト



  • 読者は、'inline'が何をするかもしれないか、およびその帰結を知る。

オリエンテーション


C++ 'template'とは本当には何であるのか、およびその使い方に取り組んだ記事(インライン化を含む)があります。

C++におけるヘッダーファイルの意図と使用法を詳説した記事があります。


本体

ト書き
Hypothesizer 7は、独白する。


1: 忌まわしき'inline': 変更が無視される例


Hypothesizer 7
以下のような、ある'inline'ファンクションを持ったあるプロジェクトを私は持っている。

theBiasPlanet/coreUtilitiesTests/inlineTest1/ClassA.hpp

@C++ ソースコード
#ifndef __theBiasPlanet_coreUtilitiesTests_inlineTest1_ClassA_hpp__
#define __theBiasPlanet_coreUtilitiesTests_inlineTest1_ClassA_hpp__

namespace theBiasPlanet {
	namespace coreUtilitiesTests {
		namespace inlineTest1 {
			class ClassA {
				private:
					int i_memberVariableA0;
				public:
				    inline ClassA ();
					inline int inlineMethodA0 ();
			};
		}
	}
}

#endif

theBiasPlanet/coreUtilitiesTests/inlineTest1/ClassA.ipp

@C++ ソースコード
#include "theBiasPlanet/coreUtilitiesTests/inlineTest1/ClassA.hpp"
#include <iostream>

using namespace ::std;

namespace theBiasPlanet {
	namespace coreUtilitiesTests {
		namespace inlineTest1 {
			inline ClassA::ClassA () {
			}
			
			inline int ClassA::inlineMethodA0 () {
				cout << "### ClassA::inlineMethodA0 version: 1" << endl << flush;
				return i_memberVariableA0;
			}
		}
	}
}

注目すべきだが、それは'.ipp'になっているが、その理由は、そのファイルはコンパイルされるべきソースファイルではなく、インクルードされるべきファイルであることだ(ただし、「.ipp」の中の「i」は、'inline'を指すものであり、'included'をではない)。

その'inline'ファンクションをコールする新たなソースファイルを作成しよう、しかし、その前に、私はその'inline'ファンクションの実装を変更する。

theBiasPlanet/coreUtilitiesTests/inlineTest1/ClassA.ipp

@C++ ソースコード
			~
			inline int ClassA::inlineMethodA0 () {
				cout << "### ClassA::inlineMethodA0 version: 2" << endl << flush;
				return i_memberVariableA0;
			}
			~

theBiasPlanet/coreUtilitiesTests/inlineTest1/ClassC.hpp

@C++ ソースコード
#ifndef __theBiasPlanet_coreUtilitiesTests_inlineTest1_ClassC_hpp__
#define __theBiasPlanet_coreUtilitiesTests_inlineTest1_ClassC_hpp__

namespace theBiasPlanet {
	namespace coreUtilitiesTests {
		namespace inlineTest1 {
			class ClassC {
				private:
					int i_memberVariableC0;
				public:
				    ClassC ();
					int methodC0 ();
			};
		}
	}
}

#endif

theBiasPlanet/coreUtilitiesTests/inlineTest1/ClassC.cpp

@C++ ソースコード
#include "theBiasPlanet/coreUtilitiesTests/inlineTest1/ClassC.hpp"
#include <iostream>
#include "theBiasPlanet/coreUtilitiesTests/inlineTest1/ClassA.ipp"

using namespace ::std;

namespace theBiasPlanet {
	namespace coreUtilitiesTests {
		namespace inlineTest1 {
			ClassC::ClassC () {
			}
			
			int ClassC::methodC0 () {
				ClassA l_classA;
				return l_classA.inlineMethodA0 ();
			}
		}
	}
}

私は、その新たなソースファイル(のみ)をコンパイルし、リンクをしてプログラムを作り、そのプログラムを実行するが、そのファンクションコールは、古い実装を実行する、「ClassA::inlineMethodA0 version: 1」を表示して。


2: 'inline'が行なうと想定されていること


Hypothesizer 7
'inline'は、ファンクションコールをファンクションの本体で置換する(それを、私は、'本当にインライン化する'と呼ぶ)ものと想定されている。

本当に'inline'なファンクションは、青写真であって、オブジェクトではない(それが意味するのは、いかなるオブジェクトファイル中にもそれは何らのエンティティとしても存在しないということだ)。


3: 'inline'は、多分、それが行なうと想定されていることを行なわないが、それが問題はなのではない


Hypothesizer 7
'inline'は、単に、それが行なうと想定されていることを行なうことを保証しないだけではなく、多分、それはそれが行なうと想定されていることを行なわないであろう。

実際、私のシンプルなテストケースの全てにおいて、私のGCCおよびVisual C++は、決して本当にはインライン化を行なわなかった。

それ自体は、特に悪いことではない、もしも、それが、単に最適化の問題であるとして片付けられるものであるならば。

実際、もしも、コンパイラーが、本当にはインライン化しないほうがパフォーマンス的に良いと判断するのであれば、それについて不平を言うつもりは私には全くない。

しかし、そのような最適化は、プログラマーに対して出しゃばらないように行なわれるべきだと私は主張する。

それはどういう意味なのか?えーと、プログラムの振る舞いは、単にコンパイラーがある最適化の判断をしたからという理由によって変わるべきではないということだ。

言い換えると、当該ファンクションは、少なくとも外見えには、それが'inline'であるように見えるべきだ、たとえ、本当にはそうでないとしても。

それが、私がいかなる最適化からも要求することだ。

それは過剰な要求だろうか?私はそうは思わない。


4: 'inline'が多分、行なうこと: ドッペルゲンガーを作成する


Hypothesizer 7
あるソースファイル内でのある'inline'ファンクションのコール群は、多分、当該オブジェクトファイル内に1つのドッペルゲンガーを作成する。

それはどういう意味なのか?もしも、そのファンクションが本当にはインライン化されないならば、それらのコール群はファンクションコールのままでいなければならない。 . . . 何へのコールなのか?勿論、あるファンクションオブジェクトへである。しかし、そのファンクションオブジェクトはどこにあるのか? . . . 当該ファンクションの実装はそれらファンクションコール群のソースファイルへインクルードされる('.ipp'ファイルのインクルードを介して)ので、コンパイラーは、そのソースファイルのオブジェクトファイル内に1つのファンクションオブジェクトを作成するだろう。 . . . しかし、別のソースファイルが同一の'inline'ファンクションをコールしたらどうなるのか? . . . コンパイラーは、そのオブジェクトファイル内に、同一のファンクションのもう一つのファンクションオブジェクトを作成するだろう。 . . . したがって、同一ファンクションの複数のファンクションオブジェクト(ドッペルゲンガーたち)が、複数のオブジェクトファイル内に散らばらせられることになる。

'inline'は、コンパイラーに無視されるかもしれないただのヒントだと思っている方々が一部におられるが、そうではない: たとえ、ファンクションコール群が本当にはインライン化されないとしても、'inline'は、ファンクションオブジェクト群をドッペルゲンガーにする効果を持つ(もしも、そうしたファンクションオブジェクト群がドッペルゲンガーにならなければ、重複オブジェクト・リンクエラーが起こるだろう)。


5: コンパイラーの決定がプログラマーに対して出しゃばっており、それが問題なのだ


問題は、そうしたドッペルゲンガーたちのそれぞれがオブジェクトファイルローカルにならないということだ。

はあ?そうした複数のドッペルゲンガーたちがグローバルに見えるのか?はい、見えます。

ファンクションコールは、ローカルのドッペルゲンガーをコールするように保証されていないのか?されていません。

それでは、各コールに対して、どのドッペルゲンガーがコールされるのか? . . . 競合するオブジェクトファイル群の内、リンク順序で最初にあるオブジェクトファイル内にあるものです。

問題は、コンパイラーによる、'inline'ファンクションが本当にインライン化されるか否かの決定が、差し出がましく、プログラムの振る舞いを変更することだ: もしも、ファンクションが本当にインライン化されれば、ソースファイルのコンパイル時の実装が使われるが、そうでなければ、別の実装が使われるかもしれない。

私が主張するのは、もしも、あるファンクションが'inline'であると宣言されているのであれば、それは、少なくとも、'inline'であるかのように振る舞わなければならないということであるが、現在の振る舞いはそうなっていない。


6: なぜ、そしてどのように、上記例は信頼できない振る舞いを示すのか


Hypothesizer 7
今や、なぜ上記例の'inline'ファンクションの変更が、新たなファンクションコールで無視されるのかを私は理解した。

第1に、明らかに、当該ファンクションコールは本当にはインライン化されていない。

第2に、当該の変更は確かに新たなドッペルゲンガーに反映されているが、そのドッペルゲンガーは、リンク順序が優先する競合オブジェクトファイルが存在するが故に使用されていない。

もしも、新たなオブジェクトファイルが、競合オブジェクト群の中でリンク順序が先頭になるように移動されれば、当該ファンクションへの全てのコールが新たなドッペルゲンガーへ向けられるだろう。

または、あるオブジェクトファイルがプロジェクトから取り除かれたら、当該ファンクションへの全てのコールが、思いがけず、振る舞いを変えるかもしれない。

または、もしも、コンパイラーが密かに考えを変えて本当にインライン化し始めたら、振る舞いは突然変わるだろう。


7: GCCに本当にインライン化するよう強制する


Hypothesizer 7
実は、GCCには、本当にインライン化するように強制することができる、以下のように。

theBiasPlanet/coreUtilitiesTests/inlineTest1/ClassA.hpp

@C++ ソースコード
					~
					inline int inlineMethodA0 () __attribute__((always_inline));
					~

上記例コードをそのように私は変更したので、プログラムは振る舞いを変え、当該ファンクションが本当にインライン化されたように見えるようになった(実のところ、それは本当にインライン化されたのだ)。


参考資料


<このシリーズの前の記事 | このシリーズの目次 | このシリーズの次の記事>