JavaとC++の比較(ジャバとシープラスプラスのひかく)の記事では、JavaC++の比較について説明する。

設計思想 編集

C++とJavaとの違いは、それら言語の歴史から辿ることができる。

C++とJavaは開発の目的が異なるため、両者の方針トレードオフに違いが生じている。

C++ Java
ネイティブコード マネージコード
Cの (部分的な) 上位互換 C/C++との互換性はない
プログラマを信頼する プログラマを守る
ハードウェアに近い低レベル(下位レベル)機能を操作できる メモリを抽象化した型やオブジェクトを通してのみアクセス可能
簡潔な表現 明確な表現
明示的な型破壊を許可 型安全性
マルチパラダイム手続き型オブジェクト指向総称型関数型 オブジェクト指向、総称型、関数型
演算子多重定義 演算子の効果は不変
限られた範囲の標準ライブラリ (GUI、ネットワーク、マルチスレッドを含む)機能豊富で容易に使用できる標準ライブラリ
組み込み、パーソナルコンピュータ、ワークステーション 業務システム(Webフロントエンド、Webバックエンド)、携帯端末

C言語とC++は相互運用性が確保されており、C++からCのライブラリ(関数群)を直接利用することが可能になっている。なお、2023年現在のC/C++最新規格はそれぞれC17英語版/C18英語版およびC++20だが、双方に独自の機能追加と仕様変更が重ねられており、C言語に対する完全な上位互換性はなくなっている。ただし、互いの差異を埋めて互換性を向上するための機能追加もなされている。

また、JavaはC/C++との互換性はないが、ScalaKotlinGroovyといった後発のJava仮想マシン (JVM) ベースのABI互換言語との間で相互運用が可能になっている。

言語の特徴 編集

文法 編集

  • Java文法はシンプルなLALRパーサによって解析できる文脈自由文法である。C++の構文解析は、それよりも複雑である。例えば、Foo<1>(3);は、Fooが変数であれば比較シーケンスであるが、Fooがクラスのテンプレート名であればオブジェクトを生成する。
  • C++では名前空間レベルの定数、変数関数が認められている。Javaでは、宣言はクラスインタフェースの中に書かなければならない。
  • C++のconstは、「論理的に読み取り専用データである」ことを示す。Javaのfinalは、「変数が再び割り当てられない」ことを示す。const intfinal intなど、基本型にとってはこれらは概ね等価であるが、C++では変数および定数の宣言時初期化もしくは定義時初期化が必要であるのに対し、Javaでは初期化が一回だけ実行されるという条件が満たされればよい。また、C++のポインタ自身に対するconst修飾は、Javaの参照に対するfinal修飾と同じ意味を持つ。
  • C++11では、コンパイル時に値が決まる定数式 (constant expression) すなわちリテラルを表すconstexpr修飾子をサポートするようになった[1]。Javaでは通例static finalフィールドで定数を定義する。
C++ Java
const int n = 100; // 値型。
n = 0; // 誤り。
struct Rectangle { int x, y, width, height; };
Rectangle *const p = new Rectangle(); // ポインタ自身を変更不能とする。
p = new Rectangle(); // 誤り。
p->x = 5; // 正しい。p は依然として同じ Rectangle を参照している。
delete p;
const Rectangle* p = new Rectangle(); // ポインタの参照先を変更不能とする。
delete p;
p = new Rectangle(); // 正しい。
p->x = 5; // 誤り。
const Rectangle& r = *p;
r = Rectangle(); // 誤り。
r.x = 5; // 誤り。
delete p;
const Rectangle r = Rectangle(); // 値型。
r = Rectangle(); // 誤り。
r.x = 5; // 誤り。r は Rectangle 型の定数。
final int n = 100; // プリミティブ型。
n = 0; // 誤り。
class Rectangle { int x, y, width, height; };
final Rectangle r = new Rectangle(); // 参照型。
r = new Rectangle(); // 誤り。
r.x = 5; // 正しい。r は依然として同じ Rectangle を参照している。
final int n;
final Rectangle r;
n = 100; // 正しい。
r = new Rectangle(); // 正しい。
  • C++においてconst修飾されたメンバ関数は、関数内でメンバ変数を変更することができなくなる(mutable指定された変数を除く)。Javaには相当機能はない。
  • Javaにおいてfinal修飾されたメソッドは派生クラスでオーバーライドできなくなる。また、final修飾されたクラスは派生クラスを定義できなくなる。なお、C++11ではJavaのようにメンバ関数のオーバーライドや派生クラスの定義を禁止するfinal修飾子が追加された。
  • C++はgoto文をサポートする。Javaはサポートしないが、ラベル付break文ラベル付continue文で、構造上ややgotoライクな機能を提供する。実際には、Javaは、コードを読みやすくするため、構造化制御フローを強要する。
  • C++はJavaが持たないやや低レベルな機能を提供する。C++には、特有のメモリ記憶位置や低レベルオペレーティングシステムコンポーネントを書くために必要なタスクを操るのに役立つポインタがある。同様にして、多くのC++コンパイラはインラインアセンブラをサポートする。Javaでは、そのようなコードは全て外部ライブラリに配置し、Java Native Interface (JNI) を通してアクセスしなければならない。そのため、呼び出しのたびに、大きなオーバーヘッドが発生する。

意味論 編集

  • C++はCとの互換性を保つため組込型の暗黙な型変換をある程度許可しているが、大抵のコンパイラでは警告を出す。また、複合型の暗黙型変換も定義できる。一方、Javaではプリミティブ型の暗黙的な変換として数値型の精度が大きくなる型変換(拡張変換)のみを許可する。数値型の精度が低下する暗黙的な型変換(縮小変換)は許可されず、明示的なキャストを要求する。
    • この影響はJavaとC++双方にある条件式(ifwhileforの脱出条件)にも現れる。C++では条件式の型は厳密にboolである必要はなく、文脈的に変換可能であればよい[2]。Cとの互換性から、C++のbool整数型の一種であり、他の整数型や浮動小数点数型、ポインタなどから暗黙変換することができ、ゼロあるいはゼロに相当する値の場合はfalseに、それ以外はtrueに変換され、またこの条件式の文脈における暗黙変換はコンパイラ警告の対象にはならない。一方、Javaでは条件式の型はbooleanである必要があるが、booleanは整数型ではなく、数値型からの暗黙的な型変換ができない。単純なキャスト構文による型変換もできず、booleanに変換するためには何らかの比較演算子による明示的な比較が必要となる[3]。そのため、数値型の変数aに対してif (a = 5)のようなコードは、Javaではコンパイルエラーになる。これは、if (a == 5)ミスタイプに対する対策となるが、C/C++で変数xが非ゼロまたはnon-nullであることをチェックするif (x)のようなコードをJavaへ移植する際には、0またはnullとの明示的な比較が必要になる。
  • 関数の引数を渡すとき、C++は参照渡し値渡し両方をサポートする。Javaではすべての引数は値渡しであるが、オブジェクト(非プリミティブ変数)の引数は参照になり、これは間接参照が言語に備わっていることを意味する。
  • Javaのプリミティブ型は大きさと値の範囲が指定されている。一方、C++の組み込み型は最小限度が定められているものの、正確な大きさは定められておらず、環境によって異なる。また同じコンパイラでもバージョンが違えば型の大きさが異なることもあるし、コンパイラの設定で変更可能な場合もある。C++で値の範囲が指定されていないとは、仮に同じ16ビットの符号付き整数だとしても、ある環境では2の補数で[-32768, 32767]、別の環境では1の補数で[-32767, 32767]の範囲になるという例があるということである。C++11ではサイズと内部表現が規定された整数型(std::int32_tなど)がオプションとして追加された。C++20では符号付き整数が2の補数であることが規定された[4]
    • Javaの文字型charは、内部表現がUTF-16であることが規定されている。文字列を表現するクラスとして、java.lang.Stringが用意されている。一方、C++の文字型にはcharwchar_tワイド文字)の2種類が存在し、それぞれの文字列クラスとしてstd::stringおよびstd::wstringが定義されているが、文字型の幅(ビット数)や内部表現はプラットフォームに依存する。規定されているのはcharのサイズが1バイト (sizeof(char) == 1) ということのみである。さらに、1バイトのビット数が規定されておらず、8ビット以上であればよい、という制約しかない。例えば、charシングルバイト文字であったとしても、ASCIIであるとはかぎらないし、マルチバイト文字であったとしても、日本語環境でShift_JISあるいはEUC-JPが使われたり、中国語環境でGB 2312あるいはBig5が使われたり、もしくはUTF-8であったりしてもかまわない。また、wchar_tの内部表現は、UTF-16もしくはUTF-32であってもかまわないし、あるいはUnicodeと無関係の内部表現であってもかまわない。C++11ではUTF-16/UTF-32対応の文字型char16_t/char32_tと文字列クラスstd::u16string/std::u32stringが追加された[注釈 1]
    • Javaの文字列リテラルはString型のオブジェクトである。C++の文字列リテラルはヌル終端文字列をぴったり格納する固定長配列型である。
  • C++の浮動小数点数の丸め誤差と精度と演算はプラットフォームに依存する。Javaは異なるプラットフォームでの一貫した結果を保証する高精度浮動小数点モデルを提供しているが、通常は最適な浮動小数点演算性能を得るためにより大雑把な演算モードが使われる。
  • C++ではポインタを使ってメモリアドレスを直接操作できる。Javaはメモリアドレスを直接操作できるポインタを持っていない(オブジェクトの参照と配列参照だけはポインタを持っているが、どちらもメモリアドレスの直接アクセスを許可しない)。C++ではポインタへのポインタを定義できるが、Javaではオブジェクトアクセスにだけ参照を用いる。
  • どちらの言語も配列は固定の長さをもつ。C++の配列はメモリ空間上のオブジェクトの連続であり、配列自身をオブジェクトのように扱うことはできない(第一級オブジェクトではない)。通例、C言語のように配列先頭要素へのポインタを用いて配列の受け渡しを行うが、C++ではサイズの指定された固定長配列への参照を定義することもできる。配列範囲外アクセスはチェックされない。なお、構造体あるいはクラスでラップするか、C++11で追加された標準ライブラリに含まれるstd::arrayを用いることで、固定長配列を第一級オブジェクトとして扱うことができる。Javaの配列は第一級オブジェクトであるが、配列の一部分のみを参照するサブ配列は定義できず、インデックス範囲を別途指定する必要がある。また、配列範囲外アクセスのチェックが強制される。
  • C++では関数へのポインタ(あるいは関数への参照)によって、関数あるいはメンバ関数を間接参照することができる。また、ポインタによって関数オブジェクトを指す方法もある。Java 7以前では代替方法としてインタフェースを利用するしかなかったが、Java 8ではメソッド参照の機能が追加された。
  • C++ではプログラマが演算子を多重定義できる(利用者定義演算子)。Javaは演算子多重定義をサポートしない。文字列型java.lang.Stringに対してのみ、文字列連結が可能な演算子++=が予め用意されているだけである。
  • Javaはリフレクションや任意に新しいコードを動的ロードする機能をサポートする標準APIを持つ。
  • Javaは1.5以降でジェネリクスをサポートする。ただし型引数に使えるのは参照型のみであり、intのようなプリミティブ型は使用できないため、java.lang.Integerのようなプリミティブラッパークラスボックス化する必要がある。C++はテンプレートによりジェネリックプログラミングをサポートする。名前解決が失敗しない限り、型引数にはいかなる型でも使えるため、静的ダックタイピングに応用できる強力な柔軟性を持つが、適合しなかった場合のコンパイルエラーメッセージが膨大・難解になる欠点がある。この点に関してはC++20の「コンセプト」機能によってある程度の解消が図られている[6]
  • JavaとC++はいずれも、ネイティブ型(これらも"基本型"または"組込型"として知られる)とユーザー定義型(これも"複合型"として知られる)を区別する。Javaでは、ネイティブ型は値としての意味しかなく、複合型は参照としての意味だけを持つ。C++では、すべての型が値としての意味を持つが、任意の型の参照を作ることが可能である。
  • C++は任意のクラスの多重継承をサポートする。また、状態の多重継承にまつわる問題を回避するための機構として、仮想継承をサポートする。Javaは型の多重継承をインタフェースによりサポートするが、実装は単一継承のみをサポートする。Javaでは、サブクラスはただ1つのスーパークラスからのみ派生することができるが、クラスは複数のインタフェースを実装することができる。なお、Java 8ではインタフェースのデフォルトメソッドにより、実装の多重継承をサポートするようになったが、状態の多重継承は依然としてサポートしない。
  • Javaはインタフェースとクラスを明確に区別している。C++では、純粋仮想関数(抽象メソッドにあたる)を並べたクラスでインタフェースを表現し、複数のインタフェースの実装は多重継承によって模倣される。
  • Javaはマルチスレッドをサポートした標準ライブラリと言語機能を持つ。Javaのキーワードsynchronizedはマルチスレッドアプリケーションをサポートするシンプルでセキュアな相互排他ロック(mutex)を提供するが、synchronized セクションはLIFOオーダーで残されなければならない。Java 1.5では並行プログラミングのためのライブラリが強化され、java.util.concurrent.Semaphoreクラスをはじめとする、柔軟なMutexロックメカニズムを提供するための明示的な同期オブジェクトなどもサポートするようになった。一方、C++では長らくスレッドが標準化されていなかったため、プラットフォームごとのスレッド機能を使うか、Boost C++ライブラリなどを利用する必要があったが、C++11でスレッドが標準化された。

リソース管理 編集

  • Javaは自動メモリ管理のためのガベージコレクション (GC) を必要とする。GCの具体的なアルゴリズムや実装形態はJVMに左右されるが、マーク・アンド・スイープをベースに通例世代別ガベージコレクションが採用されている[7][8]。このため、循環参照によるメモリリークは原理的に発生しないが、オブジェクトが実際に破棄されるタイミングは非決定論的である。C++のメモリ管理は普通、手動で行われるか、ライブラリによって実装されたスマートポインタを通して行われる。C++でガベージコレクションを実装することも不可能ではないが、標準規格で要求されているわけではなく、実際には滅多に使われない。また、C++ではオブジェクトを再配置できないが、一般にオブジェクトの再配置が可能であれば、明示に解放するスタイルより空間と時間効率が良くなることが知られている。
    • C++のスマートポインタのうち、複数個所でインスタンスが共有されるもの(共有ポインタ)に関しては主に参照カウントが用いられる。その場合、循環参照の注意が必要という欠点がある。Boost C++ライブラリboost::shared_ptr/boost::scoped_ptrおよびC++11で追加された標準ライブラリのstd::shared_ptr/std::unique_ptrを筆頭に、様々なサードパーティ製のライブラリでもスマートポインタが実装されている。
  • メモリ割り当ては、C++では任意に可能だが、Javaはオブジェクトインスタンス化を通してのみ可能である。Javaでは、バイト配列を作ることで任意のブロック割り当てを再現できる。もっとも、Javaの配列もオブジェクトである。
  • JavaとC++はリソース管理に異なる手法を用いる。Javaは主にガベージコレクションに頼り、メモリの再利用だけができ、他のリソースは最後まで回収されないかもしれない。だが、C++は主にRAII (Resource Acquisition Is Initialization) というイディオムに頼る。これは2つの言語間の以下のような様々な違いに現れている。
    • C++では、自動変数(ローカル変数)は構造体やクラスのような複合型であっても組込型同様スタックに割り当てられ、変数の属するブロックを抜けるときに破棄される。巨大な固定長配列を内包する複合型を定義することもできるが、実行時にスタックオーバーフローを引き起こす可能性があるため、通例malloc関数やnew演算子などによる動的メモリ確保を使ってヒープに割り当てる。Javaでは、ローカル変数はスタックに割り当てられるものの、クラスや配列といった複合型(参照型)のオブジェクト本体は常にヒープに割り当てられ、ガベージコレクタによって回収される(これはあくまで概念上の話で、実装上はエスケープ解析最適化でスタックにオブジェクトを割り当てる仮想マシンもある[9])。
    • C++はデストラクタを持っているが、Javaはファイナライザを持っている。双方はオブジェクトの解放時に優先的に呼び出されるが、それらは重大性が異なる。
      • C++オブジェクトのデストラクタは、オブジェクトが解放されるときに必ず呼び出される(ようにコンパイラは実装しなければならない)。例外が投げられたときでも、スタック上のオブジェクトは、スタックの巻き戻し(アンワインド)に伴ってデストラクタが呼ばれる。また、デストラクタは定義さえすれば、利用する側に特別な記述は不要である。このことを利用して確実なリソースの解放を行うのが広義のRAIIである。
      • Javaでは、オブジェクト解放はガベージコレクタによって暗黙のうちに実行される。Javaオブジェクトのファイナライザは、最後にアクセスされた後と、実際に解放される前に、ときどき非同期に呼び出されるが、決して何も起こらないかも知れない。ファイナライザを要求するオブジェクトはごくわずかにすぎない。ファイナライザは解放状態を優先するオブジェクトの多少のクリーンナップを保証しなければならないオブジェクトによって要求されるだけであり、だいたいはJVM外のリソースへ放出される。Javaでは安全な同期によるリソース解放は、try/finally文を構築して明示的に行われなければならない。ファイナライザの動作タイミングはGCに左右され、非決定論的であり、また誤った使い方をするとトラブルの原因になるため、Java 9以降はファイナライザの使用が非推奨となっている[10]
    • C++では、時としてdangling pointer(破棄されたオブジェクトを参照するポインタ)が存在してしまう。dangling pointerを間接参照するときは、基本的にプログラムのバグである。Javaでは、ガベージコレクタは参照されているオブジェクトは解放しない。
    • C++では初期化されていないプリミティブなオブジェクトを持つことが可能である。Javaでは、初期化が強制される。
    • C++では、領域を割り当てられたが到達不能であるオブジェクトが発生してしまうことがある。到達不能オブジェクトとは、それへの到達可能な参照が全く存在しないオブジェクトのことである。到達不能オブジェクトは解放(破棄)することができず、メモリリークを引き起こす。それとは対照的に、Javaではオブジェクトは、それがユーザープログラムによって到達不可能になる「までに」ガベージコレクタによって解放される (注: 異なる到達可能性の「強さ」を考慮に入れた、Javaのガベージコレクタとともに働く、「弱い参照」がサポートされている)。
    • Javaでは非メモリリソースをリークしないように注意深く解放コードを逐一書く必要がある(ただしJava 7のtry-with-resources文により、ある程度緩和されている)。一方でC++では、前述のRAIIによってリークしにくい例外安全なコードを書きやすくなっている。

ライブラリ 編集

JavaはC++と比べ標準ライブラリの提供する範囲が広い。標準C++ライブラリ文字列コンテナIOストリームのような比較的一般的な目的のコンポーネントだけを提供する。Java標準ライブラリネットワーキンググラフィカルユーザインタフェースXML処理、ロギングデータベースアクセス、暗号化やそのほか様々な領域のコンポーネントを含む。このような機能は、C++ではサードパーティー製のライブラリやOS固有のAPIによって実現されていることが多いが、どんな環境でも用意されているとは限らない。

C++はCに対する部分的な上位互換性を持つため、(多くのオペレーティングシステムAPIのような)Cライブラリも直接使用できる。Javaでは、そのような環境固有のライブラリで提供される機能の多くが、クロスプラットフォームでリッチな標準ライブラリで提供される。その一方で、Javaからネイティブなオペレーティングシステムやハードウェア機能に直接アクセスするには、Java Native Interface (JNI) を使用する必要がある。

拡張命令のサポート 編集

x86SSEやAVX、ARMのNEONのようなCPU固有のSIMD拡張命令を使ってベクトル化する場合、C++ではコンパイラがサポートする組み込み関数(intrinsics)を使ったり、OpenMPディレクティブによるベクトル化ヒントを挿入したり、コンパイラの最適化に任せたり、といった方法がある。ただしコンパイル時の静的なコード生成の場合、実行環境でサポートされていない命令に到達するとアプリケーションがクラッシュしてしまうため、実行環境に応じて動的分岐するディスパッチ処理が必要となる。また、フォールバック処理の数に応じてバイナリも肥大化してしまう。

JavaではそのようなCPU固有の拡張命令や最適化ヒントを直接ソースコード中に記述することはできず、JNIを利用するしかないが、JITコンパイルによって実行環境に合わせた最適化も可能であるため、VMが対応している命令セットであれば実行環境がサポートする最上位の高速な命令セットを使用して最適化を図ることもできる[11][12]。JIT最適化を利用する場合は実行環境に応じたバイナリを事前に用意する必要はない。ただし最適化の度合いはJITコンパイラの性能に左右される。

ランタイム 編集

  • C++は通常、機械語に直接コンパイルされてから、オペレーティングシステムおよびCPUによって直接実行される。Javaは通常バイトコードにコンパイルされてからJava仮想マシン (JVM) がインタプリタでバイトコードを解釈するか、またはJITがバイトコードをマシンコードにコンパイルしつつ実行される。理論上、動的再コンパイルはどの言語でも使うことができる(しかしJavaのほうが向いている)が、現在のところ、どちらの言語でも動的に再コンパイルされることは稀である。
  • 強制によらない表現力のため、C++の多くのエラー要因(範囲外チェックされない配列アクセス、未使用ポインタ、型の不一致など)はコンパイル時または実行時の不適当なオーバーヘッド無しに信頼できるチェックを行えない。このため、低レベルバッファオーバフローページフォールトセグメンテーションフォルトを導いてしまう。標準やサードパーティーのライブラリがそのようなエラーを避けることを助ける高水準な(動的配列リストマップのような)抽象概念を提供している。一方、Javaではそのようなエラーは単純に起こすことも、JVMに検出されることも無く、例外によってアプリケーションに報告される。
  • Javaは、配列アクセスの境界チェックを行い、そして領域外にアクセスすることが判明したときに明確な振る舞い(例外の送出)を要求する。これにより、一般に実行が低速になる代わりに不安定さの源が除去される。ただし、コンパイラ解析で不必要な境界チェックが消去される場合もある。C++はネイティブな配列の配列外アクセスの振る舞いを要求しないため、通常は境界チェックしないのが一般的である。ただしstd::vectorのようなC++標準ライブラリでは、at()メンバ関数の使用という形で、境界チェック付きアクセスを任意に選択できる。要約すると、Javaの配列は「常に安全で、厳しく強いられる、可能な限り高速」だがC++のネイティブ配列は「常に高速、完全に強制されない、潜在的に危険」ということである。

その他 編集

  • JavaとC++では多くのソースファイルでコードを分割するために異なる方法を使用する。Javaはすべてのプログラム定義でファイル名とパスが影響するパッケージシステムを使用する。Javaでは、コンパイラは実行可能クラスファイルをインポートする。C++はソースファイル間で宣言を分割するヘッダファイルソースコード包含システムおよび名前空間を使用する。(参考: importとincludeの比較
  • コンパイルされたJavaバイトコードファイルは通常、C++コンパイラの出力する機械語のコードファイルよりも小さい。第一に、Javaバイトコードは通常、ネイティブな機械語よりもコンパクトである。第二に、C++のテンプレートやマクロが類似コードの重複を発生させやすいことが挙げられる。第三に、Javaは常に標準ライブラリを動的リンクするため、標準ライブラリのコードを出力に含まないということが挙げられる。反面、Javaバイトコードを翻訳実行する環境(JVMやJIT)が要求される。
  • C++コンパイラは、Javaにはない、言葉通りのプリプロセッシング(前処理)の段階があることが特徴的である。これを用いるために、Javaユーザーの中には、ビルドプロセスにプリプロセッサを付加する者がいる。
  • 双方の言語は、配列が固定サイズである。Javaでは、配列は第一級オブジェクトであるが、C++では配列はベースとなるオブジェクトの連続した領域であり、最初の要素と随意的な配列の長さをポインタを使って参照しているに過ぎない。Javaでは、配列は境界チェックされ、長さもわかっているが、C++では配列を連続した領域として扱うだけである。C++とJava双方は、リサイズ、サイズ保存できるコンテナクラス (それぞれ、std::vectorjava.util.Vectorまたはjava.util.ArrayList) を提供している。
  • Javaの除算と剰余演算子は0を切り捨てるよう正しく定義されている。C++は、これらの演算子が0を切り捨てるか「マイナス無限大に切り捨てる」かを明確に指定しない。Javaでは、-3 / 2は常に-1となる。一方、C++ではプラットフォームに依存し、-1を返すかも知れないし-2を返すかも知れない。C99およびC++11はJavaと同じように除算の仕様を定義しており、すべてのaとb (b != 0) で(a/b)*b + (a%b) == aを保証する。古いC/C++規格にこの保証がない理由は、仮にCPUの除算命令がこのような定義でなかったとしても、C/C++の除算を直接CPUの除算命令にコンパイルできるようにするためである。ただし、古いC/C++でも標準ライブラリのdiv()関数 (<stdlib.h>/<cstdlib>) を用いれば常に-3 / 2の商として-1という結果を得られる。

パフォーマンス 編集

このセクションでは、Microsoft WindowsLinuxのような一般的なOSでのC++とJava相互の演算パフォーマンスを比較する。

Javaの初期バージョンは、C++のような静的コンパイルされる言語に比べ著しく性能が低かった。これは、C++ではソースコードはハードウェアが直接に解釈できる機械語にコンパイルされるのに対し、Javaでは共通の(ハードウェアに依存しない)仮想機械語であるJavaバイトコードにコンパイルされ、それをJava仮想マシンがインタプリタ的に実行していたからである。例として、

Java/C++構文 C++コンパイラが生成したx86機械語コード Javaコンパイラが生成したバイトコード
vector[i]++;
mov edx,[ebp+4h]
mov eax,[ebp+1Ch]
inc dword ptr [edx+eax*4]
aload_1
iload_2
dup2
iaload
iconst_1
iadd
iastore

のように、ソースコードレベルでは同様な命令でも、Javaバイトコードの方がネイティブな機械語よりも長くなる傾向にある (C++によるコードでは、3行目でロード、インクリメント、保存が1命令で行われており、短く済んでいる)。

しかし、Javaは長期稼働するサーバやデスクトップのためにジャストインタイム (JIT) コンパイラテクノロジを発展させ、それがC++との性能差を縮めるであろうと言われている。JITコンパイルとは、Javaバイトコードをインタプリタ的に実行するのではなく、実行時にネイティブな機械語にコンパイルしてから実行する方式である。

以下は、JavaはC++よりも高速であるという研究[13]の主張の一部である。

  • CおよびC++では、「なんでも指せる」というポインタの性質が最適化を難しくしている(ただしこの問題はC99のrestrictキーワードによって回避できるケースもある)。
  • Javaでは新たに確保されたメモリがガベージコレクションによって物理的に連続した領域に集められるので、アクセスする際にキャッシュミスが起こりにくい。
  • 実行時コンパイルはそれがどのプロセッサの上で実行されるか、どのコードを実行するかが解っているため、各CPUに特化したコードを生成したり、高い分岐予測精度を実現したりできる(ホットスポット)。

一般的に、Javaは、メモリ確保やファイルI/Oのような演算においてはC++より性能がよいが、算術演算や三角関数計算ではC++の方が優れた徴候を示す[14]。数値演算について述べると、Javaは新しいバージョン[どれ?]で大きく進歩しているものの、浮動小数点数を様々なプラットフォームで再現するためのオーバーヘッド等により、未だに[いつ?]C++やFortranより遅い[15]

脚注 編集

注釈 編集

  1. ^ C++17までは、これらの型のリテラルのエンコードは実装定義であり、UTF-16/UTF-32であることがすべての処理系で保証されていなかったが、C++20では改めてUTF-16/UTF-32に規定された[5]

出典 編集

外部リンク 編集