私はC/C++開発者ですが、いつも困惑する質問がいくつかあります。
ありがとう
- 「通常の」コードとインラインコードの間に大きな違いはありますか?
はいといいえ。いいえ、インライン関数またはメソッドは通常のものとまったく同じ特性を持っているため、最も重要なのは、両方ともタイプセーフであるということです。はい、コンパイラによって生成されるアセンブリコードが異なるためです。通常の関数では、各呼び出しはいくつかのステップに変換されます。スタックへのパラメーターのプッシュ、関数へのジャンプ、パラメーターのポップなどですが、インライン関数の呼び出しは、次のような実際のコードに置き換えられます。大きい。
- インラインコードは単にマクロの「形式」ですか?
いいえ!マクロは単純なテキスト置換であり、重大なエラーにつながる可能性があります。次のコードについて考えてみます。
#define unsafe(i) ( (i) >= 0 ? (i) : -(i) )
[...]
unsafe(x++); // x is incremented twice!
unsafe(f()); // f() is called twice!
[...]
インライン関数を使用すると、関数が実際に実行される前にパラメーターが評価されることが確実になります。また、型チェックが行われ、最終的には仮パラメータの型に一致するように変換されます。
- コードをインライン化することを選択する場合、どのようなトレードオフを行う必要がありますか?
通常、インライン関数を使用するとプログラムの実行が速くなりますが、バイナリコードが大きくなります。詳細については、 GoTW# をお読みください。
以前の回答で示唆されているように、inline
キーワードを使用すると、関数呼び出しをインライン化することでコードを高速化できますが、多くの場合、実行可能ファイルが増加します。 「関数呼び出しのインライン化」とは、引数を適切に入力した後、ターゲット関数への呼び出しを関数の実際のコードに置き換えることを意味します。
ただし、最近のコンパイラは、高度な最適化に設定されている場合、ユーザーからのプロンプトなしで関数呼び出しを自動的にインライン化するのに非常に優れています。実際、コンパイラは通常、人間よりも速度向上のためにインラインを呼び出すものを決定するのに優れています。
パフォーマンス向上のために関数inline
を明示的に宣言することは、(ほとんど?)常に不要です!
さらに、コンパイラーは、inline
要求が適切である場合、および無視することができます。コンパイラーは、関数の呼び出しをインライン化できない場合(つまり、重要な再帰または関数ポインターを使用する場合)だけでなく、関数が大きすぎて意味のあるパフォーマンスを向上できない場合にもこれを行います。
ただし、inline
キーワードを使用してインライン関数を宣言する 他の効果があります 、実際には1つの定義規則を満たすために必要な場合があります( ODR):C++標準のこのルールでは、特定のシンボルは複数回宣言できますが、定義できるのは1回だけであると規定されています。リンクエディタ(=リンカ)が複数の同一のシンボル定義を検出すると、エラーが生成されます。
この問題の1つの解決策は、コンパイルユニットがstatic
を宣言して内部リンケージを与えることにより、特定のシンボルをエクスポートしないようにすることです。
ただし、代わりに関数inline
をマークする方がよい場合がよくあります。これにより、リンカは、コンパイルユニット全体でこの関数のすべての定義を1つの定義、1つのアドレス、および共有関数静的変数にマージするように指示されます。
例として、次のプログラムについて考えてみます。
// header.hpp
#ifndef HEADER_HPP
#define HEADER_HPP
#include <cmath>
#include <numeric>
#include <vector>
using vec = std::vector<double>;
/*inline*/ double mean(vec const& sample) {
return std::accumulate(begin(sample), end(sample), 0.0) / sample.size();
}
#endif // !defined(HEADER_HPP)
// test.cpp
#include "header.hpp"
#include <iostream>
#include <iomanip>
void print_mean(vec const& sample) {
std::cout << "Sample with x̂ = " << mean(sample) << '\n';
}
// main.cpp
#include "header.hpp"
void print_mean(vec const&); // Forward declaration.
int main() {
vec x{4, 3, 5, 4, 5, 5, 6, 3, 8, 6, 8, 3, 1, 7};
print_mean(x);
}
両方の.cpp
ファイルにはヘッダーファイルが含まれるため、mean
の関数定義が含まれます。ファイルは二重包含に対するインクルードガードを使用して保存されますが、これにより、コンパイル単位が異なっていても、同じ関数の2つの定義が作成されます。
ここで、これら2つのコンパイルユニットをリンクしようとすると、たとえば次のコマンドを使用します。
⟩⟩⟩ g++ -std=c++11 -pedantic main.cpp test.cpp
「duplicatesymbol__Z4meanRKNSt3__16vectorIdNS_9allocatorIdEEEE」(関数mean
の- マングル名 )というエラーが表示されます。
ただし、関数定義の前にあるinline
修飾子のコメントを外すと、コードは正しくコンパイルおよびリンクされます。
関数テンプレートは特殊なケースです。そのように宣言されているかどうかに関係なく、常にインラインです。これは、コンパイラがそれらにインライン呼び出しを行うことを意味するものではありませんが、ODRに違反することはありません。クラスまたは構造体内で定義されているメンバー関数についても同じことが言えます。
インラインコードは本質的にマクロのように機能しますが、最適化できる実際の実際のコードです。関数呼び出しの設定(パラメーターを適切なレジスターにロードする)に必要な作業は、メソッドが実行する実際の作業量が少ない場合に比べてコストがかかるため、非常に小さい関数はインライン化に適していることがよくあります。インライン化を使用すると、コードはそれを使用するすべてのメソッドに直接「貼り付け」られるため、関数呼び出しを設定する必要はありません。
インライン化によりコードサイズが大きくなり、これが主な欠点です。コードが大きすぎてCPUキャッシュに収まらない場合は、大幅な速度低下が発生する可能性があります。コードを増やすと問題が発生する可能性が非常に高い場所でメソッドを使用している可能性が低いため、これについて心配する必要があるのはまれです。
要約すると、インライン化は、何度も呼び出されるがあまり多くの場所では呼び出されない小さなメソッドを高速化するのに理想的です(ただし、100の場所でも問題ありません。重大なコードの膨張を取得するには、非常に極端な例に入る必要があります)。
編集:他の人が指摘しているように、インライン化はコンパイラへの提案にすぎません。巨大な25行のメソッドをインライン化するような愚かな要求をしていると思われる場合は、自由に無視できます。
はい-インラインコードには、関数呼び出しやレジスタ変数のスタックへの保存は含まれていません。 '呼び出されるたびにプログラムスペースを使用します。したがって、プロセッサに分岐や状態の保存、キャッシュのクリアなどがないため、全体として実行にかかる時間が短縮されます。
マクロとインラインコードは類似点を共有しています。大きな違いは、インラインコードが関数として特別にフォーマットされているため、コンパイラと将来のメンテナがより多くのオプションを利用できることです。具体的には、コードスペースを最適化するようコンパイラーに指示した場合、または将来のメンテナがコード内の多くの場所で拡張して使用することになった場合、簡単に関数に変換できます。
コードをインライン化することを選択する場合、どのようなトレードオフを行う必要がありますか?
レジスタの保存と関数へのジャンプはコードスペースを占有するため、非常に小さな関数の場合、インラインcanは関数よりもスペースを占有しないことに注意してください。 。
-アダム
それはコンパイラに依存します...
ダムコンパイラがあるとしましょう。関数をインライン化する必要があることを示すことにより、関数が呼び出されるたびに、関数のコンテンツのコピーが配置されます。
利点:関数呼び出しのオーバーヘッドがありません(パラメーターの入力、現在のPCのプッシュ、関数へのジャンプなど)。たとえば、大きなループの中央部分で重要になる可能性があります。
不便:生成されたバイナリを膨らませます。
マクロですか?コンパイラはまだパラメータのタイプなどをチェックするため、実際にはそうではありません。
スマートコンパイラはどうですか?関数が複雑すぎる/大きすぎると「感じる」場合は、インラインディレクティブを無視できます。そしておそらく、単純なゲッター/セッターのようないくつかの些細な関数を自動的にインライン化することができます。
インラインは、コンパイラーへのヒントであり(コンパイラーはコードをインライン化しないことを決定する場合があります!)、マクロはコンパイル前のソースコードテキスト生成であるため、インライン化が「強制」されるという点でマクロとは異なります。
関数をインラインでマークするということは、コンパイラーが オプション コンパイラがそうすることを選択した場合、それが呼び出される「インライン」に含めること。対照的に、マクロは 常に その場で拡張されます。インライン関数には適切なデバッグシンボルが設定されており、マクロのデバッグが混乱している間、シンボリックデバッガーがソースのソースを追跡できるようになっています。インライン関数は有効な関数である必要がありますが、マクロは...そうではありません。
関数をインラインで宣言することを決定することは、主にスペースのトレードオフです-コンパイラがそれをインライン化することを決定した場合(特に静的でもない場合、プログラムはより大きくなります。その場合、少なくとも1つのインライン化されていないコピーが外部オブジェクト);実際、関数が大きい場合、キャッシュに収まるコードが少なくなるため、パフォーマンスが低下する可能性があります。ただし、一般的なパフォーマンスの向上は、関数呼び出し自体のオーバーヘッドを取り除くことだけです。内部ループの一部として呼び出される小さな関数の場合、これは理にかなっているトレードオフです。
コンパイラを信頼する場合は、内部ループで使用される小さな関数をinline
自由にマークしてください。コンパイラは、インライン化するかどうかを決定する際に正しいことを行う責任があります。
インライン化は、速度を上げるための手法です。ただし、プロファイラーを使用して、状況でこれをテストしてください。私は(MSVC)インライン化が常に提供されるとは限らず、確かに見事な方法ではないことを発見しました。ランタイムは時々数パーセント減少しましたが、わずかに異なる状況では数パーセント増加しました。
コードの実行が遅い場合は、プロファイラーを取り出して問題点を見つけ、それらに取り組みます。
ヘッダーファイルへのインライン関数の追加を停止しました。結合は増加しますが、見返りはほとんどありません。
インライン化すべきかどうかの答えはスピードに帰着します。関数を呼び出すタイトなループにあり、それが超巨大な関数ではないが、関数の呼び出しに多くの時間が浪費されている場合は、その関数をインラインにすると、多くの効果が得られます。あなたのお金。
まず、インラインは、関数をインライン化するためのコンパイラーへの要求です。したがって、インライン化するかどうかはコンパイラー次第です。
F.e.でコードをインラインとしてマークしている場合C++では、コードをインラインで実行する必要があることもコンパイラに指示しています。そのコードブロックは、呼び出された場所に「多かれ少なかれ」挿入されます(したがって、スタックでのプッシュ、ポップ、ジャンプが削除されます)。したがって、はい...関数がそのような動作に適しているかどうかをお勧めします。
「インライン」は、2000年代の「レジスタ」に相当します。気にしないでください。コンパイラーは、最適化するものを決定する上で、あなたよりも優れた仕事をすることができます。
インラインコードの方が高速です。関数呼び出しを実行する必要はありません(すべての関数呼び出しには時間がかかります)。不利な点は、関数が実際には関数として存在せず、したがってポインターがないため、インライン関数へのポインターを渡すことができないことです。また、関数をパブリックにエクスポートすることはできません(たとえば、ライブラリ内のインライン関数は、ライブラリにリンクしているバイナリ内では使用できません)。もう1つは、さまざまな場所から関数を呼び出すと、バイナリのコードセクションが大きくなることです(関数のコピーが1つだけあり、常にそこにジャンプするのではなく、関数のコピーが生成されるたびに)
通常、関数をインライン化するかどうかを手動で決定する必要はありません。例えば。 GCCは、最適化レベル(-Ox)および他のパラメーターに応じて自動的に決定します。 「関数の大きさ」などを考慮します。 (命令の数)、コード内で呼び出される頻度、インライン化によってバイナリがどれだけ大きくなるか、およびその他のいくつかのメトリック。例えば。関数が静的であり(したがって、とにかくエクスポートされない)、コード内で1回だけ呼び出され、関数へのポインターを使用しない場合、悪影響がないため、GCCが自動的にインライン化することを決定する可能性があります(バイナリ一度だけインライン化しても大きくなりません)。
インライン化することにより、コンパイラは呼び出しポイントに関数の実装を挿入します。これで行っているのは、関数呼び出しのオーバーヘッドを取り除くことです。ただし、インライン化のすべての候補が実際にコンパイラーによってインライン化されるという保証はありません。ただし、小さい関数の場合、コンパイラは常にインラインになります。したがって、何度も呼び出される関数のコード数が限られている場合(数行)、関数呼び出しのオーバーヘッドは関数自体の実行よりも長くかかる可能性があるため、インライン化のメリットがあります。
インライン化の良い候補の典型的な例は、単純な具象クラスのゲッターです。
CPoint
{
public:
inline int x() const { return m_x ; }
inline int y() const { return m_y ; }
private:
int m_x ;
int m_y ;
};
一部のコンパイラ(VC2005など)には、積極的なインライン化のオプションがあり、そのオプションを使用するときに「inline」キーワードを指定する必要はありません。
上記を繰り返すことはしませんが、呼び出された関数は実行時に解決されるため、仮想関数はインライン化されないことに注意してください。
インライン化は通常、最適化のレベル3(GCCの場合は-O3)で有効になります。場合によっては、速度が大幅に向上する可能性があります(可能な場合)。
プログラムに明示的にインライン化すると、コードサイズが大きくなり、速度が向上します。
コードサイズと速度のどちらが適切かを確認し、プログラムに含めるかどうかを決定する必要があります。
最適化のレベル3をオンにして、それを忘れて、コンパイラーに任せることができます。