1. ホーム
  2. c#

[解決済み] C#はC++より本当に遅いのか?

2023-06-19 23:57:47

質問

この問題について、以前から疑問に思っていました。

もちろん、C#には速度に最適化されていないものがあるので、それらのオブジェクトや言語調整(LinQなど)を使用すると、コードが遅くなることがあります。

しかし、それらの微調整を一切使用せず、ただC#とC++の同じコードピースを比較した場合(一方を他方に翻訳するのは簡単です)。本当にそんなに遅くなるのでしょうか?

理論的には、JIT コンパイラーはリアルタイムでコードを最適化し、より良い結果を得るはずだからです。

マネージドかアンマネージドか?

JITコンパイラはリアルタイムでコードをコンパイルしますが、それは1回のオーバーヘッドであり、同じコードを(一度到達してコンパイルした)実行時に再びコンパイルする必要はないことを覚えておく必要があります。

GCも、何千ものオブジェクトを作成したり破棄したりしない限り(StringBuilderの代わりにStringを使うような場合)、それほど多くのオーバーヘッドを追加するものではありません。そして、C++でそれを行うことはまたコストがかかるでしょう。

もうひとつのポイントは、.Net で導入された DLL 間のより良いコミュニケーションです。.Net プラットフォームは、Managed COM ベースの DLL よりもはるかに優れた通信を行います。

私は、言語が遅くなるべき固有の理由はないと思っていますし、C# が C++ よりも遅いとはあまり思っていません (経験からも、良い説明がないことからも)...。

では、C# で書かれた同じコードの一部は、C++ で書かれた同じコードより遅くなるのでしょうか?

もしそうなら、なぜですか?

他のいくつかの参照(それについて少し話すが、WHYについての説明なし)。

C++ より遅いなら、なぜ C# を使いたいのですか?

どのように解決するのですか?

警告 あなたが質問した内容は実に複雑で、おそらくあなたが思っているよりもずっと複雑です。そのため、これは 本当に 長い回答になります。

純粋に理論的な観点からは、これに対する単純な答えがあるでしょう: C# には、C++ と同等の速度になることを本当に妨げるものは (おそらく) 何もないのです。しかし、理論とは裏腹に、現実的な理由としては が遅くなる現実的な理由があります。

ここでは、言語機能、仮想マシン実行、ガベージコレクションという3つの基本的な違いについて考えてみます。後者の 2 つは一緒になることが多いですが、独立していることもあるので、別々に見ていきます。

言語機能

C++はテンプレートを非常に重視しており、テンプレートシステムの機能はコンパイル時に可能な限り多くのことができるようにすることを主な目的としているため、プログラムの観点からは静的と言えます。そのため、ユーザーからの入力に依存しないものは基本的に何でもコンパイル時に計算することができ、実行時には単なる定数になっています。しかし、これに対する入力には、型情報のようなものも含まれるため、C#の実行時にリフレクションで行うことの大部分は、通常C++のテンプレートメタプログラミングによってコンパイル時に行われるものである。テンプレートができることは静的に行いますが、リフレクションができることすべてを行うことはできないのです。

言語機能の違いは、C# を C++ に (またはその逆に) 変換することによって 2 つの言語を比較するほとんどすべての試みが、無意味で誤解を招く結果をもたらす可能性があることを意味します (同じことが、他のほとんどの言語のペアにも当てはまります)。単純な事実として、数行のコードより大きなものについては、ほとんど誰もその言語を同じように (または十分に近い方法で) 使うことはなく、そのような比較はそれらの言語が実際の生活でどのように動作するかについて何も教えてくれません。

仮想マシン

合理的な最新の VM と同様に、Microsoft の .NET 用仮想マシンは JIT (別名 "dynamic") コンパイルが可能で、実行することができます。しかし、これは多くのトレードオフを意味します。

主に、コードの最適化 (他のほとんどの最適化問題と同様) は、大部分が NP 完全な問題です。本当につまらない、あるいはおもちゃのようなプログラム以外では、結果を本当に最適化できない (すなわち、真の最適を見つけられない) ことはほぼ確実で、オプティマイザは単にコードを より良い にするだけです。しかし、よく知られている多くの最適化は、実行にかなりの時間(そして多くの場合、メモリ)を必要とします。JIT コンパイラでは、コンパイラが実行されている間、ユーザーは待機していることになります。より高価な最適化技術のほとんどは除外されます。静的コンパイルには2つの利点があります。まず、遅い場合(例えば、大規模なシステムの構築)には、通常サーバーで実行されますし 誰も はそれを待っている時間はありません。第二に、実行形式を生成することができる 一度だけ を生成し、多くの人に何度も使ってもらうことができます。前者は最適化のコストを最小化し、後者ははるかに少ないコストをはるかに多くの実行回数に渡って償却します。

元の質問 (および他の多くの Web サイト) で言及されているように、JIT コンパイルにはターゲット環境をより認識する可能性があり、この利点は (少なくとも理論的には) 相殺されるはずです。この要素が静的コンパイルの欠点の少なくとも一部を相殺できることは間違いないでしょう。いくつかの特定のタイプのコードとターゲット環境では、静的コンパイルは ができます。 は静的コンパイルの利点を、時にはかなり劇的に上回ります。しかし、少なくとも私のテストと経験では、これはかなり珍しいことです。ターゲットに依存する最適化は、ほとんどの場合、かなり小さな違いをもたらすか、かなり特定のタイプの問題に対してのみ (とにかく自動的に) 適用されるようです。このようなことが明らかになるのは、比較的古いプログラムを最新のマシンで実行する場合です。C++で書かれた古いプログラムは、おそらく32ビットコードにコンパイルされており、最新の64ビットプロセッサ上でも32ビットコードを使用し続けるでしょう。C#で書かれたプログラムはバイトコードにコンパイルされ、VMはそれを64ビットのマシンコードにコンパイルする。もし、このプログラムが64ビットコードとして実行されることで実質的な利益を得ることができれば、それは実質的なアドバンテージとなり得るのである。64ビットプロセッサーがかなり新しいものであった短期間は、このようなことがかなり起こりました。64 ビット プロセッサの恩恵を受けそうな最近のコードは、通常、64 ビット コードに静的にコンパイルされます。

VMを使用することで、キャッシュの使用量を改善できる可能性もあります。VMの命令は、ネイティブのマシン命令よりもコンパクトであることが多いのです。より多くの命令が一定の量のキャッシュメモリに収まるので、必要なときに任意のコードがキャッシュに収まる可能性が高くなります。このことは、VMコードの解釈実行を、多くの人が最初に期待するよりも(速度面で)より競争力のあるものに保つのに役立ちます。 ロット がかかる時間で、最新の CPU で多くの命令を実行できます。 1 のキャッシュミスにかかる時間です。

また、この要因が 必ずしも とはまったく異なるものです。たとえば)C++ コンパイラが、(JIT の有無にかかわらず)仮想マシン上で実行することを意図した出力を生成することを妨げるものは何もありません。実際、Microsoft の C++/CLI は ほぼ であり、仮想マシン上で実行することを意図した出力を生成する (多くの拡張を伴うとはいえ) 準拠した C++ コンパイラーです。

逆もまた真なりです。Microsoft には、C# (または VB.NET) コードをネイティブの実行ファイルにコンパイルする、.NET Native があります。これにより、一般に C++ に近いパフォーマンスが得られますが、C#/VB の機能は保持されます (たとえば、ネイティブ コードにコンパイルされた C# はまだリフレクションをサポートしています)。パフォーマンス重視の C# コードがある場合、これは役に立つかもしれません。

ガベージコレクション

私が見たところ、ガベージ コレクションはこれら 3 つの要素のうち最もよく理解されていないものだと思います。明らかな例として、ここでの質問では次のように言及されています: "GCは、何千ものオブジェクトを作成および破棄しない限り、多くのオーバーヘッドも追加しません[...]"。現実には、もしあなたが .NETでは、コピーコレクタの一種である世代交代型スカベンジャを使用しています。ガベージコレクタは、ポインタ/参照がある場所(たとえば、レジスタや実行スタック)から開始することによって動作します。 既知の から開始します。次に、ヒープ上に割り当てられたオブジェクトへのこれらのポインタを追いかけます。そして、それらのオブジェクトをさらにポインタや参照先を調べ、すべてのポインタをチェーンの末端まで追いかけ、(少なくとも潜在的に)アクセス可能なすべてのオブジェクトを見つけます。次のステップでは、すべてのオブジェクトのうち、(少なくとも である可能性がある である)すべてのオブジェクトを取り出し、それらすべてをヒープで管理されているメモリの一端にある連続したチャンクにコピーすることでヒープをコンパクトにします。残りのメモリはその後自由になります (ファイナライザを実行する必要がありますが、少なくともよく書かれたコードでは十分にまれなので、当面は無視することにします)。

これが意味するところは、もしあなたが を作成し を行う場合、ガベージコレクションはほとんどオーバーヘッドを追加しません。ガベージコレクションサイクルにかかる時間は、作成されたオブジェクトの数にほぼ完全に依存しますが ではなく の数に依存します。オブジェクトを急いで作成したり破壊したりすることの主な結果は、単にGCがより頻繁に実行されなければならないということですが、各サイクルは依然として高速になります。もしあなたがオブジェクトを作成し はしない オブジェクトを作成し、それらを破棄する場合、GCはより頻繁に実行されます。 の各サイクルは、潜在的に生きているオブジェクトへのポインタを追いかけるのに多くの時間を費やすため、大幅に遅くなります。 まだ使用中のオブジェクトをコピーするのに多くの時間を費やすからです。

この問題に対処するため、世代交代は以下のようなオブジェクトを想定して動作します。 を持つ は、かなり長い間生存しているオブジェクトは、さらに長い間生存し続ける可能性があるという前提に基づいて動作します。これに基づいて、ガベージコレクションのサイクルの数を生き延びたオブジェクトが "tenured" になるシステムがあり、ガベージコレクタは単にそれらがまだ使用されていると仮定し始め、サイクルごとにコピーする代わりに、単にそれらをそのままにします。これは、世代スキャンが一般的に GC の他のほとんどの形式よりもかなり低いオーバーヘッドを持つように、十分な頻度で有効な仮定です。

手動によるメモリ管理は、多くの場合、同様に十分に理解されていません。1 つの例を挙げると、比較の多くの試みは、すべての手動メモリ管理が同様に 1 つの特定のモデル (たとえば、最適な割り当て) に従っていると仮定しています。これは、ガベージ コレクションに関する多くの人々の信念 (たとえば、参照カウントを使用して通常行われるという広範な仮定) よりも、現実に近いとは言えないことがよくあります (あるとすれば)。

ガベージ コレクションの両方についてさまざまな戦略があることを考えると の両方についてさまざまな戦略があるため、全体的な速度という点でこの 2 つを比較するのはかなり困難です。メモリの割り当てや解放の速度を (それ自体で) 比較しようとすると、よくても無意味、悪くても誤解を招くような結果が出ることはほぼ確実です。

ボーナス トピック ベンチマーク

非常に多くのブログ、Web サイト、雑誌記事などが、ある方向から見た客観的な証拠を提供すると主張しているので、この件に関しても私の意見を述べます。

これらのベンチマークのほとんどは、10 代の子供たちが自分の車でレースをし、勝った人が両方の車を所有することに決めたようなものです。しかし、Web サイトは 1 つの重要な方法で異なります。ベンチマークを公開している人は、両方の車を運転することができます。不思議なことに、彼の車はいつも優勝し、他のみんなは「私を信じてください、私は...」と言わなければならないのです。 本当に を、全力で運転することです。

ほとんど意味のない結果をもたらす貧弱なベンチマークを作成するのは簡単です。意味のある結果を出すベンチマークを設計するのに必要なスキルに近いものを持つ人はほとんど誰でも、自分が欲しいと決めた結果を出すベンチマークを作成するスキルも持っています。実際、それはおそらく より簡単です。 特定の結果を生成するコードを書くことは、本当に意味のある結果を生成するコードよりも簡単でしょう。

私の友人であるJames Kanzeが言ったように、「自分で改竄していないベンチマークは決して信用しないこと」です。

結論

単純な答えはありません。私は、勝者を選ぶためにコインを投げ、勝者の割合を (たとえば) 1 から 20 の間の数字で選び、妥当で公正なベンチマークのように見えるコードを書き、その当然の結論を出すことができると合理的に確信しています (少なくともいくつかのターゲット プロセッサで。別のプロセッサでは割合が少し変わるかもしれません)。

他の人が指摘したように ほとんどの場合 のコードでは、速度はほとんど関係ありません。これに対する補足として (これはもっと頻繁に無視されます)、速度が問題となるような小さなコードでは、通常 たくさん . 少なくとも私の経験では、本当に重要なコードでは、C++ がほとんど常に勝者です。C# に有利な要素は確かにありますが、実際には C++ に有利な要素がそれを上回っているように見えます。選択の結果を示すベンチマークを見つけることはできますが、実際のコードを書くときには、ほとんどの場合、C#よりもC++の方が速くできます。それは、より多くのスキルや書くための努力を要するかもしれません (または、そうでないかもしれません)。