「Hello、World!」を出力する.cファイルをコンパイルして取得した.oファイルの理由を知りたいのですが。「Hello、World!」も出力するJava .classファイルよりも大きいですか?
9 に答える
Javaはバイトコードを使用してプラットフォームに依存せず「プリコンパイル」されますが、バイトコードはインタープリターによって使用され、十分にコンパクトになるため、コンパイルされたCプログラムで表示されるマシンコードとは異なります。Javaコンパイルの全プロセスを見てください。
Java program
-> Bytecode
-> High-level Intermediate Representation (HIR)
-> Middle-level Intermediate Representation (MIR)
-> Low-level Intermediate Representation (LIR)
-> Register allocation
-> EMIT (Machine Code)
これは、Javaプログラムからマシンコードへの変換のチェーンです。ご覧のとおり、バイトコードはマシンコードから遠く離れています。インターネットでは、実際のプログラムでこの道を示す良いものを見つけることができません(例)。私が見つけたのはこのプレゼンテーションだけです。ここでは、各ステップでコードのプレゼンテーションがどのように変わるかを確認できます。コンパイルされたcプログラムとJavaバイトコードがどのようにそしてなぜ異なるのかがあなたに答えられることを願っています。
更新: 「バイトコード」の後のすべてのステップは、そのコードをコンパイルする決定に応じて、実行時にJVMによって実行されます(これは別の話です... JVMはバイトコードの解釈とネイティブプラットフォームに依存するコードへのコンパイルの間でバランスを取っています)
最後に、JavaHotSpot™クライアントコンパイラの線形スキャンレジスタ割り当てから取得した良い例が見つかりました(JVM内で何が起こっているかを理解するための良い読み物です)。Javaプログラムがあると想像してください。
public static void fibonacci() {
int lo = 0;
int hi = 1;
while (hi < 10000) {
hi = hi + lo;
lo = hi - lo;
print(lo);
}
}
その場合、そのバイトコードは次のとおりです。
0: iconst_0
1: istore_0 // lo = 0
2: iconst_1
3: istore_1 // hi = 1
4: iload_1
5: sipush 10000
8: if_icmpge 26 // while (hi < 10000)
11: iload_1
12: iload_0
13: iadd
14: istore_1 // hi = hi + lo
15: iload_1
16: iload_0
17: isub
18: istore_0 // lo = hi - lo
19: iload_0
20: invokestatic #12 // print(lo)
23: goto 4 // end of while-loop
26: return
各コマンドは1バイト(JVMは256コマンドをサポートしますが、実際にはその数より少ない)+引数を取ります。一緒にそれは27バイトかかります。すべてのステージを省略し、マシンコードを実行する準備が整いました。
00000000: mov dword ptr [esp-3000h], eax
00000007: push ebp
00000008: mov ebp, esp
0000000a: sub esp, 18h
0000000d: mov esi, 1h
00000012: mov edi, 0h
00000017: nop
00000018: cmp esi, 2710h
0000001e: jge 00000049
00000024: add esi, edi
00000026: mov ebx, esi
00000028: sub ebx, edi
0000002a: mov dword ptr [esp], ebx
0000002d: mov dword ptr [ebp-8h], ebx
00000030: mov dword ptr [ebp-4h], esi
00000033: call 00a50d40
00000038: mov esi, dword ptr [ebp-4h]
0000003b: mov edi, dword ptr [ebp-8h]
0000003e: test dword ptr [370000h], eax
00000044: jmp 00000018
00000049: mov esp, ebp
0000004b: pop ebp
0000004c: test dword ptr [370000h], eax
00000052: ret
結果として83(16進数で52 + 1バイト)バイトかかります。
PS。リンク(他の人が言及した)や、compiledcおよびbytecodeファイルヘッダー(おそらくそれらも異なります。cとの関係はわかりませんが、bytecodeファイルではすべての文字列がに移動されます)は考慮されていません。特別なヘッダープール、およびプログラムではヘッダーなどでその「位置」が使用されます)
UPDATE2:おそらく言及する価値がありますが、x86および他のほとんどのプラットフォームに基づくマシンコードはレジスターで機能しますが、Javaはスタック(istore / iloadコマンド)で機能します。ご覧のとおり、マシンコードはレジスタで「いっぱい」であり、より単純なスタックベースのバイトコードと比較して、コンパイルされたプログラムに余分なサイズを与えます。
この場合のサイズの違いの主な原因は、ファイル形式の違いです。ELF()ファイルのこのような小さなプログラム形式では.o
、スペースの面で深刻なオーバーヘッドが発生します。
たとえば.o
、「Hello、world」プログラムのサンプルファイルは864バイトを使用します。それは(readelf
コマンドで探索)で構成されています:
- 52バイトのファイルヘッダー
- 440バイトのセクションヘッダー(40バイトx 11セクション)
- 81バイトのセクション名
- 160バイトのシンボルテーブル
- 43バイトのコード
- 14バイトのデータ(
Hello, world\n\0
) - 等
.class
同様のプログラムのファイルは、より多くのシンボル名を含み、これらの名前が長いにもかかわらず、415バイトしかかかりません。これは、( Javaクラスビューアで探索)で構成されています。
- 289バイトの定数プール(定数、シンボル名などを含む)
- 94バイトのメソッドテーブル(コード)
- 8バイトの属性テーブル(ソースファイル名参照)
- 24バイトの固定サイズのヘッダー
参照:
Cプログラムは、プロセッサ上で実行されるネイティブマシンコードにコンパイルされていても(もちろん、OSを介してディスパッチされます)、オペレーティングシステムの多くのセットアップと破棄を行う必要があり、動的にリンクされます。 Cライブラリなどのライブラリ。
一方、Javaは、仮想プラットフォーム(基本的にはコンピューター内のシミュレートされたコンピューター)のバイトコードにコンパイルされます。これは、Java自体と一緒に特別に設計されているため、このオーバーヘッドの多くは(両方の理由で必要になる場合でも)コードとVMインターフェイスは明確に定義されています)VM自体に移動して、プログラムコードを無駄のないものにすることができます。
ただし、コンパイラごとに異なり、それを減らすか、コードを異なる方法でビルドするためのいくつかのオプションがあり、異なる効果があります。
とはいえ、それはそれほど重要ではありません。
クラスファイルはJavaバイトコードです。
C / C ++ライブラリとオペレーティングシステムライブラリは、C ++コンパイラが最終的に実行可能バイナリを作成するために生成するオブジェクトコードにリンクされているため、おそらくこれよりも小さくなります。
簡単に言えば、バイナリを作成するためにリンクされる前に、JavaバイトコードをCコンパイラによって生成されたオブジェクトコードと比較するようなものです。違いは、JVMがJavaバイトコードを解釈してプログラムの目的を適切に実行するのに対し、Cはオペレーティングシステムがインタープリターとして機能するため、オペレーティングシステムからの情報を必要とするという事実です。
また、Cでは、オブジェクトファイルの1つで外部ライブラリから少なくとも1回参照するすべてのシンボル(関数など)がインポートされます。複数のオブジェクトファイルで使用している場合でも、インポートされるのは1回だけです。この「インポート」が発生する可能性がある2つの方法があります。静的リンクを使用すると、関数の実際のコードが実行可能ファイルにコピーされます。これによりファイルサイズが大きくなりますが、外部ライブラリ(.dll / .soファイル)が不要であるという利点があります。ダイナミックリンクではこれは起こりませんが、その結果、プログラムを実行するには追加のライブラリが必要になります。
Javaでは、いわばすべてが動的に「リンク」されます。
つまり、JavaプログラムはJavaバイトコードにコンパイルされるため、別のインタプリタ(Java仮想マシン)を実行する必要があります。
c-compilerによって生成された.oファイルがJavaコンパイラによって生成された.classファイルよりも小さいという100%の保証はありません。それはすべてコンパイラの実装に依存します。
.o
ファイルのサイズとファイルの違いの主な理由の1つは.class
、Javaバイトコードがマシン命令よりも少し高レベルであるということです。もちろん、それほど高レベルではありませんが、それでもかなり低レベルのものですが、プログラム全体を効果的に圧縮するように機能するため、違いが生じます。(CコードとJavaコードの両方にスタートアップコードを含めることができます。)
もう1つの違いは、Javaクラスファイルが比較的小さな機能を表すことが多いことです。さらに小さな部分にマップするCオブジェクトファイルを持つことは可能ですが、多くの場合、より多くの(関連する)機能を1つのファイルに入れるのが一般的です。スコーピングルールの違いは、これを強調するようにも機能します(Cには、モジュールレベルのスコープに対応するものは実際にはありませんが、代わりにファイルレベルのスコープがあります。Javaのパッケージスコープは複数のクラスファイルで機能します)。プログラム全体のサイズを比較すると、より良いメトリックが得られます。
「リンクされた」サイズに関しては、Java実行可能JARファイルは圧縮されて配信されるため、(特定のレベルの機能に対して)小さくなる傾向があります。Cプログラムを圧縮形式で提供することは比較的まれです。(標準ライブラリのサイズにも違いがありますが、Cプログラムはlibc以外のライブラリが存在することを期待でき、Javaプログラムは巨大な標準ライブラリにアクセスできるため、それらは無駄になる可能性があります。ぎこちないです。)
次に、情報のデバッグの問題もあります。特に、IOを実行するデバッグを使用してCプログラムをコンパイルすると、フィルターで除外するには少し扱いにくいという理由だけで、含まれている標準ライブラリの型に関する多くの情報を取得できます。Javaコードには、オブジェクトファイルで利用可能な関連情報を信頼できるため、実際にコンパイルされたコードに関するデバッグ情報のみが含まれます。これにより、コードの実際のサイズが変更されますか?いいえ。ただし、ファイルサイズに大きな影響を与える可能性があります。
全体として、CプログラムとJavaプログラムのサイズを比較するのは難しいと思います。むしろ、それらを比較して、あまり役に立たないことを簡単に学ぶことができます。
ELF形式のファイルのほとんど(単純な関数の場合は90%).o
はジャンクです。.o
空の関数本体を1つ含むファイルの場合、次のようなサイズの内訳が予想されます。
- 1%コード
- 9%のシンボルと再配置テーブル(リンクに必須)
- 90%のヘッダーオーバーヘッド、コンパイラやアセンブラによって保存された役に立たないバージョン/ベンダーノートなど。
コンパイルされたCコードの実際のサイズを確認するには、size
コマンドを使用します。
Javaはマシンに依存しない言語にコンパイルされます。これは、コンパイル後、実行時にJava仮想マシン(JVM)によって変換されることを意味します。Cはマシン命令にコンパイルされるため、プログラムがターゲットマシンで実行するためのすべてのバイナリです。
Javaはマシンに依存しない言語にコンパイルされるため、特定のマシンの特定の詳細はJVMによって処理されます。(つまり、Cにはマシン固有のオーバーヘッドがあります)
それはとにかく私がそれについて考える方法です:-)
いくつかの考えられる理由:
- Javaクラスファイルには、初期化コードはまったく含まれていません。それはあなたの1つのクラスと1つの関数を持っているだけです-確かに非常に小さいです。それに比べて、Cプログラムには、静的にリンクされた初期化コードがある程度あり、DLLサンクが含まれている可能性があります。
- Cプログラムには、ページ境界に位置合わせされたセクションがある場合もあります。これにより、コードセグメントがページ境界で開始されるようにするために、プログラムサイズに最低4kbが追加されます。