共有オブジェクトは、同じシステム内の複数のアプリケーションで使用できます。したがって、共有オブジェクトの性能の影響は、それを使用するアプリケーションだけでなく、システム全体に至るほどに大きくなる可能性があります。
共有オブジェクト内の実際のコードは、実行中のプロセスの性能に直接影響しますが、ここでは共有オブジェクト自体の実行時処理に焦点を絞って性能の問題を説明します。次の節では、再配置によるオーバーヘッドとともに、テキストサイズや純度 (purity) などの面についても見ながら、この処理について詳しく説明します。
性能について述べる前に、使用可能なツールと ELF ファイルの内容の解析に対するその使用法を理解しておくと役立ちます。
ELF ファイルに定義されているセクションまたはセグメントのいずれかのサイズに対する参照は、頻繁に行われます (ELF 形式の詳細については、第 7 章「オブジェクトファイル」を参照)。ファイルのサイズは、size(1) コマンドを使用して表示できます。次に例を示します。
$ size -x libfoo.so.1 59c + 10c + 20 = 0x6c8 $ size -xf libfoo.so.1 ..... + 1c(.init) + ac(.text) + c(.fini) + 4(.rodata) + ¥ ..... + 18(.data) + 20(.bss) ..... |
最初の例は、SunOSTM オペレーティングシステムの以前のリリースから引き続き使用されてきたカテゴリである、共有オブジェクトテキスト、データ、および bss のサイズを示します。ELF 形式は、データをセクションに編成することによって、ファイル内のデータを表現するためのより精密な方法を提供します。2 番目の例は、ファイルの読み込み可能な各セクションのサイズを表示しています。
セクションは、セグメントと呼ばれる単位に割り当てられます。セグメントの一部は、ファイルの部分がメモリーにどのように割り当てられるかを記述します (mmap(2) のマニュアルページを参照)。これらの読み込み可能セグメントは、dump(1) コマンドを使用して、LOAD エントリを調べることによって表示できます。次に例を示します。
$ dump -ov libfoo.so.1 libfoo.so.1: ***** PROGRAM EXECUTION HEADER ***** Type Offset Vaddr Paddr Filesz Memsz Flags Align LOAD 0x94 0x94 0x0 0x59c 0x59c r-x 0x10000 LOAD 0x630 0x10630 0x0 0x10c 0x12c rwx 0x10000 |
ここで、共有オブジェクト libfoo.so.1 には、一般にテキストセグメントおよびデータセグメントと呼ばれる 2 つの読み込み可能なセグメントがあります。テキストセグメントは、その内容の読み取りと実行 (r-x) も可能になるように割り当てられます。これに対して、データセグメントは、その内容の変更 (rwx) も可能になるように割り当てられます。データセグメントのメモリーサイズ (Memsz) が、ファイルサイズ (Filesz) とは異なることに注意してください。この違いは、実際にはデータセグメントの一部であり、セグメントが読み込まれると動的に作成される .bss セクションを示すものです。
ただし、通常プログラマは、関数とデータ要素をそのコード内に定義するシンボルの点からファイルについて考えます。これらのシンボルは、nm(1) を使用して表示できます。次に例を示します。
$ nm -x libfoo.so.1 [Index] Value Size Type Bind Other Shndx Name ......... [39] |0x00000538|0x00000000|FUNC |GLOB |0x0 |7 |_init [40] |0x00000588|0x00000034|FUNC |GLOB |0x0 |8 |foo [41] |0x00000600|0x00000000|FUNC |GLOB |0x0 |9 |_fini [42] |0x00010688|0x00000010|OBJT |GLOB |0x0 |13 |data [43] |0x0001073c|0x00000020|OBJT |GLOB |0x0 |16 |bss ......... |
シンボルを含むセクションは、シンボルテーブルのセクションインデックス (Shndx) フィールドを参照し、dump(1) を使用してファイル内のセクションを表示することによって判定できます。次に例を示します。
$ dump -hv libfoo.so.1 libfoo.so.1: **** SECTION HEADER TABLE **** [No] Type Flags Addr Offset Size Name ......... [7] PBIT -AI 0x538 0x538 0x1c .init [8] PBIT -AI 0x554 0x554 0xac .text [9] PBIT -AI 0x600 0x600 0xc .fini ......... [13] PBIT WA- 0x10688 0x688 0x18 .data [16] NOBI WA- 0x1073c 0x73c 0x20 .bss ......... |
前出の nm(1) および dump(1) の例による出力を使用すると、セクション .init、.text、および .fini に対する関数 _init、foo、および _fini の関連付けが見られます。これらのセクションは読み取り専用であるため、テキストセグメントの一部です。
同様に、データ配列 data と bss は、それぞれセクション .data と .bss に関連付けられています。これらのセクションは書き込み可能であるため、データセグメントの一部です。
前出の dump(1) の表示は例のために簡素化されています。
ここで説明したツール情報を利用すると、生成するすべての ELF ファイル内でのコードおよびデータの位置を解析できます。この知識は、次の節での説明を理解する上で役立ちます。
アプリケーションがある共有オブジェクトを使用して構築される場合、そのオブジェクトの読み込み可能な内容全体が、実行時にそのプロセスの仮想アドレス空間に割り当てられます。共有オブジェクトを使用する各プロセスは、まずメモリー内にある共有オブジェクトの単一のコピーを参照します。
共有オブジェクト内の再配置は処理されて、シンボリック参照を該当する定義に結合します。これにより、共有オブジェクトがリンカーによって生成されたときには得られなかった真の仮想アドレスが計算されます。通常、これらの再配置によって、プロセスのデータセグメント内のエントリが更新されます。
メモリー管理スキーマは、プロセス間で共有オブジェクトの共有メモリーをページ細部のレベルで動的リンクするときの基本となります。メモリーページは、実行時に変更されていなければ共有できます。プロセスは、データ項目の書き込み時、または共有オブジェクトへの参照の再配置時に共有オブジェクトのページに書き込む場合、そのページの専用コピーを生成します。この専用コピーは共有オブジェクトの他のユーザーに対して何も影響しませんが、このページは、他のプロセス間での共有に伴う利点をすべて失います。この方法で変更されたテキストページは、「純粋でない」(impure) と呼ばれます。
メモリーに割り当てられた共有オブジェクトのセグメントは、2 つの基本的なカテゴリに分類されます。これは、読み取り専用の「テキスト」セグメントと、読み書き可能な「データ」セグメントです (ELF ファイルからこの情報を取得する方法については、「役に立つツール」を参照)。共有オブジェクトを開発するときの主要目的は、テキストセグメントを最大化して、データセグメントを最小化することにあります。これにより、共有オブジェクトの初期設定と使用に必要な処理の量を削減しながら、コード共有の量を最適化できます。次の節では、この目的を達成するために役立つメカニズムを示します。
オブジェクトをレイジー読み込みするように設定すると、共有オブジェクトの依存関係の読み込みは、最初に参照されるまで延期できます (「動的依存関係のレイジーローディング」を参照)。
依存関係の数が少ないアプリケーションの場合、アプリケーションを実行すると、レイジー読み込み可に設定されているかどうかに関係なく、すべての依存関係が読み込まれることがあります。ただし、レイジー読み込みでは依存関係の処理が処理の起動時から延期され、処理の実行期間全体にわたって広がるので、全体的な処理性能は向上します。
多くの依存関係を持つアプリケーションの場合、レイジー読み込みを使用すると、実行の特定スレッドに参照されないためにまったく読み込まれない依存関係もあります。
実行時に必要なページ変更が最も少ないプログラムを作成するために、コンパイラは、-K pic オプションによって、位置に依存しないコードを生成します。動的実行可能ファイル内のコードは、通常、メモリー内の固定アドレスに結合されていますが、位置に依存しないコードは、プロセスのアドレス空間内にある任意の場所に読み込みできます。このコードは、特定のアドレスに結合されていないため、それを使用する各プロセスの異なるアドレスでページ変更を行わなくても、正しく実行されます。
位置に依存しないコードを使用すると、再配置可能な参照が、共有オブジェクトのデータセグメント内のデータを使用する間接参照の形式で生成されます。この結果、テキストセグメントコードは読み取り専用のままになり、すべての再配置更新がデータセグメント内の対応するエントリに適用されます。これらの 2 つのセクションの使用方法については、「大域オフセットテーブル (プロセッサ固有)」と 「プロシージャのリンクテーブル (プロセッサ固有)」を参照してください。
共有オブジェクトが位置に依存しないコードから構築される場合、テキストセグメントでは通常、実行時に大量の再配置を実行する必要があります。この再配置を処理するために実行時リンカーが用意されていますが、この処理によるシステムオーバーヘッドによって深刻な性能低下が生じるおそれがあります。
テキストセグメントに対して再配置を必要とする共有オブジェクトは、dump(1) を使用して、TEXTREL エントリの出力を調べることによって識別できます。次に例を示します。
$ cc -o libfoo.so.1 -G -R. foo.c $ dump -Lv libfoo.so.1 | grep TEXTREL [9] TEXTREL 0 |
TEXTREL エントリの値は不適切です。共有オブジェクトにこの値が存在する場合は、テキスト再配置があることを示しています。
テキスト再配置を含む共有オブジェクトの作成を防止するには、リンカーの -z text フラグを使用することをお勧めします。このフラグを使用すると、リンカーは、入力として使用された、位置に依存しないコード以外のコードのソースを示す診断を生成し、意図した共有オブジェクトの生成は失敗に終わります。次に例を示します。
$ cc -o libfoo.so.1 -z text -G -R. foo.c Text relocation remains referenced against symbol offset in file foo 0x0 foo.o bar 0x8 foo.o ld: fatal: relocations remain against allocatable but ¥ non-writable sections |
ここでは、ファイル foo.o から位置に依存しないコード以外のコードが生成されたために、テキストセグメントに対して 2 つの再配置が生成されています。これらの診断は、可能な場合、再配置の実行に必要なシンボリック参照すべてを示します。この場合、再配置はシンボル foo と bar に対するものです。
-K pic オプションを使用しないという原因以外で、共有オブジェクトの生成時にテキスト再配置が作成される最も一般的な原因は、位置に依存しない適切なプロトタイプによって符号化されていない手書きアセンブラコードを含めているというものです。
中間アセンブラファイルを生成するコンパイラ機能を使用すると、いくつかの単純なテストケースソースファイルを試すことによって、位置からの独立性を有効にするために使用されるコーディング手法がわかります。
プロセッサによっては、位置に依存しないフラグの 2 つ目の形式として -K PIC も使用できます。このフラグを使用すると、追加コードによるオーバーヘッドと引き換えに、より多数の再配置を処理できます (詳細については、cc(1) を参照)。
「基本システム」で説明したように、共有オブジェクトのテキストセグメントだけがそれを使用するすべてのプロセスによって共有され、データセグメントは通常共有されません。共有オブジェクトを使用する各プロセスは、通常、そのデータセグメント全体の専用メモリーコピーをそのセグメント内に書き込まれるデータ項目として生成します。データセグメントを削減するにはテキストセグメントに書き込まれることがないデータ要素を移動するか、またはデータ項目を完全に削除します。
次の節では、データセグメントのサイズを削減するために使用できるいくつかのメカニズムについて説明します。
読み取り専用のデータ要素はすべて、テキストセグメントに移動する必要があります。これは、const 宣言を使用して達成できます。たとえば、次の文字列は、書き込み可能なデータセグメントの一部である .data セクションにあります。
char * rdstr = 'this is a read-only string'; |
これに対して、次の文字列は、テキストセグメント内にある読み取り専用データセクションである .rodata セクション内にあります。
const char * rdstr = 'this is a read-only string'; |
読み取り専用要素をテキストセグメントに移動することによるデータセグメントの削減は目的に沿うものですが、再配置を必要とするデータ要素を移動すると、逆効果になるおそれがあります。たとえば、次の文字列配列があるとします。
char * rdstrs[] = { "this is a read-only string", "this is another read-only string" }; |
最初は、次の定義の方が良く思われるかもしれません。
const char * const rdstrs[] = { ..... }; |
これにより、文字列とこれらの文字列へのポインタ配列は、確実に .rodata セクションに置かれます。ただし、ユーザーがアドレス配列を読み取り専用と認識しても、実行時にはこれらのアドレスを再配置しなければなりません。したがって、この定義では再配置が作成されます。この定義は次のように表わします。
const char * rdstrs[] = { ..... }; |
これにより、配列文字列は読み取り専用のテキストセグメント内に保持されますが、配列ポインタは、再配置できる書き込み可能なデータセグメント内に保持されます。
コンパイラによっては、位置に依存しないコードを生成するときに、実行時再配置を起こして、このような項目を書き込み可能セグメントに置くことになる読み取り専用割り当てを検出できるものがあります (たとえば、.picdata)。
多重定義されたデータを短縮すると、データを削減できます。同じエラーメッセージが複数回発生するプログラムの場合は、1 つの大域なデータを定義し、他のインスタンスすべてにこれを参照させると効率が良くなります。次に例を示します。
const char * Errmsg = "prog: error encountered: %d"; foo() { ...... (void) fprintf(stderr, Errmsg, error); ...... |
この種のデータ削減に適した対象は文字列です。共有オブジェクトでの文字列の使用は、strings(1) を使用して調べることができます。次に例を示します。
$ strings -10 libfoo.so.1 | sort | uniq -c | sort -rn |
上記のコードは、ファイル libfoo.so.1 内に、データ文字列のソートされたリストを生成します。このリスト内の各項目には、文字列の出現回数を示す接頭辞が付いています。
データ項目用の常時記憶領域は、関連する機能が自動 (スタック) 変数を使用するように設計できる場合、完全に削除することができます。常時記憶領域を少しでも削除すると、通常これに対応して、必要な実行時再配置の数も減ります。
大きなデータバッファーは、通常、常時記憶領域を使用して定義するのではなく、動的に割り当てる必要があります。これにより、アプリケーションの現在の呼び出しで必要なバッファーだけが割り当てられるため、メモリー全体を節約できます。動的割り当てを行うと、互換性に影響を与えることなくバッファーのサイズを変更できるため、柔軟性も増します。
「共有可能性の最大化」で説明したメカニズムの多くは、共有オブジェクトを使用するときに生じるページングを削減するために役立ちます。ここでは、一般的なソフトウェア性能に関する考慮事項のいくつかについてさらに説明します。
新しいページにアクセスするすべてのプロセスでページフォルトが発生します。これはコストのかかる操作であり、また共有オブジェクトは多数のプロセスで使用できるため、共有オブジェクトへのアクセスによって生成されるページフォルトの数を減らすと、プロセスおよびシステム全体の効率が改善されます。
使用頻度の高いルーチンとそのデータを隣接するページの集合として編成すると、参照の効率が良くなるため、性能は通常向上します。あるプロセスがこれらの関数の 1 つを呼び出すとき、この関数がすでにメモリー内にある場合があります。これは、この関数が、使用頻度の高い他の関数のすぐ近くに存在するためです。同様に、相互に関連する関数をグループ化すると、参照効率が向上します。たとえば、関数 foo() への呼び出しによって、常に関数 bar() が呼び出される場合は、これらの関数を同じページ上に置きます。cflow(1)、tcov(1)、prof(1)、および gprof(1) は、コードカバレージとプロファイリングを判定するために役立ちます。
関連する機能を各自の共有オブジェクトに分離することもお勧めします。標準 C ライブラリは従来、関連しない多数の関数を含んで構築されていて、たとえば単一の実行可能ファイルがこのライブラリ内のすべてを使用することはほとんどありません。このライブラリは広範囲に使用されるため、実際に使用頻度の最も低い関数がどれかを判定することもかなり困難です。これに対して、共有オブジェクトを最初から設計する場合は、関連する関数だけを共有オブジェクト内に保持することをお勧めします。これにより、参照効率が向上するだけでなく、オブジェクト全体のサイズを減らすという効果も得られます。
「再配置処理」では、実行時リンカーが動的実行可能ファイルと共有オブジェクトを再配置して、実行可能プロセスを作成するためのメカニズムについて説明しました。「シンボルの検索」と 「再配置が実行されるとき」は、この再配置処理を 2 つの領域に分類して、関連のメカニズムを簡素化して説明しています。これらの 2 つのカテゴリは、再配置による性能への影響を考慮するためにも最適です。
実行時リンカーは、シンボルを検索する必要がある場合、デフォルトでは各オブジェクトを検索して検索を行います。実行時リンカーは、まず動的実行可能ファイルから始めて、オブジェクトが読み込まれるのと同じ順序で各共有オブジェクトへと進みます。ほとんどの場合、シンボル再配置を必要とする共有オブジェクトは、シンボル定義の提供者になります。
この場合、この再配置に使用されるシンボルが共有オブジェクトのインタフェースの一部として必要ではない場合、このシンボルは静的変数または自動変数に変換される可能性が高くなります。シンボル削減は、共有オブジェクトのインタフェースから削除されたシンボルにも適用できます (詳細については、「シンボル範囲の縮小」を参照)。これらの変換を行うことによって、リンカーは、共有オブジェクトの作成中にこれらのシンボルに対するシンボル再配置を処理しなければならなくなります。
共有オブジェクトから表示できなければならない唯一の大域データ項目は、そのユーザーインタフェースに関するものです。しかし、大域データは異なる複数のソースファイルにある複数の関数から参照できるように定義されていることが多いため、これは歴史的に達成が困難です。シンボルの縮小 (「シンボル範囲の縮小」を参照) を適用することによって、不要な大域シンボルを削除できます。共有オブジェクトからエクスポートされた大域シンボルの数を少しでも減らせば、再配置のコストを削減し、性能全体を向上させることができます。
直接結合では、多数のシンボル再配置や依存関係を伴う動的プロセスでのシンボル検索のオーバーヘッドを大幅に削減できます (「直接結合」を参照)。
すべてのデータ参照再配置は、アプリケーションが制御を取得する前に、プロセス初期設定段階で実行する必要があります。これに対して、関数参照は、関数の最初のインスタンスが呼び出されるまで延期できます。データ再配置の数を減らすことによって、プロセスの実行時初期設定も削減されます。
初期設定再配置コストは、たとえば機能インタフェースによってデータ項目を返すなど、データ再配置を関数再配置に変換して延期することもできます。この変換によって、初期設定再配置コストがプロセスの実行期間中に効率的に分配されると、性能は明らかに向上します。いくつかの機能インタフェースはプロセスの特定の呼び出しでは決して呼び出されない可能性もあるため、それらの再配置オーバーヘッドもすべてなくなります。
機能インタフェースを使用した場合の利点については、「コピー再配置」で説明します。この節では、動的実行可能ファイルと共有オブジェクトの間で使用される特殊でコストのかかる再配置メカニズムについて述べ、この再配置によるオーバーヘッドを回避する方法の例を示します。
再配置は、デフォルトでは、適用対象のセクションによってグループ化されます。ただし、オブジェクトを -z combreloc オプションによって構築すると、プロシージャーリンクテーブル再配置を除くすべてが、単一の共通セクション .SUNW_reloc に置かれます。
この方法で再配置レコードを結合すると、すべての RELATIVE 再配置が 1 つにグループ化され、すべてのシンボルの再配置はシンボル名によって並べ替えられます。RELATIVE 再配置をグループ化すると DT_RELACOUNT/DT_RELCOUNT .dynamic エントリを使用した最適な実行時処理が行われ、シンボルのエントリを並べ替えると実行時にシンボルを検索する時間を削減できます。
共有オブジェクトは、通常、位置に依存しないコードによって構築されます。このタイプのコードから外部データ項目への参照は、1 組のテーブルによる間接アドレス指定を使用します (詳細については、「位置に依存しないコード」を参照)。これらのテーブルは、データ項目の実アドレスによって実行時に更新されます。この実アドレスによって、コード自体を変更することなくデータにアクセスすることができます。
ただし、動的実行可能ファイルは通常、位置に依存しないコードからは作成されません。したがって、これらのファイルが作成する外部データへの参照は、その参照を行うコードを変更することによって実行時にしか実行できないように見えます。読み取り専用のテキストセグメントの変更は回避する必要があるため、コピー再配置と呼ばれる再配置手法が、この参照を解決するために使用されます。
動的実行可能ファイルを作成するためにリンカーが使用され、データ項目への参照が依存共有オブジェクトのどれかに常駐する場合は、動的実行可能ファイルの .bss で、共有オブジェクト内のデータ項目のサイズに等しいスペースが割り当てられます。このスペースには、共有オブジェクトに定義されているのと同じシンボリック名も割り当てられます。リンカーは、このデータ割り当てとともに特殊なコピー再配置レコードを生成して、実行時リンカーに対し、共有オブジェクトから動的実行可能ファイル内のこの割り当てスペースへデータをコピーするように指示します。
このスペースに割り当てられたシンボルは大域であるため、すべての共有オブジェクトからの参照をすべてを満たすために使用されます。この結果、動的実行可能ファイルはデータ項目を継承し、この項目を参照するプロセス内の他のオブジェクトすべてがこのコピーに結合されます。コピーの元となるデータは未使用になります。
このメカニズムを明確に示す例があります。この例では標準 C ライブラリ内で保持されるシステムエラーメッセージの配列を使用します。SunOS オペレーティングシステムの以前のリリースでは、この情報へのインタフェースが、2 つの大域変数 sys_errlist[] および sys_nerr によって提供されました。最初の変数はエラーメッセージ文字列を提供し、2 つ目の変数は配列自体のサイズを示しました。これらの変数はアプリケーション内で、通常次のように使用されていました。
$ cat foo.c extern int sys_nerr; extern char * sys_errlist[]; char * error(int errnumb) { if ((errnumb < 0) || (errnumb >= sys_nerr)) return (0); return (sys_errlist[errnumb]); } |
ここで、アプリケーションは、関数 error を使用して、番号 errnumb に対応するシステムエラーメッセージを取得するために提供しています。
このコードを使用して作成された動的実行可能ファイルを調べると、コピー再配置の実装が更に詳細に示されます。
$ cc -o prog main.c foo.c $ nm -x prog | grep sys_ [36] |0x00020910|0x00000260|OBJT |WEAK |0x0 |16 |sys_errlist [37] |0x0002090c|0x00000004|OBJT |WEAK |0x0 |16 |sys_nerr $ dump -hv prog | grep bss [16] NOBI WA- 0x20908 0x908 0x268 .bss $ dump -rv prog **** RELOCATION INFORMATION **** .rela.bss: Offset Symndx Type Addend 0x2090c sys_nerr R_SPARC_COPY 0 0x20910 sys_errlist R_SPARC_COPY 0 ......... |
ここで、リンカーは、動的実行可能ファイルの .bss にスペースを割り当てて、sys_errlist および sys_nerr によって表わされるデータを受け取っています。これらのデータは、プロセス初期設定時に、実行時リンカーによって C ライブラリからコピーされます。このため、これらのデータを使用する各アプリケーションは、データの専用コピーを各自のデータセグメントで取得します。
この手法には、実際には 2 つの欠点があります。まず、各アプリケーションでは、実行時のデータコピーによるオーバーヘッドによって性能が低下します。もう 1 つは、データ配列 sys_errlist のサイズが、C ライブラリのインタフェースの一部になるという点です。この配列のサイズが変わると、新しいエラーメッセージが追加されて、この配列を参照する動的実行可能ファイルすべてで新しいエラーメッセージにアクセスするための新しいリンク編集が行われます。この新しいリンク編集が行われないと、動的実行可能ファイル内の割り当てスペースが不足して、新しいデータを保持できません。
このような欠点は、動的実行可能ファイルに必要なデータが機能インタフェースによって提供されればなくなります。ANSI C 関数 strerror(3C) は、この点を示すものです。この関数は、提示されたエラー番号に基づいて該当するエラー文字列へのポインタを返すように実装されます。この関数の実装状態は次のようになります。
$ cat strerror.c static const char * sys_errlist[] = { "Error 0", "Not owner", "No such file or directory", ...... }; static const int sys_nerr = sizeof (sys_errlist) / sizeof (char *); char * strerror(int errnum) { if ((errnum < 0) || (errnum >= sys_nerr)) return (0); return ((char *)sys_errlist[errnum]); } |
foo.c のエラールーチンは、ここではこの機能インタフェースを使用するように単純化できます。これによって、プロセス初期設定時に元のコピー再配置を実行する必要がなくなります。
また、データは共有オブジェクト限定のものであるため、そのインタフェースの一部ではなくなります。したがって、共有オブジェクトは、データを使用する動的実行可能ファイルに悪影響を与えることなく、自由にデータを変更できます。共有オブジェクトのインタフェースからデータ項目を削除すると、一般に共有オブジェクトのインタフェースとコードが維持しやすくなるとともに、性能も向上します。
コピー再配置は回避する必要がありますが、ldd(1) に -d オプションまたは -r オプションのどちらかをつけて使用すると、動的実行可能ファイル内にそれがないかを検査できます。
たとえば、動的実行可能ファイル prog が当初、次の 2 つのコピー再配置が記録されるように、共有オブジェクト libfoo.so.1 に対して構築されている場合を考えます。
$ nm -x prog | grep _size_ [36] |0x000207d8|0x40|OBJT |GLOB |15 |_size_gets_smaller [39] |0x00020818|0x40|OBJT |GLOB |15 |_size_gets_larger $ dump -rv size | grep _size_ 0x207d8 _size_gets_smaller R_SPARC_COPY 0 0x20818 _size_gets_larger R_SPARC_COPY 0 |
さらに、これらのシンボルについて異なるサイズを含む、この共有オブジェクトの新しいバージョンが提供されているとします。
$ nm -x libfoo.so.1 | grep _size_ [26] |0x00010378|0x10|OBJT |GLOB |8 |_size_gets_smaller [28] |0x00010388|0x80|OBJT |GLOB |8 |_size_gets_larger |
この場合、動的実行可能ファイルに対して ldd(1) を実行すると、次のように表示されます。
$ ldd -d prog libfoo.so.1 => ./libfoo.so.1 ........... copy relocation sizes differ: _size_gets_smaller (file prog size=40; file ./libfoo.so.1 size=10); ./libfoo.so.1 size used; possible insufficient data copied copy relocation sizes differ: _size_gets_larger (file prog size=40; file ./libfoo.so.1 size=80); ./prog size used; possible data truncation |
ここで、ldd(1) は、動的実行可能ファイルが、共有オブジェクトが提供しなければならないデータすべてをコピーするけれども、その割り当てスペースで許容できる量しか受け付けないということを知らせています。
位置に依存しないコードだけでアプリケーションを作成すれば、コピー再配置を完全に排除することができます (「位置に依存しないコード」を参照)。
リンカーの -Bsymbolic オプションを使用すると、シンボルの参照を共有オブジェクト内の大域定義に結合できます。このオプションは本来、実行時リンカーそのものを作成するために設計されたもので、長い歴史があるといえます。
オブジェクトのインタフェースを定義し、非公開シンボルをローカルに縮小することは、-Bsymbolic オプションを使用した実行時再配置のコストを削減する望ましい仕組みです (「シンボル範囲の縮小」を参照)。実際に、-Bsymbolic を使用すると直感的にはわからない副産物のできることがあります。
シンボリックに結合されたシンボルが割り込み (interposition) された場合、シンボリックに結合されたオブジェクトの外からのそのシンボルへの参照は、その割り込みに結合します。オブジェクトそのものはすでに内部的に結合されています。本質的に、同じ名前を持つ 2 つのシンボルは、プロセス内から参照されます。シンボリックに結合されたデータシンボルは、コピーを再配置し (「コピー再配置」を参照)、同じ割り込み状態を作成します。
シンボリックに結合された共有オブジェクトは、.dynamic エントリ DT_SYMBOLIC で表されます。このタグは情報を提供するだけで、実行時リンカーは、これらのオブジェクトからのシンボルの検索を他のオブジェクトからの場合と同じ方法で処理します。シンボリック結合はリンカーフェーズで作成されたものと想定されます。
実行時リンカーは、アプリケーションの実行中に処理された共有オブジェクトすべてのプロファイリング情報を生成できます。これが可能となるのは、実行時リンカーが、共有オブジェクトをアプリケーションに結合しなければなないためであり、したがって、実行時リンカーはすべての大域関数割り当てを傍受できます (これらの割り当ては、.plt エントリによって起こります。このメカニズムの詳細は、「再配置が実行されるとき」を参照)。
共有オブジェクトのプロファイルは、環境変数 LD_PROFILE
でその名前を指定することによって有効になります。この環境変数を使用すると、一度に 1 つの共有オブジェクトを解析できます。ただし、環境変数の設定は、1 つまたは複数のアプリケーションによる共有オブジェクトの使用を解析するために使用できます。次の例では、コマンド ls(1) の 1 回の呼び出しによる libc の使用が解析されています。
$ LD_PROFILE=libc.so.1 ls -l |
次の例では、環境変数の設定によって、アプリケーションが libc を使用するたびに、環境変数の設定期間に関する解析情報が蓄積されます。
$ LD_PROFILE=libc.so.1; export LD_PROFILE $ ls -l $ make $ ... |
プロファイリングが有効な場合、プロファイルデータファイルがなければ、ファイルが作成されて、実行時リンカーに割り当てられます。上記の例で、このデータファイルは /var/tmp/libc.so.1.profile です。64 ビットライブラリは、拡張プロファイル形式を必要とし、.profilex 接尾辞を使用して書かれます。代替ディレクトリを指定して、環境変数 LD_PROFILE_OUTPUT
によってプロファイルデータを格納することもできます。
このプロファイルデータファイルは、profil(2) データを保存して、指定の共有オブジェクトの使用に関連するカウント情報を呼び出すために使用されます。このプロファイルデータは、gprof(1) によって直接調べることができます。
gprof(1) は通常、cc(1) の -xpg オプションにを使用してコンパイルされた実行可能ファイルにより作成された gmon.out プロファイルデータを解析するために使用されます。実行時リンカーのプロファイル解析では、このオプションによってコードをコンパイルする必要はありません。依存共有オブジェクトがプロファイルされるアプリケーションは、profil(2) に対して呼び出しを行うことができません。これは、このシステム呼び出しでは、同じプロセス内で複数の呼び出しが行われないためです。同じ理由から、cc(1) の -xpg オプションによって、これらのアプリケーションをコンパイルすることもできません。このコンパイラによって生成されたプロファイリングのメカニズムが profil(2) の上にも構築されるためです。
このプロファイリングメカニズムの最も強力な機能の 1 つに、複数のアプリケーションに使用される共有オブジェクトの解析があります。通常、プロファイリング解析は、1 つまたは 2 つのアプリケーションを使用して実行されます。しかし共有オブジェクトは、その性質上、多数のアプリケーションで使用できます。これらのアプリケーションによる共有オブジェクトの使用方法を解析すると、共有オブジェクトの全体の性能を向上させるには、どこに注意すべきかを理解できます。
次の例は、ソース階層内でいくつかのアプリケーションを作成したときの libc の性能解析を示しています。
$ LD_PROFILE=libc.so.1 ; export LD_PROFILE $ make $ gprof -b /usr/lib/libc.so.1 /var/tmp/libc.so.1.profile ..... granularity: each sample hit covers 4 byte(s) .... called/total parents index %time self descendents called+self name index called/total children ..... ----------------------------------------------- 0.33 0.00 52/29381 _gettxt [96] 1.12 0.00 174/29381 _tzload [54] 10.50 0.00 1634/29381 <external> 16.14 0.00 2512/29381 _opendir [15] 160.65 0.00 25009/29381 _endopen [3] [2] 35.0 188.74 0.00 29381 _open [2] ----------------------------------------------- ..... granularity: each sample hit covers 4 byte(s) .... % cumulative self self total time seconds seconds calls ms/call ms/call name 35.0 188.74 188.74 29381 6.42 6.42 _open [2] 13.0 258.80 70.06 12094 5.79 5.79 _write [4] 9.9 312.32 53.52 34303 1.56 1.56 _read [6] 7.1 350.53 38.21 1177 32.46 32.46 _fork [9] .... |
特殊名 <external> は、プロファイル中の共有オブジェクトのアドレス範囲外からの参照を示しています。したがって、上記の例では、1634 は、動的実行可能ファイル、または プロファイル解析の進行中に libc によって結合された他の共有オブジェクトから発生した libc 内の関数 open(2) を呼び出しています。
共有オブジェクトのプロファイルは、マルチスレッド化に対し安全です。ただし、あるスレッドがプロファイルデータ情報を更新しているときに、もう 1 つのスレッドが fork(2) を呼び出す場合は例外です。fork1(2) を使用すると、この制限はなくなります。