C&を同期する方法パフォーマンスの低下を最小限に抑えるC ++ライブラリ?
-
08-07-2019 - |
質問
ベクトル、行列、四元数などを処理するための多数の数学ルーチンを備えたCライブラリがあります。組み込み作業やLua拡張機能としてよく使用するため、Cのままにしておく必要があります。さらに、C APIを使用した数学演算のオブジェクト管理と演算子のオーバーロードをより便利にするC ++クラスラッパーがあります。ラッパーはヘッダーファイルのみで構成され、可能な限りインライン化が使用されます。
Cコードをラップすることと、実装を直接C ++クラスに移植してインライン化することに対して、かなりのペナルティがありますか?このライブラリは、タイムクリティカルなアプリケーションで使用されます。では、間接性を排除することで、2つのポートのメンテナンスの頭痛を補うことができますか?
Cインターフェースの例:
typedef float VECTOR3[3];
void v3_add(VECTOR3 *out, VECTOR3 lhs, VECTOR3 rhs);
C ++ラッパーの例:
class Vector3
{
private:
VECTOR3 v_;
public:
// copy constructors, etc...
Vector3& operator+=(const Vector3& rhs)
{
v3_add(&this->v_, this->v_, const_cast<VECTOR3> (rhs.v_));
return *this;
}
Vector3 operator+(const Vector3& rhs) const
{
Vector3 tmp(*this);
tmp += rhs;
return tmp;
}
// more methods...
};
解決
ラッパー自体はインライン化されますが、通常、Cライブラリへのメソッド呼び出しはインライン化されません。 (これには技術的に可能なリンク時間の最適化が必要ですが、今日のツールではせいぜい初歩的なものです)
一般に、関数呼び出し自体はそれほど高価ではありません。サイクルコストは過去数年間で大幅に減少しており、簡単に予測できるため、コールペナルティは無視できます。
ただし、インライン化はさらなる最適化への扉を開きます。v= a + b + cがある場合、ラッパークラスはスタック変数の生成を強制しますが、インライン化された呼び出しの場合、データの大部分はFPUに保持できますスタック。また、インラインコードを使用すると、命令を簡素化したり、定数値を考慮したりすることができます。
したがって、投資前の対策のルールは当てはまりますが、ここでは改善の余地があります。
典型的な解決策は、C実装を、インライン関数または「C」として使用できる形式にすることです。 body:
// V3impl.inl
void V3DECL v3_add(VECTOR3 *out, VECTOR3 lhs, VECTOR3 rhs)
{
// here you maintain the actual implementations
// ...
}
// C header
#define V3DECL
void V3DECL v3_add(VECTOR3 *out, VECTOR3 lhs, VECTOR3 rhs);
// C body
#include "V3impl.inl"
// CPP Header
#define V3DECL inline
namespace v3core {
#include "V3impl.inl"
} // namespace
class Vector3D { ... }
これは、比較的単純なボディを持つ選択されたメソッドに対してのみ意味があります。通常、メソッドは直接必要ないため、メソッドをC ++実装用の別のネームスペースに移動します。
(インラインはコンパイラーのヒントにすぎず、メソッドを強制的にインライン化しないことに注意してください。 しかし、それは良いことです。内側のループのコードサイズが命令キャッシュを超える場合、インライン化はパフォーマンスを簡単に低下させます)
pass / return-by-referenceを解決できるかどうかは、コンパイラの強度に依存します。 foo(X * out) スタック変数を強制しますが、 X foo() レジスタに値を保持します。
他のヒント
C ++クラス関数でCライブラリ呼び出しをラップするだけの場合(つまり、C ++関数はC関数を呼び出すだけです)、コンパイラーはこれらの呼び出しを最適化し、パフォーマンスが低下しないようにします。
パフォーマンスに関する他の質問と同様に、あなたは答えを得るために測定するように言われます(そしてそれは厳密に正しい答えです)。
しかし、経験則として、実際にインライン化できる単純なインラインメソッドの場合、パフォーマンスの低下はありません。一般に、呼び出しを別の関数に渡すだけのインラインメソッドは、インライン化の優れた候補です。
ただし、ラッパーメソッドがインライン化されていなくても、重要なループでラッパーメソッドが呼び出されていない限り、パフォーマンスが低下することはありません。それでも、ラップされた関数自体が多くの作業を行わなかった場合にのみ、測定可能です。
このタイプのことは、最後に心配することです。コードを正しく維持しやすくし、適切なアルゴリズムを使用していることを最初に心配します。
最適化に関連するすべての場合と同様に、答えは、最適化が価値があるかどうかを知る前にパフォーマンス自体を測定する必要があるということです。
- 2つの異なる関数をベンチマークします。1つはCスタイルの関数を直接呼び出し、もう1つはラッパーを介して呼び出します。どちらが速く実行されるか、または差が測定の誤差範囲内にあるかどうかを確認します(つまり、測定可能な差がないことを意味します)。
- 前の手順の2つの関数によって生成されたアセンブリコードを確認します(gccでは、
-S
または-save-temps
を使用します)。コンパイラがバカなことをしたかどうか、またはラッパーにパフォーマンスのバグがあるかどうかを確認します。
ラッパーを使用しないほうがパフォーマンスの違いが大きすぎる場合を除き、バグを導入するリスクがあるため、再実装はお勧めできません(正気に見えるが間違っている結果を引き起こす可能性もあります)。違いが大きい場合でも、C ++がCと非常に互換性があり、C ++コード内でもCスタイルでライブラリを使用することを覚えておけば、単純でリスクが少なくなります。
パフォーマンスの大きな違いに気付かないと思います。ターゲットプラットフォームがすべてのデータタイプをサポートしていると仮定すると、
DSと他のいくつかのARMデバイス用のコーディングをしていますが、浮動小数点は悪です... floatをFixedPoint&lt; 16,8&gt;にtypedefする必要がありました
関数呼び出しのオーバーヘッドがあなたを遅くしているのではないかと心配なら、Cコードのインライン化やマクロ化をテストしてみませんか?
また、Cコードのconstの正確性を向上させてはいけません-const_castは、特に制御するインターフェイスでは、控えめに使用する必要があります。