GCC学習(5)ダイナミックライブラリインターフェイスの可視性

C ++の可視性のサポートが非常に重要なのはなぜですか?

簡単に言えば、以前に公開されていた(不要な)ELFシンボルのほとんどを非表示にします。つまり、次のことを意味します。

  • ダイナミックライブラリ(DSO、ダイナミック共有オブジェクトをロードする時間を大幅に短縮します。テスト後、大規模なテンプレートライブラリの読み込み時間が6分以上から6秒に変更されました。
  • これにより、オプティマイザーはより優れたコードを生成できます。PLTの間接的な値(関数呼び出しまたは変数アクセスをグローバルオフセットテーブル(PICコードなど)で検索する必要がある場合)を完全に回避できるため、最新のプロセッサでのパイプラインの一時停止を大幅に回避できるため、大幅に高速化されます。 upコードの速度。さらに、ほとんどのシンボルがローカルでバインドされている場合、DSO全体で完全に安全に省略(削除)できます。これにより、インラインの自由度が高まります。特に、インラインは「万が一の場合」のエントリポイントを維持する必要がありません。
  • ダイナミックライブラリのサイズを5〜20%削減します。ELFによってエクスポートされるシンボルテーブルの形式は非常にスペースを消費し、完全なエラーシンボル名を提供できます。多数のテンプレートを使用する場合、平均で約1000バイトかかります。C ++テンプレートは多くのシンボルを使用しますが、一般的なC ++ライブラリは簡単に30,000シンボル、約5〜6Mbを超える可能性があります。したがって、不要なシンボルの60〜80%を削除すると、DSOは数メガバイトになる可能性があります。
  • シンボルの競合の可能性が低くなります。このパッチのサポートにより、異なる処理に同じシンボルを内部的に使用していた2つのライブラリの古い問題がようやく解消されました。ハレルヤ!

上記のライブラリは極端なケースですが、新しい可視性のサポートにより、エクスポートされたシンボルテーブルが200,000を超えるシンボルから18,000未満に削減されます。バイナリファイルのサイズも21Mb削減されました!誰かがGNUリンカーバージョンスクリプトもそれを行うことができると提案するかもしれません。たぶんCプログラムの場合、これは正しいですが、C ++の場合、正しくありません-ワイルドカードは多くの誤ったシンボルを引き起こす可能性があるため、各シンボルを面倒に指定して公開しない限り(およびその複雑な名前の混乱)、ワイルドカードを使用する必要があります。クラスまたは関数の名前を変更する場合は、リンカースクリプトを更新する必要があります。上記のライブラリの場合、バージョンスクリプトを使用して、シンボルが40,000未満のシンボルテーブルを取得することはできません。さらに、スクリプトのリンカーバージョンを使用しても、GCCがコードをより適切に最適化することはできません。

Windows互換

Windows以外のバージョンのGCCは__declspec(dllexport)C/C++インターフェイスを共有ライブラリとしてマークするために使用されるものと同様のインターフェイスを提供できません。これは、WindowsおよびPOSIXで大規模なポータブルアプリケーションを扱う人々を苛立たせます[2]。優れたダイナミックライブラリインターフェイスの設計方法。優れたコードを書くことは、クラスの可視性を設計することと同じくらい重要です。

WindowsDLLとELFDSOの構文は異なりますが、Windowsのすべてのコードdllimport、マクロコンパイルを選択するときに使用するか使用するかを選択することに注意してくださいdllexport。プログラムにパッチを適用するだけで、WindowsのDLLコンパイルサポートを再利用できます。このパッチは5分で完了します。

ここでGCCが反映している機能セマンティクスが異なるウィンドウ:__declspec(dllexport)void(* foo)(void)およびvoid(__declspec(dllexport)* foo)(void)意味が完全に異なって表現されておりGCCでの非型警告のアプリケーションの所有物ではないことを示唆しています。

新しいC ++の可視属性サポートを使用するにはどうすればよいですか?

ヘッダーファイルでは、インターフェイスまたはAPIを公開する場合は常に__attribute__ ((visibility ("default")))構造体、クラス、および関数の宣言の前配置するだけで済みます(マクロを定義すると簡単になります)。定義で指定されています。その後、GCCがソースファイルをコンパイルするたびに、追加のパラメーターが-fvisibility=hiddenmakeシステムに追加されます。それで全部です!共有境界エラーをスローする場合は、以下の「C ++例外問題」を参照して、nm -C -DDSOの前後の違いを処理するために出力を使用してください。

#if defined _WIN32 || defined __CYGWIN__
  #ifdef BUILDING_DLL
    #ifdef __GNUC__
      #define DLL_PUBLIC __attribute__ ((dllexport))
    #else
      #define DLL_PUBLIC __declspec(dllexport) // Note: actually gcc seems to also supports this syntax.
    #endif
  #else
    #ifdef __GNUC__
      #define DLL_PUBLIC __attribute__ ((dllimport))
    #else
      #define DLL_PUBLIC __declspec(dllimport) // Note: actually gcc seems to also supports this syntax.
    #endif
  #endif
  #define DLL_LOCAL
#else
  #if __GNUC__ >= 4
    #define DLL_PUBLIC __attribute__ ((visibility ("default")))
    #define DLL_LOCAL  __attribute__ ((visibility ("hidden")))
  #else
    #define DLL_PUBLIC
    #define DLL_LOCAL
  #endif
#endif

extern "C" DLL_PUBLIC void function(int a);
class DLL_PUBLIC SomeClass
{
    
    
   int c;
   DLL_LOCAL void privateMethod();  // Only for use within this DSO
public:
   Person(int _c) : c(_c) {
    
     }
   static void foo(int a);
};

これは、より最適化されたコードを生成するのに役立ちます。コンパイルユニットの外部で定義を宣言すると、GCCは現在のコンパイルユニットがDSOの内部にあるか外部にあるかを判断できません。最悪の場合が考慮され、グローバルオフセットテーブル(GOT、グローバルオフセット)が考慮されます。表)このメカニズムにより、ダイナミックリンクライブラリはコードスペースと追加の再配置オーバーヘッドを負担します。このオーバーヘッドを減らすために、非表示の可視性属性hidden visibility(つまり、上記の例ではDLL_LOCAL)を手動で指定することにより、現在のDSOによって定義されたクラス、構造、関数、または変数の可視性をGCCに通知する必要があります。ヘッダーファイル。これにより、GCCはコードを最適化できます。

(毎回可視性を指定する)の問題を解決するために、GCCはオプションを追加しました-fvisibilityを使用し-fvisibility = hiddenて、可視性属性として明示的にマークされていないすべての宣言に非表示の可視性があることをGCCに通知します。上記の例のように、表示済み(DSOからエクスポート)としてマークされているクラスの場合でも、たとえばプライベートメンバーを非表示にするなど、マークを付けることができます。そのため、(DSO内から)呼び出されたときに最適なコードは次のようになります。古いコードを新しいシステムを使用するように変換するために、GCCは#pragmaGCC可視性コマンドもサポートするようになりました。

extern void foo(int);
#pragma GCC visibility push(hidden)
extern void someprivatefunct(int);
#pragma GCC visibility pop

#pragma GCC visibilityより強い-fvisibilityまた、外部宣言にも影響します。-fvisibility定義にのみ影響するため、既存のコードは最小限の変更で再コンパイルできます。Cの場合、この比率はC ++より正確です。C ++インターフェイスは影響を受ける-fvisibilityクラスを使用する傾向があります。

最後に、新しいコマンドオプション-fvisibility-inlines-hiddenこれにより、すべてのインラインクラスメンバー関数の非表示の可視性が得られ、使用される-fvisibility = hiddenほど大きくはありませんが、エクスポートされたシンボルテーブルとバイナリサイズが大幅に削減されます。ただし、-fvisibility-inlines-hiddenソースファイルを変更せずに使用できます。関数自体または関数のローカル静的データにとってアドレス識別子が重要なインラインを上書きする必要がない限り、上書きする必要があります。

C ++例外の問題(お読みください!)

例外の原因となったバイナリではなく、バイナリを使用してユーザー定義タイプの例外をキャッチするには、typeinfo検索が必要です。戻って最後のステートメントをもう一度読んでください。これが異常が不思議に誤動作し始める理由です!関数や変数と同様に、複数の共有オブジェクト間で発生する型はパブリックインターフェイスであり、デフォルトの可視性が必要です。明らかな最初のステップは、共有オブジェクトの境界を越えてスローできるすべてのタイプをデフォルトの可視性として常にマークすることです。これを行う必要があります。これは、(たとえば)例外コードのタイプがDLL Aに実装されているDLL B場合でも、そのタイプのインスタンスをスローするとDLL CcatchハンドラーがDLLBを検索するためtypeinfoです。
しかし、それだけではありません。ますます難しくなっています。デフォルトでは、シンボルの可視性は「デフォルト」ですが、リンカーが1つの非表示定義(1つの定義のみ)のみを検出した場合、typeinfoシンボルは永続的に非表示になります(C ++標準のODR-1定義ルールを覚えておいてください)。これはすべてのシンボルに当てはまりますが、typeinfoを介して影響を受ける可能性が高くなります。vtableのないクラスのtypeinfoシンボルは、EHを使用する各クラスのオブジェクトファイルでオンデマンドで定義され、定義が弱いため、これらの定義はリンク時に1つのコピーにマージされます。

この結果、オブジェクトファイルでのみ定義されているプリプロセッサを忘れた場合、またはいつでもスローされない場合、型は明示的なパブリックとして宣言さ-fvisibility = hiddenれ、ターゲットファイルで非表示としてマークされます。これにより、他のすべての定義が上書きされます。デフォルトの可視性でありtypeinfo、出力バイナリファイルに表示されなくなります(その後、そのタイプをスローすると、キャプチャされたバイナリファイルでterminate()が呼び出されます)。バイナリは完全にリンクされ、正しく機能しなくても機能します。

これについて警告するのは良いことですが、使い捨てタイプを一般に公開しないようにする理由はたくさんあります。プログラム全体の最適化がGCCに追加されるまで、コンパイラーはどの例外をローカルでキャッチするかを知りません。

他の漠然とリンクされたエンティティ(クラステンプレートの静的データメンバーなど)にも同じ問題が発生する可能性があります。クラスに非表示の可視性がある場合、データメンバーは複数のDSOでインスタンス化され、別々に参照される可能性があり、損傷を引き起こす可能性があります。

この問題は、dynamic_castのオペランドとしてクラスを使用する場合にも発生します。必ずそのようなものをすべてエクスポートしてください。

実践的な教育

次の手順は、ライブラリに完全なサポートを追加して、最高品質のコードを生成し、バイナリファイルのサイズ、読み込み時間、およびリンク時間を最小限に抑える方法です。すべての新しいコードは、最初からこのサポートを備えている必要があります。そして、特に速度に厳しい要件があるライブラリでは、それを完全に実装することはあなたの時間の価値があります-これは時間の1回限りの投資であり、それ以上のものではありません。ただし、これはお勧めしませんが、非常に短時間でライブラリに基本的なサポートを追加できます。

メインヘッダーファイル(またはどこにでも含まれる特定のヘッダー)で、次のコードを記述します。コードは、前述のTnFOXライブラリから取得されます。

// Generic helper definitions for shared library support
#if defined _WIN32 || defined __CYGWIN__
  #define FOX_HELPER_DLL_IMPORT __declspec(dllimport)
  #define FOX_HELPER_DLL_EXPORT __declspec(dllexport)
  #define FOX_HELPER_DLL_LOCAL
#else
  #if __GNUC__ >= 4
    #define FOX_HELPER_DLL_IMPORT __attribute__ ((visibility ("default")))
    #define FOX_HELPER_DLL_EXPORT __attribute__ ((visibility ("default")))
    #define FOX_HELPER_DLL_LOCAL  __attribute__ ((visibility ("hidden")))
  #else
    #define FOX_HELPER_DLL_IMPORT
    #define FOX_HELPER_DLL_EXPORT
    #define FOX_HELPER_DLL_LOCAL
  #endif
#endif

// Now we use the generic helper definitions above to define FOX_API and FOX_LOCAL.
// FOX_API is used for the public API symbols. It either DLL imports or DLL exports (or does nothing for static build)
// FOX_LOCAL is used for non-api symbols.

#ifdef FOX_DLL // defined if FOX is compiled as a DLL
  #ifdef FOX_DLL_EXPORTS // defined if we are building the FOX DLL (instead of using it)
    #define FOX_API FOX_HELPER_DLL_EXPORT
  #else
    #define FOX_API FOX_HELPER_DLL_IMPORT
  #endif // FOX_DLL_EXPORTS
  #define FOX_LOCAL FOX_HELPER_DLL_LOCAL
#else // FOX_DLL is not defined: this means FOX is a static lib.
  #define FX_API
  #define FOX_LOCAL
#endif // FOX_DLL

もちろん、FOXをライブラリに適したプレフィックスに置き換えることもできます。Win32もサポートするプロジェクトの場合、上記のよく知られたものの多くが見つかります(ほとんどのWin32マクロメカニズムを再利用してGCCをサポートできます)。説明:

  • 定義されている場合_WIN32(Windowsのコンパイル時に自動的に定義されます)
  • 定義されている場合FOX_DLL_EXPORTS、ライブラリをコンパイルし、エクスポートされたシンボルが必要になります。したがってFOX_DLL_EXPORTS、FOXDLLライブラリをコンパイルするようにコンパイルシステムで定義します。MSVCは、すべてのIDEで_EXPORTS終了コンテンツを定義します(CMakeのデフォルト設定と同じです。CMakeWikiBuildingWinDLLを参照してください)。
  • 定義されていない場合FOX_DLL_EXPORTS(つまり、クライアントがライブラリを使用している場合)、入力ライブラリとシンボルを使用します
  • WIN32定義されていない場合(つまり、UnixでGCCをコンパイルする)
  • __GNUC__>=4trueの場合、GCCバージョンが4.0より大きいことを意味するため、これらの新機能がサポートされます
  • 各ライブラリの非テンプレートおよび非静的関数定義(ヘッダーファイルとソースファイル)について、それがパブリックであるか内部であるかを判別します。
  • オブジェクトがパブリック形式で使用されている場合は、FOX_APIこのようにマークするため使用しますextern FOX_API PublicFunc()
  • 内部でのみ使用される場合はFOX_LOCAL、次のようにマークを付けるために使用extern FOX_LOCAL PublicFunc()ます。静的関数を分割する必要も、テンプレート化する必要もないことを忘れないでください。
  • ライブラリで定義されている非テンプレートクラス(ヘッダーファイルとソースファイル)ごとに、パブリッククラスか内部クラスかを決定します
  • 公開されている場合は、次のFOX_APIようマークしますFOX_API PublicClass
  • 内部で使用する場合は、次のFOX_LOCALようマークしますFOX_LOCAL PublicClass
  • インターフェイスの一部ではない派生クラスのメンバー関数、特にプライベートで友人によって使用されないメンバー関数には、それぞれFOX_LOCALのマークを付ける必要があります。
  • ビルドシステム(Makefileなど)では、各GCC呼び出しのコマンドライン引数に-fvisibility = hiddenおよび-fvisibility-inlines-hiddenオプションを追加することをお勧めします。共有オブジェクトの境界を正しく通過するすべての例外を含め、後でライブラリを徹底的にテストすることを忘れないでください。

前後の違いを確認したい場合は、コマンドnm -C -D使用して、エクスポートされたすべてのシンボルを非ハイブリッド形式で一覧表示してください


拡張読み取り:https//developer.ibm.com/technologies/systems/articles/au-aix-symbol-visibility/
[1] https://gcc.gnu.org/wiki/Visibility
[2] "__ declspec"はMicrosoft C ++の特別なキーワードであり、いくつかの属性を使用して標準のC ++を拡張できます。これらの属性は、align、allocate、deprecated、dllexport、dllimport、naked、noinline、noreturn、nothrow、novtable、selectany、thread、property、およびuuidです。

おすすめ

転載: blog.csdn.net/weixin_39258979/article/details/113799071