C#とC-パフォーマンスの大きな違い


94

CとC#の類似コード間でパフォーマンスの大きな違いを見つけています。

Cコードは次のとおりです。

#include <stdio.h>
#include <time.h>
#include <math.h>

main()
{
    int i;
    double root;

    clock_t start = clock();
    for (i = 0 ; i <= 100000000; i++){
        root = sqrt(i);
    }
    printf("Time elapsed: %f\n", ((double)clock() - start) / CLOCKS_PER_SEC);   

}

そして、C#(コンソールアプリ)は次のとおりです。

using System;
using System.Collections.Generic;
using System.Text;

namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            DateTime startTime = DateTime.Now;
            double root;
            for (int i = 0; i <= 100000000; i++)
            {
                root = Math.Sqrt(i);
            }
            TimeSpan runTime = DateTime.Now - startTime;
            Console.WriteLine("Time elapsed: " + Convert.ToString(runTime.TotalMilliseconds/1000));
        }
    }
}

上記のコードでは、C#は0.328125秒(リリースバージョン)で完了し、Cの実行には11.14秒かかります。

cは、mingwを使用してWindows実行可能ファイルにコンパイルされています。

私は常に、C / C ++がC#.netより高速であるか、少なくともC#.netに匹敵するという仮定の下にありました。正確には、Cの実行速度が30倍以上遅いのはなぜですか。

編集:C#オプティマイザーが使用されていなかったため、ルートを削除していたようです。ルートの割り当てをルート+ =に変更し、最後に合計を出力しました。また、/ O2フラグを最大速度に設定してcl.exeを使用してCをコンパイルしました。

結果は次のとおりです。Cの場合3.75秒C#の場合2.61秒

Cはまだ時間がかかりますが、これは許容範囲です


18
DateTimeだけでなく、StopWatchを使用することをお勧めします。
Alex Fort

2
どのコンパイラフラグ?どちらも最適化を有効にしてコンパイルされていますか?
2009年

2
C ++コンパイラで-ffast-mathを使用する場合はどうですか?
Dan McClain、

10
なんて面白い質問でしょう!
ロバートS.

4
たぶん、C sqrt関数はC#のこれほど良くありません。その後、それはCでは問題になりませんが、それに接続されたライブラリがあります。数学関数なしでいくつかの計算を試してください。
klew 2009年

回答:


61

「ルート」を使用したことがないため、コンパイラーはメソッドを最適化するために呼び出しを削除している可能性があります。

平方根の値をアキュムレータに累積し、メソッドの最後に出力して、何が起こっているかを確認することができます。

編集:参照Jalfの答え以下


1
少し実験したところ、そうではないことがわかりました。ループのコードが生成されますが、おそらくランタイムはそれをスキップするのに十分スマートです。蓄積したとしても、C#が、まだCのパンツを打つ
ダナ

3
問題は反対側にあるようです。C#はすべての場合に適切に動作します。彼のCコードは最適化なしでコンパイルされているようです
jalf

2
あなたの多くはここでポイントを逃しています。私はc#がc / c ++よりも優れている多くの同様のケースを読んでいますが、常に反論は専門家レベルの最適化を採用することです。プログラマーの99%は、c#コードよりもわずかに速くコードを実行させるために、そのような最適化手法を使用する知識を持っていません。c / c ++のユースケースは狭まっています。

167

デバッグビルドを比較する必要があります。私はあなたのCコードをコンパイルし、そして得ました

Time elapsed: 0.000000

最適化を有効にしない場合、実行するベンチマークはまったく価値がありません。(最適化を有効にすると、ループが最適化されます。そのため、ベンチマークコードにも欠陥があります。通常、結果などを合計し、最後に出力して、ループを強制的に実行する必要があります)

測定しているのは、基本的には「どのコンパイラーが最も多くのデバッグオーバーヘッドを挿入するか」ということです。そして、答えはCです。しかし、それはどのプログラムが最も速いかを教えてくれません。スピードが欲しいときに最適化を有効にするからです。

ところで、言語が互いに「速い」という概念を放棄すれば、長期的には頭痛の種を大幅に減らすことができます。C#は英語よりも速度が速くありません。

C言語には、単純な最適化されていないコンパイラでも効率的なものもあれば、すべてを最適化するためにコンパイラに大きく依存しているものもあります。そしてもちろん、C#や他の言語でも同じことが言えます。

実行速度は以下によって決定されます。

  • 実行しているプラ​​ットフォーム(OS、ハードウェア、システムで実行している他のソフトウェア)
  • コンパイラ
  • あなたのソースコード

優れたC#コンパイラは効率的なコードを生成します。悪いCコンパイラは遅いコードを生成します。C#コンパイラを介して実行できるC#コードを生成したCコンパイラはどうですか?それはどれくらい速く走りますか?言語にはスピードがありません。あなたのコードはそうします。



18
良い答えですが、少なくとも類推では言語速度については同意しません。ウェルシュは長い母音の頻度が高いため、他の言語よりも遅い言語であることがわかっています。さらに、人々は、言葉(および単語リスト)の方が速く言うほど、覚えやすくなります。web.missouri.edu/~cowann/docs/articles/before%201993/... en.wikipedia.org/wiki/Vowel_length en.wikipedia.org/wiki/Welsh_language
exceptionerror

1
それはあなたがウェルシュで言っていることに依存していませんか?すべてが遅いとは思えない。
jalf

5
++こんにちは、ここで脇道に行かないでください。同じプログラムが1つの言語で別の言語より速く実行される場合、それは異なるアセンブリコードが生成されるためです。この特定の例では、99%以上の時間がフロート状態iになるsqrtため、測定されます。
Mike Dunlavey、2009年

116

簡潔に説明します。すでに回答済みとマークされています。C#には、明確に定義された浮動小数点モデルがあるという大きな利点があります。それはたまたま、x86およびx64プロセッサー上のFPUおよびSSE命令セットのネイティブ動作モードと一致します。偶然ありません。JITterはMath.Sqrt()をいくつかのインライン命令にコンパイルします。

ネイティブC / C ++には、長年にわたる下位互換性が備わっています。/ fp:precise、/ fp:fast、および/ fp:strictコンパイルオプションが最もよく表示されます。したがって、sqrt()を実装するCRT関数を呼び出し、選択した浮動小数点オプションをチェックして結果を調整する必要があります。それは遅いです。


66
これはC ++プログラマーの間の奇妙な信念であり、C#によって生成されたマシンコードは、ネイティブコンパイラによって生成されたマシンコードとはどういうわけか異なると考えているようです。種類は1つだけです。使用するgccコンパイラスイッチや、インラインアセンブリを記述しても、FSQRT命令は1つしかありません。ネイティブ言語が生成したため、常に高速であるとは限りません。CPUは関係ありません。
ハンスパッサント2012年

16
これが、ngen.exeを使用して事前に解決することです。ここでは、JavaではなくC#について話しています。
ハンスパッサント

20
@ user877329-本当に?ワオ。
Andras Zoltan

7
いいえ、x64ジッタはSSEを使用します。Math.Sqrt()は、sqrtsdマシンコード命令に変換されます。
ハンスパッサント'19

6
技術的には言語間の違いではありませんが、.net JITterは、一般的なC / C ++コンパイラーと比較して最適化を制限しています。最大の制限の1つは、SIMDがサポートされていないためにコードが4倍ほど遅くなることが多いことです。多くの組み込み関数を公開しないことも大きな悪影響を及ぼす可能性がありますが、それはあなたが何をしているのかに大きく依存します。
CodesInChaos 2013年

57

私はC ++とC#の開発者です。.NETフレームワークの最初のベータ版からC#アプリケーションを開発しており、C ++アプリケーションの開発で20年以上の経験があります。まず、C#コードはC ++アプリケーションよりも高速になることはありませんが、マネージコード、その機能、相互運用層、メモリ管理の内部、動的型システム、およびガベージコレクタについては、詳しく説明しません。それでも、ここに記載されているベンチマークはすべて正しくない結果を生むと言い続けます。

説明させてください:最初に考慮する必要があるのは、C#用のJITコンパイラ(.NET Framework 4)です。現在、JITは、さまざまな最適化アルゴリズム(Visual Studioに付属するデフォルトのC ++オプティマイザーよりも積極的である傾向があります)を使用してCPUのネイティブコードを生成し、.NET JITコンパイラーによって使用される命令セットは、実際のCPUをより反映していますマシン上で、マシンコードの特定の置換を行って、クロックサイクルを削減し、CPUパイプラインキャッシュのヒット率を向上させ、命令の並べ替えや分岐予測に関連する改善などのハイパースレッディング最適化をさらに生成できます。

つまり、RELEASEビルド(DEBUGビルドではない)の正しいパラメータを使用してC ++アプリケーションをコンパイルしない限り、C ++アプリケーションのパフォーマンスは、対応するC#または.NETベースのアプリケーションよりも遅くなる可能性があります。C ++アプリケーションでプロジェクトのプロパティを指定するときは、「完全な最適化」と「高速なコードを優先する」を必ず有効にしてください。64ビットマシンを使用している場合は、ターゲットプラットフォームとしてx64を生成するように指定する必要があります。指定しない場合、コードはパフォーマンスを大幅に低下させる変換サブレイヤー(WOW64)を介して実行されます。

コンパイラーで正しい最適化を実行すると、C ++アプリケーションの場合は.72秒、C#アプリケーションの場合は(両方ともリリースビルドで)1.16秒になります。C#アプリケーションは非常に基本的であり、ヒープではなくスタックのループで使用されるメモリを割り当てるため、実際には、オブジェクトに関係する実際のアプリケーション、大量の計算、およびより大きなデータセットよりもはるかに優れています。したがって、提供されている数値は、C#および.NETフレームワークに偏った楽観的な数値です。このバイアスがあっても、C ++アプリケーションは同等のC#アプリケーションよりも半分の時間で完了します。私が使用したMicrosoft C ++コンパイラには、適切なパイプラインとハイパースレッディング最適化(WinDBGを使用してアセンブリ命令を表示する)がなかったことに注意してください。

Intelコンパイラを使用すると(AMD / Intelプロセッサで高性能アプリケーションを生成するための業界の秘密です)、同じコードがC ++実行可能ファイルでは.54秒で実行されますが、Microsoft Visual Studio 2010では.72秒で実行されます。したがって、最終的に、最終結果はC ++の場合は.54秒、C#の場合は1.16秒になります。したがって、.NET JITコンパイラによって生成されるコードは、C ++実行可能ファイルよりも214%倍長くかかります。.54秒で費やされた時間のほとんどは、ループからではなく、システムから時間を取得するために費やされました。

統計に欠けているのは、タイミングに含まれていない起動時間とクリーンアップ時間です。C#アプリケーションは、C ++アプリケーションよりも起動と終了に多くの時間を費やす傾向があります。この背後にある理由は複雑であり、.NETランタイムコード検証ルーチンと、メモリ割り当てとガベージを最適化するためにプログラムの最初(そして結果として最後)で多くの作業を実行するメモリ管理サブシステムに関係しています。コレクタ。

C ++および.NET ILのパフォーマンスを測定するときは、アセンブリコードを見て、すべての計算が行われていることを確認することが重要です。私が見つけたのは、C#に追加のコードを配置せずに、上記の例のほとんどのコードが実際にはバイナリから削除されたことです。これは、インテルC ++コンパイラーに付属しているオプティマイザーなど、より強力なオプティマイザーを使用した場合のC ++にも当てはまります。上記で提供した結果は100%正確であり、アセンブリレベルで検証されています。

インターネット上の多くのフォーラムの主な問題は、多くの初心者がテクノロジーを理解せずにMicrosoftのマーケティング宣伝に耳を傾け、C#がC ++よりも速いという誤った主張をすることです。JITコンパイラーがCPUのコードを最適化できるため、理論的にはC#はC ++よりも高速であると主張しています。この理論の問題は、パフォーマンスを遅くする.NETフレームワークに存在する多くの配管が存在することです。C ++アプリケーションには存在しない配管。さらに、経験豊富な開発者は、特定のプラットフォームに使用する適切なコンパイラーを知っており、アプリケーションのコンパイル時に適切なフラグを使用します。Linuxまたはオープンソースプラットフォームでは、ソースを配布し、適切な最適化を使用してコードをコンパイルするインストールスクリプトを作成できるため、これは問題ではありません。Windowsまたはクローズドソースプラットフォームでは、複数の実行可能ファイルを配布し、それぞれに特定の最適化を行う必要があります。展開されるWindowsバイナリは、msiインストーラーによって検出されたCPUに基づいています(カスタムアクションを使用)。


22
1.マイクロソフトは、C#の高速化に関するこれらの主張を行ったことはありません。その主張は、速度の約90%、開発が高速(したがって、調整に時間がかかる)、メモリとタイプセーフティによりバグが少ないことです。すべてが当てはまります(私はC ++で20年、C#で10年を持っています)2.ほとんどの場合、起動パフォーマンスは意味がありません。3. LLVMのようなより高速なC#コンパイラもあります(そのため、IntelをAppleにすることはAppleからAppleにではありません)
ベン

13
起動パフォーマンスは意味がありません。これは、ほとんどのエンタープライズWebベースのアプリケーションで非常に重要です。これが、Microsoftが.NET 4.0にプリロード(自動起動)するWebページを導入した理由です。アプリケーションプールがときどきリサイクルされると、各ページが初めて読み込まれるときに、複雑なページに大幅な遅延が発生し、ブラウザーでタイムアウトが発生します。
リチャード

8
マイクロソフトは、以前のマーケティング資料で.NETのパフォーマンスが高速であると主張しました。彼らはまた、ガベージコレクターがパフォーマンスにほとんどまたはまったく影響を与えなかったというさまざまな主張をしました。これらの主張の一部は、以前の版で(ASP.NETおよび.NETの)さまざまな本になりました。Microsoftは、C#アプリケーションがC ++アプリケーションよりも高速であると明確に述べていませんが、「Just-In-Time Means Run-It-Fast」(msdn.microsoft.com/ en-us / library / ms973894.aspx)。
リチャード

71
-1、この発言は、「C#コードはC ++アプリケーションよりも速くなることはない」という明らかな大げさな不正確で誤解を招く記述でいっぱいです
BCoates

32
-1。Rico Mariani対Raymond ChenのC#対Cパフォーマンスの戦いを読む必要があります:blogs.msdn.com/b/ricom/archive/2005/05/16/418051.aspx。つまり、Microsoftで最も賢い人の1人が、Cバージョンを単純なC#バージョンよりも高速にするために多くの最適化を行いました。
Rolf Bjarne Kvinge 2012

10

rootを使用しないので、私の最初の推測はコンパイラの最適化です。割り当てるだけで、何度も上書きできます。

編集:くそー、9秒でビート!


2
私はあなたが正しいと言います。実際の変数は上書きされ、それを超えて使用されることはありません。cscはおそらくループ全体を放棄するでしょうが、c ++コンパイラはおそらくそれを残しました。より正確なテストは、結果を蓄積し、その結果を最後に出力することです。また、シード値をハードコーディングするのではなく、ユーザー定義のままにしておきます。これは、c#コンパイラーにものを省く余地を与えません。

7

ループが最適化されているかどうかを確認するには、コードを次のように変更してみてください

root += Math.Sqrt(i);

Cコードで同様にansを実行し、ループの外側にrootの値を出力します。


6

たぶん、C#コンパイラは、どこでもrootを使用しないことに気付いているので、forループ全体をスキップするだけです。:)

それはそうではないかもしれませんが、原因が何であれ、それはコンパイラの実装に依存していると思います。最適化とリリースモードを使用して、Microsoftコンパイラー(cl.exe、win32 sdkの一部として入手可能)でCプログラムをコンパイルしてみてください。他のコンパイラよりもパフォーマンスが向上するはずです。

編集:Math.Sqrt()には副作用がないことを知っている必要があるため、コンパイラーがforループを最適化できるとは思いません。


2
多分それはそれを知っています。

2
@ニール、@ジェフ:そうですね、それはかなり簡単にわかるでしょう。実装によっては、Math.Sqrt()の静的分析はそれほど難しくないかもしれませんが、具体的にどの最適化が実行されるのかはわかりません。
John Feminella、2009年

5

時差が何であれ。おそらく「経過時間」は無効です。両方のプログラムがまったく同じ条件で実行されることが保証できる場合にのみ有効です。

たぶん、あなたは勝利を試みるべきです。$ / usr / bin / time my_cprog; / usr / bin / time my_csprogと同等


1
なぜこれが反対投票されたのですか?誰かが割り込みとコンテキスト切り替えがパフォーマンスに影響を与えないと想定していますか?誰でもTLBミス、ページスワッピングなどを想定できますか?
トム

5

(コードに基づいて)CとC#でさらに2つの比較可能なテストをまとめました。これら2つは、インデックス付けにモジュラス演算子を使用して、より小さな配列を書き込みます(少しオーバーヘッドが追加されますが、ちょっと粗いレベルでパフォーマンスを比較しようとしています)。

Cコード:

#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <math.h>

void main()
{
    int count = (int)1e8;
    int subcount = 1000;
    double* roots = (double*)malloc(sizeof(double) * subcount);
    clock_t start = clock();
    for (int i = 0 ; i < count; i++)
    {
        roots[i % subcount] = sqrt((double)i);
    }
    clock_t end = clock();
    double length = ((double)end - start) / CLOCKS_PER_SEC;
    printf("Time elapsed: %f\n", length);
}

C#の場合:

using System;

namespace CsPerfTest
{
    class Program
    {
        static void Main(string[] args)
        {
            int count = (int)1e8;
            int subcount = 1000;
            double[] roots = new double[subcount];
            DateTime startTime = DateTime.Now;
            for (int i = 0; i < count; i++)
            {
                roots[i % subcount] = Math.Sqrt(i);
            }
            TimeSpan runTime = DateTime.Now - startTime;
            Console.WriteLine("Time elapsed: " + Convert.ToString(runTime.TotalMilliseconds / 1000));
        }
    }
}

これらのテストは配列にデータを書き込みます(そのため、.NETランタイムがsqrt opをカリングすることは許可されません)が、配列は大幅に小さくなります(過剰なメモリを使用したくない)。これらをリリース構成でコンパイルし、コンソールウィンドウ内から実行しました(VSから開始するのではなく)。

私のコンピューターでは、C#プログラムは6.2〜6.9秒で変化しますが、Cバージョンは6.9〜7.1で変化します。


5

平方根ルーチンのステップ実行を含め、アセンブリレベルでコードをシングルステップ実行するだけで、おそらく質問に対する答えが得られます。

教育を受けた推測の必要はありません。


私はこれを行う方法を知りたいのですが
Josh Stodola

IDEまたはデバッガーによって異なります。pgmの先頭で中断します。逆アセンブルウィンドウを表示し、シングルステップを開始します。GDBを使用する場合、一度に1つの命令をステップ実行するためのコマンドがあります。
Mike Dunlavey、2009年

これは良いヒントですが、これは実際に何が起こっているのかを理解するのに役立ちます。インライン化や末尾呼び出しなどのJIT最適化も表示されますか?
gjvdkamp

参考:私にとっては、これはVC ++がfaddとfsqrtを使用していることを示していますが、C#はcvtsi2sdとsqrtsdを使用しました。
danio

2

ここで問題となる可能性のあるもう1つの要因は、Cコンパイラがターゲットプロセッサフ​​ァミリの汎用ネイティブコードにコンパイルされるのに対し、C#コードをコンパイルしたときに生成されたMSILがJITコンパイルされて、完全なプロセッサをターゲットにすることです。可能性のある最適化。したがって、C#から生成されたネイティブコードは、Cよりもかなり高速になる可能性があります。


理論的にはそうです。実際には、それが実質的に測定可能な違いをもたらすことはありません。運が良ければ、おそらく1〜2%。
2009年

または-「汎用」プロセッサの許可リストにない拡張機能を使用する特定のタイプのコードがある場合。SSEフレーバーのようなもの。プロセッサターゲットを高く設定して、どのような違いがあるかを確認してください。
gbjbaanb 2009年

1

これは言語自体とは関係なく、平方根関数のさまざまな実装と関係があるように思えます。


異なるsqrt実装がそのような格差を引き起こすのではないかと私は強く疑っています。
Alex Fort

特にC#でも、ほとんどの数学関数は依然としてパフォーマンスが重要であると見なされ、そのように実装されています。
マシューオレニク

fsqrtはIA-32プロセッサの命令であるため、最近の言語実装は無関係です。
わからない

デバッガーでMSVCのsqrt関数にステップインします。これは、fsqrt命令を実行するだけではありません。
bk1e 2009年

1

実際のところ、ループは最適化されていません。Johnのコードをコンパイルし、結果の.exeを調べました。ループの要点は次のとおりです。

 IL_0005:  stloc.0
 IL_0006:  ldc.i4.0
 IL_0007:  stloc.1
 IL_0008:  br.s       IL_0016
 IL_000a:  ldloc.1
 IL_000b:  conv.r8
 IL_000c:  call       float64 [mscorlib]System.Math::Sqrt(float64)
 IL_0011:  pop
 IL_0012:  ldloc.1
 IL_0013:  ldc.i4.1
 IL_0014:  add
 IL_0015:  stloc.1
 IL_0016:  ldloc.1
 IL_0017:  ldc.i4     0x5f5e100
 IL_001c:  ble.s      IL_000a

ランタイムがループを実現するのに十分スマートでない限り、何もせずにスキップしますか?

編集:C#を次のように変更します。

 static void Main(string[] args)
 {
      DateTime startTime = DateTime.Now;
      double root = 0.0;
      for (int i = 0; i <= 100000000; i++)
      {
           root += Math.Sqrt(i);
      }
      System.Console.WriteLine(root);
      TimeSpan runTime = DateTime.Now - startTime;
      Console.WriteLine("Time elapsed: " +
          Convert.ToString(runTime.TotalMilliseconds / 1000));
 }

(私のマシンでは)経過時間が0.047から2.17になります。しかし、それは1億の追加演算子を追加するオーバーヘッドだけなのでしょうか。


3
C#コンパイラーは定数の折りたたみやデッドコードの削除などの処理を実行しますが、ILがロード時に残りの処理を行うため、ILを確認しても最適化についてはあまりわかりません。
Daniel Earwicker 2009年

それが本当かもしれないと思ったのです。ただし、強制的に機能させても、Cバージョンよりも9秒間高速です。(私はそれをまったく期待していなかったでしょう)
ダナ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.