web-dev-qa-db-ja.com

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

C anc 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の実行が30倍以上遅くなっているのはなぜですか?

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

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

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

92
John

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

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

編集: Jalf's answer 以下を参照

60
Brann

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

Time elapsed: 0.000000

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

あなたが測定しているのは基本的に「どのコンパイラが最も多くのデバッグオーバーヘッドを挿入するか」だと思われます。答えはCです。しかし、どのプログラムが最も速いかはわかりません。速度が必要なときは、最適化を有効にするからです。

ちなみに、言語がお互いよりも「速い」という概念を放棄すれば、長期的には多くの頭痛の種を省くことができます。 C#には、英語ほど速度がありません。

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

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

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

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

160
jalf

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

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

113
Hans Passant

私はC++およびC#開発者です。 .NETフレームワークの最初のベータ版からC#アプリケーションを開発し、C++アプリケーションの開発で20年以上の経験があります。まず、C#コードはC++アプリケーションよりも高速になることはありませんが、マネージコード、その仕組み、相互運用層、メモリ管理内部、動的型システム、ガベージコレクターについての長い議論は行いません。それにもかかわらず、ここにリストされているベンチマークはすべて間違った結果を生むと言っておこう。

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

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

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

Intelコンパイラ(ちなみにAMD/Intelプロセッサで高性能アプリケーションを生成するための業界秘密)を使用すると、同じコードがC++実行可能ファイルに対して.54秒で実行されます。 。最終的に、最終結果は、C++で0.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に基づいています(カスタムアクションを使用)。

54
Richard

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

編集:いまいましい、9秒でビート!

10
Neil N

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

root += Math.Sqrt(i);

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

7
anon

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

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

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

6
i_am_jorf

時差がどうであれ。 「経過時間」は無効かもしれません。両方のプログラムがまったく同じ条件で実行されることを保証できる場合にのみ有効です。

たぶん、あなたは勝利を試みる必要があります。 $/usr/bin/time my_cprog;/usr/bin/time my_csprogと同等

5
Tom

(コードに基づいて)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

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

経験豊富な推測の必要はありません。

5
Mike Dunlavey

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

2
David M

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

1
Jack Ryan

実際、ループは最適化されていないわけではありません。 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億の追加演算子を追加するだけのオーバーヘッドですか?

1
Dana