インラインアセンブリ言語はネイティブC ++コードよりも遅いですか?


183

インラインアセンブリ言語とC ++コードのパフォーマンスを比較しようとしたので、サイズ2000の2つの配列を100000回追加する関数を作成しました。これがコードです:

#define TIMES 100000
void calcuC(int *x,int *y,int length)
{
    for(int i = 0; i < TIMES; i++)
    {
        for(int j = 0; j < length; j++)
            x[j] += y[j];
    }
}


void calcuAsm(int *x,int *y,int lengthOfArray)
{
    __asm
    {
        mov edi,TIMES
        start:
        mov esi,0
        mov ecx,lengthOfArray
        label:
        mov edx,x
        push edx
        mov eax,DWORD PTR [edx + esi*4]
        mov edx,y
        mov ebx,DWORD PTR [edx + esi*4]
        add eax,ebx
        pop edx
        mov [edx + esi*4],eax
        inc esi
        loop label
        dec edi
        cmp edi,0
        jnz start
    };
}

ここにありmain()ます:

int main() {
    bool errorOccured = false;
    setbuf(stdout,NULL);
    int *xC,*xAsm,*yC,*yAsm;
    xC = new int[2000];
    xAsm = new int[2000];
    yC = new int[2000];
    yAsm = new int[2000];
    for(int i = 0; i < 2000; i++)
    {
        xC[i] = 0;
        xAsm[i] = 0;
        yC[i] = i;
        yAsm[i] = i;
    }
    time_t start = clock();
    calcuC(xC,yC,2000);

    //    calcuAsm(xAsm,yAsm,2000);
    //    for(int i = 0; i < 2000; i++)
    //    {
    //        if(xC[i] != xAsm[i])
    //        {
    //            cout<<"xC["<<i<<"]="<<xC[i]<<" "<<"xAsm["<<i<<"]="<<xAsm[i]<<endl;
    //            errorOccured = true;
    //            break;
    //        }
    //    }
    //    if(errorOccured)
    //        cout<<"Error occurs!"<<endl;
    //    else
    //        cout<<"Works fine!"<<endl;

    time_t end = clock();

    //    cout<<"time = "<<(float)(end - start) / CLOCKS_PER_SEC<<"\n";

    cout<<"time = "<<end - start<<endl;
    return 0;
}

次に、プログラムを5回実行して、時間と見なすことができるプロセッサのサイクルを取得します。上記の関数のみを呼び出すたびに。

そして、これが結果です。

アセンブリバージョンの機能:

Debug   Release
---------------
732        668
733        680
659        672
667        675
684        694
Average:   677

C ++バージョンの機能:

Debug     Release
-----------------
1068      168
 999      166
1072      231
1002      166
1114      183
Average:  182

リリースモードのC ++コードは、アセンブリコードよりも約3.7倍高速です。どうして?

私が書いたアセンブリコードは、GCCが生成したものほど効果的ではないと思います。私のような一般的なプログラマーが、コンパイラーが生成する対戦相手よりも速くコードを書くのは難しいです。つまり、自分の手で作成したアセンブリー言語のパフォーマンスを信頼せず、C ++に集中して、アセンブリー言語を忘れてはなりませんか?


29
かなり。ハンドコーディングされたアセンブリは、状況によっては適切ですが、アセンブリのバージョンが、より高いレベルの言語で実現できるものよりも確かに高速になるように注意する必要があります。
Magnus Hoff

161
コンパイラーによって生成されたコードを調べて、それがアセンブリーのバージョンよりも速い理由を理解しようとするのは有益かもしれません。
Paul R

34
ええ、コンパイラはあなたよりもasmを書くのが得意なようです。最新のコンパイラーは本当に優れています。
David Heffernan 2012年

20
GCCが生成したアセンブリを見ましたか?その可能なGCCはMMX命令を使用しました。関数は非常に並列です-Nプロセッサを使用して、合計を1 / N回計算できます。並列化の希望がない関数を試してください。
Chris

11
ええと、私は良いコンパイラがこれを〜100000倍速くすることを期待していました...
PlasmaHH

回答:


261

はい、ほとんどの場合。

まず第一に、低水準言語(この場合はアセンブリ)は常に高水準言語(この場合はC ++およびC)よりも高速なコードを生成するという誤った仮定から始めます。それは真実ではない。Cコードは常にJavaコードより高速ですか?いいえ、別の変数があるからです:プログラマー。コードの記述方法とアーキテクチャの詳細に関する知識は、パフォーマンスに大きな影響を与えます(この場合で見たように)。

手作りのアセンブリコードがコンパイルされたコードよりも優れている例をいつでも作成できますが、通常は架空の例または単一のルーチンであり、500.000行以上のC ++コードの真のプログラムではありません)。私は、コンパイラは、アセンブリより良いコードの95%の時間を作るとなると思う時々 、唯一のいくつかの珍しい回、あなたは、いくつかの短い、の書き込みアセンブリコードに必要になることがあり、高度に使用パフォーマンスの重要なルーチンまたはアクセスを持っている場合は、あなたのお気に入りのハイレベル言語を備え公開しません。この複雑さに少し触れたいですか?この素晴らしい答えをここで読んでください。

なんでこれ?

まず第一に、コンパイラーは私たちが想像もできない最適化を行うことができ(この短いリストを参照)、秒単位で数日を必要とする場合)最適化を行うためです。

アセンブリでコーディングするときは、明確に定義された呼び出しインターフェイスを使用して、明確に定義された関数を作成する必要があります。ただし、プログラム全体の最適化や、レジスターの割り当て定数の伝播共通の部分式の削除命令のスケジューリングなど、明白ではない複雑な最適化(ポリトープモデルなど)などの手続き間の最適化を考慮に入れることができます。上のRISCアーキテクチャ連中はこの何年も前の心配を停止(命令スケジューリングは、例えば、非常に難しいことです手でチューン)と近代的なCISCの CPUは非常に長い持っているパイプラインを あまりにも。

一部の複雑なマイクロコントローラーでは、コンパイラーがより優れた(そして保守が容易な)最終コードを生成するため、システムライブラリでさえ、アセンブリではなくCで記述されています。

コンパイラは一部のMMX / SIMDx命令自動的に単独で使用することがあり、それらを使用しない場合は単純に比較できません(他の回答はすでにアセンブリコードを非常によくレビューしています)。ループのためだけに、これはコンパイラーによって一般的にチェックされるもののループ最適化の短いリストです(C#プログラムのスケジュールが決定したときに自分で実行できると思いますか?)アセンブリで何かを書く場合、私は少なくともいくつかの簡単な最適化を検討する必要があると思います。配列の教科書の例は、サイクル展開することです(そのサイズはコンパイル時に既知です)。それを実行して、もう一度テストを実行してください。

最近では、別の理由でさまざまなCPU多すぎるために、アセンブリ言語を使用する必要があることもほとんどありません。それらすべてをサポートしますか?それぞれに特定のマイクロアーキテクチャといくつかの特定の命令セットがあります。機能ユニットの数は異なり、すべてをビジー状態に保つために、組み立て手順を調整する必要があります。Cで作成する場合はPGOを使用できますが、アセンブリでは、その特定のアーキテクチャについての高度な知識が必要になります(別のアーキテクチャについてすべてを再考してやり直す必要があります)。小さなタスクの場合、コンパイラーは通常それをよりよく行い、複雑なタスクの場合通常、作業は返済されません(そしてコンパイラとにかくより良いかもしれません)。

座ってコードを見てみると、おそらく、アセンブリに変換するよりもアルゴリズムを再設計するほうが多く得られることがわかります(SOに関するこの素晴らしい投稿を読んでください)。高度な最適化があります(そしてコンパイラへのヒント)アセンブリ言語に頼る必要がある前に効果的に適用できます。組み込み関数を頻繁に使用すると、探しているパフォーマンスが向上し、コンパイラーは最適化のほとんどを実行できることに言及する価値があるでしょう。

以上のことから、5〜10倍高速のアセンブリコードを生成できる場合でも、1週間の時間支払う、50ドル高速なCPU購入するかを顧客に尋ねる必要があります。ほとんどの場合(そして特にLOBアプリケーションでは)極端な最適化はほとんど必要ありません。


9
もちろん違います。99%で95%の人が良いと思います。時々、それは単にコストがかかる(複雑な数学のため)か、時間を費やす(それでもまたコストがかかる)ためです。時々、単に最適化を忘れていたために...
Adriano Repetti

62
@ ja72-いいえ、コードを書くのは得意ではありません。コードの最適化に優れています。
Mike Baranczak

14
あなたが本当にそれを考えるまで、それは直感に反しています。同様に、VMベースのマシンは、コンパイラーが作成するための情報を持たないランタイムの最適化を始めています。
ビルK

6
@ M28:コンパイラは同じ命令を使用できます。確かに、彼らはバイナリサイズの観点から料金を支払います(これらの命令がサポートされていない場合に備えて、フォールバックパスを提供する必要があるためです)。また、ほとんどの場合、追加される「新しい命令」はいずれにせよSMID命令であり、VMとコンパイラの両方が利用するのはかなり恐ろしいものです。VMは、起動時にコードをコンパイルする必要があるという点で、この機能に料金を支払います。
Billy ONeal、2012年

9
@BillK:PGOはコンパイラーに対して同じことを行います。
Billy ONeal、2012年

194

アセンブリコードは最適ではなく、改善される可能性があります。

  • 内部ループでレジスター(EDX)をプッシュおよびポップしています。これはループの外に移動する必要があります。
  • ループのすべての反復で配列ポインターを再ロードします。これはループの外に移動する必要があります。
  • 最新のほとんどのCPUで非常に遅いことloopわかっている命令を使用します(おそらく、古いアセンブリブックを使用した結果*)。
  • 手動ループ展開を利用しません。
  • 利用可能なSIMD命令は使用しません。

したがって、アセンブラに関するスキルセットを大幅に向上させない限り、パフォーマンスのためにアセンブラコードを記述することは意味がありません。

*もちろんloop、古代の集会本から本当に教訓を得たかどうかはわかりません。しかし、実際のコードでそれを目にすることはほとんどありません。そこにあるすべてのコンパイラーはを放出しないほどスマートなloopので、私見の悪い本や古い本でしか見られません。


loopサイズを最適化した場合、コンパイラは引き続き(および多くの「非推奨」の命令を)放出する可能性があります
phuclv

1
@phuclvはい、しかし元の質問は正確にサイズではなく速度に関するものでした。
IGR94

60

アセンブリに入る前でも、より高いレベルに存在するコード変換があります。

static int const TIMES = 100000;

void calcuC(int *x, int *y, int length) {
  for (int i = 0; i < TIMES; i++) {
    for (int j = 0; j < length; j++) {
      x[j] += y[j];
    }
  }
}

ループ回転を介してに変換することができます:

static int const TIMES = 100000;

void calcuC(int *x, int *y, int length) {
    for (int j = 0; j < length; ++j) {
      for (int i = 0; i < TIMES; ++i) {
        x[j] += y[j];
      }
    }
}

これは、メモリの局所性に関してははるかに優れています。

これはさらに最適化される可能性がありますa += b。X回実行するa += X * bことは、次のように実行することと同じです。

static int const TIMES = 100000;

void calcuC(int *x, int *y, int length) {
    for (int j = 0; j < length; ++j) {
      x[j] += TIMES * y[j];
    }
}

ただし、私のお気に入りのオプティマイザー(LLVM)はこの変換を実行しないようです。

[編集]私は、我々が持っていた場合、変換が実行されることがわかっrestrictへの修飾子をxy。確かに、この制限はなく、x[j]そしてy[j]可能性この変換の誤を行う同じ場所にエイリアス。[編集を終了]

とにかく、これは最適化されたCバージョンだと思います。すでにそれははるかに簡単です。これに基づいて、ASMでの亀裂を以下に示します(Clangで生成させたので、役に立たない)。

calcuAsm:                               # @calcuAsm
.Ltmp0:
    .cfi_startproc
# BB#0:
    testl   %edx, %edx
    jle .LBB0_2
    .align  16, 0x90
.LBB0_1:                                # %.lr.ph
                                        # =>This Inner Loop Header: Depth=1
    imull   $100000, (%rsi), %eax   # imm = 0x186A0
    addl    %eax, (%rdi)
    addq    $4, %rsi
    addq    $4, %rdi
    decl    %edx
    jne .LBB0_1
.LBB0_2:                                # %._crit_edge
    ret
.Ltmp1:
    .size   calcuAsm, .Ltmp1-calcuAsm
.Ltmp2:
    .cfi_endproc

これらのすべての命令がどこから来ているのかわからないのですが、いつでも楽しんで、比較してみることができます...しかし、コードでは、アセンブリバージョンではなく、最適化されたCバージョンを使用します。はるかにポータブルです。


答えてくれてありがとう。さて、「コンパイラの原則」という名前のクラスを取り上げたとき、コンパイラがさまざまな方法でコードを最適化することを知りました。それは、コードを手動で最適化する必要があることを意味しますか?コンパイラーよりも優れた仕事をすることができますか?それはいつも私を混乱させる質問です。
user957121

2
@ user957121:より多くの情報があれば、より最適化できます。具体的には、ここでコンパイラを妨げているのは、との間の可能なエイリアスです。つまり、コンパイラーは、私たちすべてがを持っているとは確信できません。オーバーラップがある場合、最適化は不可能です。C言語は、2つのポインタがエイリアスできないことをコンパイラに伝えるためにキーワードを導入しましたが、正確にエイリアスしなくても重複する可能性があるため、配列に対しては機能しません。xyi,j[0, length)x + i != y + jrestrict
Matthieu M.

現在のGCCとClangは自動ベクトル化します(を省略した場合の非重複をチェックした後__restrict)SSE2はx86-64のベースラインであり、シャッフルを使用すると、SSE2は一度に2x 32ビット乗算を実行できます(64ビット製品を生成するため、結果を元に戻すためにシャッフルします)。godbolt.org/z/r7F_uo。(SSE4.1が必要ですpmulld:パックされた32x32 => 32ビット乗算)。GCCには、定数整数乗算器をシフト/加算(または減算)に変換する巧妙なトリックがあり、ビット数が少ない乗算器に適しています。Clangのシャッフルヘビーコードは、Intel CPUでのシャッフルスループットのボトルネックになります。
Peter Cordes

41

短い答え:はい。

長い答え:はい、自分が何をしているかを本当に理解していて、そうする理由がある場合を除きます。


3
そして、インテルチップ用のvtuneなどのアセンブリレベルのプロファイリングツールを実行して、どこで改善できるかを確認した場合のみ
Mark Mullin

1
これは技術的に質問に答えますが、まったく役に立ちません。私から-1。
Navin、2015年

2
非常に長い答え:「はい、新しい(新しい)CPUが使用されるたびにコード全体を変更したい場合を除きます。最良のアルゴリズムを選択しますが、コンパイラーに最適化を行わせます」
Tommylee2k

35

私は私のasmコードを修正しました:

  __asm
{   
    mov ebx,TIMES
 start:
    mov ecx,lengthOfArray
    mov esi,x
    shr ecx,1
    mov edi,y
label:
    movq mm0,QWORD PTR[esi]
    paddd mm0,QWORD PTR[edi]
    add edi,8
    movq QWORD PTR[esi],mm0
    add esi,8
    dec ecx 
    jnz label
    dec ebx
    jnz start
};

リリースバージョンの結果:

 Function of assembly version: 81
 Function of C++ version: 161

リリースモードのアセンブリコードは、C ++のほぼ2倍高速です。


18
MMXの代わりにSSE(レジスタ名はのxmm0代わりにmm0)を使い始めると、2倍のスピードアップが得られます;-)
Gunther Piez

8
私は変更し、アセンブリバージョンで41を取得しました。それは4倍速いです:)
sasha

3
すべてのxmmレジスタを使用すると、最大5%も増加します
sasha

7
実際にかかった時間について考えると、組み立て、約10時間ですか?C ++、数分かな?パフォーマンスが重要なコードでない限り、ここには明らかに勝者があります。
Calimo 14

1
優れたコンパイラは、paddd xmm(を使用しなかったため、xとの間のオーバーラップをチェックした後)ですでに自動ベクトル化しyますint *__restrict x。たとえば、gccはそれを行います:godbolt.org/z/c2JG0-。または、にインライン化した後main、割り当てを確認して重複していないことを証明できるため、重複をチェックする必要はありません。(また、一部のx86-64実装でも16バイトアライメントを想定することになりますが、スタンドアロンの定義には当てはまりません。)でコンパイルするとgcc -O3 -march=native、256ビットまたは512ビットを取得できます。ベクトル化。
Peter Cordes

24

手で書いたアセンブリ言語のパフォーマンスを信用してはいけないという意味ですか

はい、それはまさにそれが意味することであり、それはすべての言語に当てはまります。言語Xで効率的なコードを書く方法がわからない場合は、Xで効率的なコードを書く能力を信用しないでください。したがって、効率的なコードが必要な場合は、別の言語を使用する必要があります。

アセンブリはこれに特に敏感です。なぜなら、まあ、あなたが見るものはあなたが得るものだからです。CPUに実行させる特定の命令を記述します。高水準言語では、コードを変換し、多くの非効率を取り除くことができるコンパイラが間にあります。アセンブリを使用すると、自分で作業できます。


2
特に現代のx86プロセッサの場合、すべてのコア内にパイプライン、複数の実行ユニット、およびその他のギミックがあるため、効率的なアセンブリコードを書くのは非常に難しいと書いていると思います。最高の実行速度を実現するためにこれらすべてのリソースの使用量のバランスをとるコードを記述すると、「従来の」アセンブリの知識に従って高速ではないはずの簡単ではないロジックのコードが生成されることがよくあります。しかし、それほど複雑でないCPUの場合、Cコンパイラのコード生成を大幅に改善できるのは私の経験です。
Olof Forshell、2012年

4
Cコンパイラのコード、最近のx86 CPUでも通常は改善されます。ただし、CPUをよく理解する必要があります。これは、現代のx86 CPUで行うのは困難です。それが私のポイントです。対象とするハードウェアを理解していない場合、そのハードウェアを最適化することはできません。そしてコンパイラはより良い仕事をするでしょう
jalf

1
そして、もし本当にコンパイラーを吹き飛ばしたいなら、コンパイラーができない方法でクリエイティブで最適化する必要があります。これは時間と報酬のトレードオフです。そのため、Cは一部のスクリプト言語であり、中間コードは他の上位言語の中間コードです。私にとっては、アセンブリは楽しみのためにもっとあります:)。grc.com/smgassembly.htmに
Hawken

22

現在、アセンブリ言語を使用する唯一の理由は、その言語ではアクセスできないいくつかの機能を使用することです。

これは以下に適用されます:

  • MMUなどの特定のハードウェア機能にアクセスする必要があるカーネルプログラミング
  • コンパイラーでサポートされていない非常に特定のベクトルまたはマルチメディア命令を使用する高性能プログラミング。

しかし、現在のコンパイラーは非常にスマートd = a / b; r = a % b;です。Cにそのような演算子がなくても、除算と剰余を利用できる場合は、1つの命令で除算と剰余を計算する単一の命令で2つの個別のステートメントを置き換えることも できます。


10
これら2つ以外にも、ASMの他の場所があります。つまり、bignumライブラリは、キャリーフラグや乗算の上部などにアクセスできるため、ASMでは通常Cよりもはるかに高速です。これらのことはポータブルCでも行うことができますが、非常に低速です。
ムーアダック

@MooingDuck言語で直接利用できないハードウェアハードウェア機能へのアクセスと見なされる可能性があります...しかし、 、高レベルのコードを手動でアセンブリに変換して、コンパイラはあなたを倒します。
Fortran、2012年

1
それはそうですが、それはカーネルプログラミングでも、ベンダー固有でもありません。わずかな作業の変更があっても、簡単にどちらかのカテゴリに分類される可能性があります。Cマッピングのないプロセッサ命令のパフォーマンスが必要な場合は、ASMと思います。
ムーアダック

1
@fortran基本的に、コードを最適化しないとコンパイラーが最適化したコードほど速くはありません。最適化は、最初にアセンブリを作成する理由です。翻訳と最適化を意味する場合、アセンブリの最適化が得意でない限り、コンパイラーがあなたを倒す理由はありません。したがって、コンパイラーに勝つには、コンパイラーではできない方法で最適化する必要があります。それはかなり自明です。アセンブリを作成する唯一の理由は、コンパイラ/インタープリタより優れている場合です。それは常にアセンブリを作成する実用的な理由です。
ホーケン

1
ただ言っておくと、Clangは組み込み関数を通じてキャリーフラグ、128ビット乗算などにアクセスできます。そして、これらすべてを通常の最適化アルゴリズムに統合できます。
gnasher729 2014

19

現代のコンパイラーがコードの最適化で素晴らしい仕事をしているのは事実ですが、それでも、アセンブリーについて学ぶことをお勧めします。

まず第一にあなたは明らかに それに脅かされていません。それは素晴らしい、素晴らしいプラスです。次に、あなたは速度の仮定を検証または破棄するためにプロファイリングすることで正しい軌道に乗っており、経験豊富な人々からの入力を求めています。人類に知られている最大の最適化ツールを持っている:

経験が増えるにつれて、それをいつ、どこで使用するかを学習します(通常、アルゴリズムレベルで深く最適化した後、コード内の最もタイトで最も内側のループ)。

インスピレーションを得るために、Michael Abrashの記事を参照することをお勧めします(彼から連絡がない場合は、彼は最適化の第一人者です。彼は、Quakeソフトウェアレンダラーの最適化でJohn Carmackと協力しました!)

「最速のコードのようなものはありません」-Michael Abrash


2
Michael Abrashの本の1つは、グラフィックプログラミングのブラックブックだと思います。しかし、組み立てを使用するのは彼だけではありません。ChrisSawyerが組み立ての最初の2つのジェットコースターの大物ゲームを自分で書きました。
ホーケン、2012

14

私はasmコードを変更しました:

 __asm
{ 
    mov ebx,TIMES
 start:
    mov ecx,lengthOfArray
    mov esi,x
    shr ecx,2
    mov edi,y
label:
    mov eax,DWORD PTR [esi]
    add eax,DWORD PTR [edi]
    add edi,4   
    dec ecx 
    mov DWORD PTR [esi],eax
    add esi,4
    test ecx,ecx
    jnz label
    dec ebx
    test ebx,ebx
    jnz start
};

リリースバージョンの結果:

 Function of assembly version: 41
 Function of C++ version: 161

リリースモードのアセンブリコードは、C ++のほぼ4倍高速です。IMHo、アセンブリコードの速度はプログラマーによって異なります


ええ、私のコードは本当に最適化する必要があります。
user957121

5
作業の4分の1しか行わないので、4倍高速です:-) shr ecx,2配列の長さはintバイト単位ではなく、既に指定されているため、これは不要です。したがって、基本的には同じ速度を実現します。あなたはpadddハロルドからの答えを試すことができ ます、これは本当に速くなります。
Gunther Piez

13

とても面白いトピックです!
SashaのコードでSSEによってMMXを変更しました
でが私の結果です。

Function of C++ version:      315
Function of assembly(simply): 312
Function of assembly  (MMX):  136
Function of assembly  (SSE):  62

SSEを使用したアセンブリコードは、C ++の5倍高速です。


12

ほとんどの高水準言語コンパイラは非常に最適化されており、それらが何をしているかを知っています。逆アセンブルコードをダンプして、ネイティブアセンブリと比較できます。私はあなたがコンパイラが使用しているいくつかの素晴らしいトリックを見ることになると思います。

ちょうど例えば、それがもう正しいかどうかはわかりませんが:):

実行:

mov eax,0

よりサイクルがかかる

xor eax,eax

同じことをします。

コンパイラーはこれらすべてのトリックを知っており、それらを使用します。


4
まだ正しい、stackoverflow.com / questions / 1396527 /…を参照してください。使用されたサイクルのためではなく、メモリフットプリントの削減のためです。
Gunther Piez

10

コンパイラはあなたを倒しました。試してみますが、保証はいたしません。TIMESによる「乗算」は、より適切なパフォーマンステストを行うためのものでyありx、16にアラインされてlengthおり、ゼロ以外の4の倍数であると想定します。

  mov ecx,length
  lea esi,[y+4*ecx]
  lea edi,[x+4*ecx]
  neg ecx
loop:
  movdqa xmm0,[esi+4*ecx]
  paddd xmm0,[edi+4*ecx]
  movdqa [edi+4*ecx],xmm0
  add ecx,4
  jnz loop

私が言ったように、私は保証をしません。しかし、それがはるかに速く実行できる場合、私は驚かれます-ここでのボトルネックは、すべてがL1ヒットであっても、メモリスループットです。


複雑なアドレッシングがコードの速度を低下させていると思います。コードをに変更してmov ecx, length, lea ecx,[ecx*4], mov eax,16... add ecx,eax[esi + ecx]をどこでも使用すると、命令ごとに1サイクルのストールが発生しないため、ループの速度が向上します。(最新のSkylakeを使用している場合、これは適用されません)。add reg、regはループをより緊密にするだけで、役に立たない場合もあります。
ヨハン

@Johanはストールではなく、余分なサイクルレイテンシですが、それがなくても問題ないことを確認してください。この問題のないCore2用にこのコードを書きました。r + rも「複雑」ではありませんか?
ハロルド2016

7

まったく同じアルゴリズムを命令ごとにアセンブリで盲目的に実装すると、コンパイラーが実行できるよりも遅いことが保証されます。

それは、コンパイラーが行う最小の最適化でさえ、最適化をまったく行わない厳密なコードよりも優れているためです。

もちろん、コンパイラーを打ち負かすことは可能です。特にそれがコードの小さなローカライズされた部分である場合、私は自分でやらなくてはなりません。4倍高速化しますが、この場合、ハードウェアの十分な知識と、一見すると直感に反する多くのトリックに大きく依存する必要があります。


3
これは言語とコンパイラに依存すると思います。人間が書いた簡単なアセンブリによって出力が簡単に打ち消される可能性がある、非常に非効率的なCコンパイラを想像できます。GCC、それほどではありません。
Casey Rodarmor

C / ++コンパイラーはこのような事業であり、主要なものは3つしかないため、C / ++コンパイラーは、何をするかについてはかなり優れている傾向があります。特定の状況では、手書きのアセンブリがより高速になる可能性があります。多くの数学ライブラリはasmにドロップして、複数の値や広い値をより適切に処理します。したがって、保証は少し強すぎますが、可能性が高いです。
ssube 2012年

@peachykeen:一般に、アセンブリがC ++よりも遅いことが保証されているという意味ではありませんでした。私は、C ++コードがあり、それを1行ずつアセンブリに盲目的に変換する場合の「保証」を意味しました。私の回答の最後の段落もお読みください:)
vsz

5

コンパイラーとして、固定サイズのループを多くの実行タスクに置き換えます。

int a = 10;
for (int i = 0; i < 3; i += 1) {
    a = a + i;
}

作り出す

int a = 10;
a = a + 0;
a = a + 1;
a = a + 2;

そして最終的には、「a = a + 0;」であることがわかります。それは役に立たないので、この行を削除します。うまくいけば、あなたの頭の中の何かがコメントとしていくつかの最適化オプションを付ける用意があります。これらの非常に効果的な最適化により、コンパイルされた言語が高速になります。


4
aがvolatileでない限り、コンパイラーがint a = 13;最初から実行する可能性は十分にあります。
vsz 2016


4

この例は、低レベルのコードに関する重要な教訓を示しているので、気に入っています。はい、Cコードと同じ速さのアセンブリを記述できます。これはトートロジー的に真実ですが、必ずしも何も意味しません。明らかに誰かがそうできなければ、アセンブラは適切な最適化を知りません。

同様に、言語の抽象化の階層を上っていくと、同じ原則が適用されます。はい、ダーティなperlスクリプトと同じくらい高速なパーサーをCで書くことができ、多くの人々がそうしています。ただし、Cを使用したためにコードが高速になるという意味ではありません。多くの場合、高級言語はあなたが考えたこともないような最適化を行います。


3

多くの場合、一部のタスクを実行する最適な方法は、タスクが実行されるコンテキストによって異なります。ルーチンがアセンブリ言語で記述されている場合、通常、命令のシーケンスをコンテキストに基づいて変更することはできません。簡単な例として、次の簡単な方法を考えます。

inline void set_port_high(void)
{
  (*((volatile unsigned char*)0x40001204) = 0xFF);
}

上記の32ビットARMコードのコンパイラは、おそらく次のようにレンダリングします。

ldr  r0,=0x40001204
mov  r1,#0
strb r1,[r0]
[a fourth word somewhere holding the constant 0x40001204]

多分

ldr  r0,=0x40001000  ; Some assemblers like to round pointer loads to multiples of 4096
mov  r1,#0
strb r1,[r0+0x204]
[a fourth word somewhere holding the constant 0x40001000]

これは、次のいずれかの方法で、手動で組み立てたコードでわずかに最適化できます。

ldr  r0,=0x400011FF
strb r0,[r0+5]
[a third word somewhere holding the constant 0x400011FF]

または

mvn  r0,#0xC0       ; Load with 0x3FFFFFFF
add  r0,r0,#0x1200  ; Add 0x1200, yielding 0x400011FF
strb r0,[r0+5]

手動で組み立てたアプローチはどちらも、16バイトではなく12バイトのコードスペースを必要とします。後者は、「ロード」を「アド」に置き換えます。これにより、ARM7-TDMIでは2サイクル速く実行されます。コードがr0が知らない/わからないコンテキストで実行される場合、アセンブリ言語のバージョンはコンパイルされたバージョンよりもいくらか優れています。一方、あるレジスタ[eg r5]が目的のアドレス0x40001204 [eg 0x40001000]の2047バイト以内の値を保持することをコンパイラが知っていて、さらに他のレジスタ[eg r7]が行くことを知っていたとします。下位ビットが0xFFである値を保持します。その場合、コンパイラーはコードのCバージョンを最適化して単純に次のようにすることができます。

strb r7,[r5+0x204]

手動で最適化されたアセンブリコードよりもはるかに短くて高速です。さらに、set_port_highが次のコンテキストで発生したとします。

int temp = function1();
set_port_high();
function2(temp); // Assume temp is not used after this

組み込みシステム用にコーディングする場合は、まったく信じられないことではありません。set_port_highがアセンブリコードで記述されている場合、コンパイラはfunction1、アセンブリコードを呼び出す前にr0(からの戻り値を保持する)を別の場所に移動し、その後、その値をr0に戻す必要がありfunction2ます(r0の最初のパラメータが予期されるため)。したがって、「最適化された」アセンブリコードには5つの命令が必要です。格納するアドレスまたは値を保持するレジスターをコンパイラーが知らなかったとしても、その4命令バージョン(使用可能なレジスターを使用するように適応できる-必ずしもr0とr1ではない)は、「最適化された」アセンブリーに勝るものです。 -言語バージョン。前述のように、コンパイラがr5とr7に必要なアドレスとデータを持ち、1つの命令で-function1これらのレジスタは変更されないため、set_port_highstrb「手動で最適化された」アセンブリコードより小さくて高速な4つの命令

プログラマーが正確なプログラムフローを知っている場合、手動で最適化されたアセンブリコードはコンパイラよりも優れている場合がありますが、コンテキストがわかる前にコードの一部が記述されている場合や、ソースコードの一部が複数のコンテキストから呼び出されます[ set_port_highコード内の50の異なる場所で使用されている場合、コンパイラーはそれらをそれぞれ最適に展開する方法を個別に決定できます]。

一般的に、アセンブリ言語は、コードの各部分が非常に限られた数のコンテキストからアプローチできる場合に最大のパフォーマンス向上をもたらす傾向があり、一部の部分でパフォーマンスが低下する傾向があることをお勧めします。コードは、さまざまなコンテキストからアプローチできます。興味深いことに(そして便利に)アセンブリがパフォーマンスにとって最も有益なケースは、多くの場合、コードが最も単純で読みやすいケースです。アセンブリ言語のコードが厄介な混乱に変わる場所は、多くの場合、アセンブリを記述することでパフォーマンスの利点が最小になる場所です。

[マイナーな注意:アセンブリコードを使用して、高度に最適化されたねばねばした混乱を生み出すことができる場所がいくつかあります。たとえば、ARMに対して実行したコードの1つは、RAMからワードをフェッチし、値の上位6ビットに基づいて約12のルーチンの1つを実行する必要がありました(多くの値が同じルーチンにマッピングされています)。私はそのコードを次のようなものに最適化したと思います:

ldrh  r0,[r1],#2! ; Fetch with post-increment
ldrb  r1,[r8,r0 asr #10]
sub   pc,r8,r1,asl #2

レジスタr8は常にメインディスパッチテーブルのアドレスを保持していました(コードがその時間の98%を費やすループ内では、他の目的で使用することはありません)。すべての64エントリは、その前の256バイトのアドレスを参照していました。プライマリループにはほとんどの場合、約60サイクルという厳しい実行時間制限があったため、9サイクルのフェッチとディスパッチは、その目標を達成するために非常に役立ちました。256ビットの32ビットアドレスのテーブルを使用すると1サイクル速くなりますが、1KBの非常に貴重なRAMを消費します[フラッシュは複数の待機状態を追加します]。64ビットの32ビットアドレスを使用するには、フェッチしたワードから一部のビットをマスクする命令を追加する必要があり、実際に使用したテーブルよりも192バイト多く使用されていました。8ビットオフセットのテーブルを使用すると、非常にコンパクトで高速なコードが得られました。しかし、コンパイラが思いつくものとは思いませんでした。また、コンパイラがテーブルアドレスを保持するためにレジスタを「フルタイム」専用にすることも期待していません。

上記のコードは、自己完結型システムとして実行するように設計されています。定期的にCコードを呼び出すことができますが、通信しているハードウェアが16ミリ秒ごとに約1ミリ秒の間隔で安全に "アイドル"状態になる特定の時間のみです。


2

最近、私が行ったすべての速度最適化は、脳が損傷した遅いコードを適切なコードに置き換えることでした。しかし、速度が非常に重要であり、私は何かを速く作ることに真剣に取り組んだため、結果は常に反復的なプロセスであり、反復ごとに問題へのより多くの洞察を与え、より少ない操作で問題を解決する方法を見つけました。最終的な速度は常に、問題にどれだけの洞察を得たかによって決まりました。いずれかの段階で、アセンブリコード、または最適化しすぎたCコードを使用した場合、より良いソリューションを見つけるプロセスが影響を受け、最終結果が遅くなります。


2

正しい方法でより深い知識を持つアセンブリ言語を使用していない限り、C ++はより高速です。

ASMでコーディングする場合、論理的に可能な場合はCPUがより多くの命令を並行して実行できるように、命令を手動で再編成します。たとえば、ASMでコーディングするときはRAMをほとんど使用していません。ASMにはコードが20000行以上あり、push / popを使用したことがありません。

オペコードの途中でジャンプして、コードと動作を自己修正し、自己修正コードによるペナルティはない可能性があります。レジスタへのアクセスには、CPUの1ティック(場合によっては.25ティック)が必要です。RAMへのアクセスには数百時間がかかる場合があります。

ASMの最後の冒険では、RAMを使用して変数を格納したことはありません(ASMの数千行分)。ASMは、C ++よりも想像以上に高速である可能性があります。しかし、それは次のような多くの変動要因に依存します。

1. I was writing my apps to run on the bare metal.
2. I was writing my own boot loader that was starting my programs in ASM so there was no OS management in the middle.

生産性の問題に気付いたので、C#とC ++を習得しています!! 純粋なASMだけを自由時間に使用して、想像できる最速のプログラムを実行することができます。しかし、何かを生み出すためには、高級言語を使用してください。

たとえば、私がコーディングした最後のプログラムはJSとGLSLを使用していて、遅いJSについて話しても、パフォーマンスの問題に気づくことはありませんでした。これは、GPUを3Dでプログラミングするという単なる概念では、GPUにコマンドを送信する言語の速度がほとんど無関係になるためです。

ベアメタル上でのアセンブラ単独の速度は反駁できません。C ++の内部ではさらに遅くなるでしょうか?-それは、アセンブラーを最初に使用しないコンパイラーでアセンブリー・コードを作成していることが原因である可能性があります。

私の評議会は、たとえ私がアセンブリを愛しているとしても、それを避けることができるのであれば、アセンブリコードを決して記述しないことです。


1

ここでのすべての回答は、1つの側面を除外しているように見えます。特定の目的を達成するためのコードを記述しないこともありますが、それだけの面白さのためです。そうするための時間を投資するのは経済的ではないかもしれませんが、間違いなく、最速のコンパイラー最適化コードスニペットを手動でローリングされたasmの代わりに速度で打ち負かすほどの満足感はありません。


コンパイラーを打ち破りたいだけの場合、通常は関数のasm出力を取得して、それをスタンドアロンのasm関数に変換して調整する方が簡単です。インライン asmの使用は、C ++とasmの間のインターフェースを正しく取得し、最適なコードにコンパイルされることを確認するための追加の作業です。(しかし、少なくともおもしろいことをしているときは、関数が何かにインライン化するときに定数伝播などの最適化を無効にすることを心配する必要はありません 。gcc.gnu.org/wiki/DontUseInlineAsm)。
Peter Cordes 2016

コンパイラーを叩いて楽しむ方法の詳細については、Collat​​z-conjecture C ++と手書きのasm Q&Aも参照してください:)また、C ++を変更するために学習した内容を使用して、コンパイラーがより良いコードを作成できるようにする方法に関する提案もあります。
Peter Cordes

@PeterCordesだからあなたが言っていることはあなたが同意することです。
madoki 16

1
はい。asmは楽しいですが、インライン asmは通常、遊んでも間違った選択です。これは技術的にはインラインasmの質問なので、少なくとも回答のこの時点で対処することをお勧めします。また、これは回答というよりもコメントです。
Peter Cordes 2016

同意した。私は以前はasmだけの男でしたが、それが80年代でした。
madoki 16

-2

c ++コンパイラーは、組織レベルで最適化した後、ターゲットのCPUの組み込み関数を利用するコードを生成します。HLLは、いくつかの理由でアセンブラを追い越したり、パフォーマンスを向上させたりすることはありません。1.)HLLは、すべての必要なサイクル(フリップとフロップ)でコンパイルされ、アクセサコード、境界チェック、および場合によっては組み込みガベージコレクション(以前のOOPマニエリスムのスコープに対応)で出力されます。HLLは最近(新しいC ++やGOのような他のものを含む)優れた仕事をしますが、アセンブラー(つまり、コード)を上回る場合は、CPUのドキュメントを参照する必要があります。 op-codeに至るまでHLLは詳細を抽象化し、それらを排除しません。ホストOSによって認識されている場合でも、アプリは実行されません。

ほとんどのアセンブラコード(主にオブジェクト)は、「ヘッドレス」として出力され、他の実行可能形式に含めることができるため、必要な処理がはるかに少なくて済みます。実行可能ファイルがアセンブラー(NAsm、YAsmなど)によって出力される場合、機能のHLLコードと完全に一致するまで、実行速度は速くなりますが、結果は正確に比較されます。

任意の形式でHLLからアセンブラーベースのコードオブジェクトを呼び出すと、変数/定数データ型にグローバルに割り当てられたメモリを使用するメモリ空間呼び出しに加えて、本質的に処理オーバーヘッドが追加されます(これはLLLとHLLの両方に適用されます)。最終的な出力では、最終的にCPUをハードウェア(オペコード)に対するAPIとabiとして使用し、アセンブラーと「HLLコンパイラー」は本質的/基本的に同じですが、唯一の真の例外は読みやすさ(文法的)です。

FAsmを使用したアセンブラーでのHello Worldコンソールアプリケーションは1.5 KB(これはWindowsではFreeBSDとLinuxではさらに小さくなっています)であり、GCCがその最高の日に投げ出すことができるものをすべて上回ります。その理由は、いくつか例を挙げると、nopsによる暗黙的なパディング、アクセス検証、および境界チェックです。本当の目標は、クリーンなHLLライブラリと、「ハードコア」な方法でCPUをターゲットにし、最近ではほとんど(最終的に)実行する最適化可能なコンパイラーです。GCCはYAsmよりも優れているわけではありません。問題となっているのは、コーディングの実践と開発者の理解であり、「最適化」は初心者の探索と暫定的なトレーニングと経験の後に行われます。

コンパイラーは、アセンブラーと同じオペコードで出力用にリンクおよびアセンブルする必要があります。これらのコードは、CPUが除くすべてのものであるためです(CISCまたはRISC [PICも])。YAsmは初期NAsmを大幅に最適化およびクリーンアップし、最終的にはそのアセンブラからのすべての出力を高速化しますが、それでもYAsmはNAsmと同様に、開発者に代わってOSライブラリをターゲットとする外部依存関係を持つ実行可能ファイルを生成するため、マイレージが異なる場合があります。最後に、C ++はアセンブラよりも信じられないほどはるかに安全な時点にあり、特に商業部門では80%以上です...


1
CとC ++は、要求しない限り境界チェックを行わず、自分で実装するかライブラリを使用しない限り、ガベージコレクションを行いません。本当の問題は、コンパイラが人間よりも優れたループ(およびグローバル最適化)を作成するかどうかです。通常、そうです。人間が実際に何をしているかを理解し、それに多くの時間を費やしている場合を除きます
Peter Cordes

1
NASMまたはYASM(外部コードなし)を使用して静的実行可能ファイルを作成できます。どちらもフラットバイナリ形式で出力できるため、実際に実行したくない場合は、ELFヘッダーを自分でアセンブルできますがld、ファイルサイズ(ファイルのサイズだけでなく)を最適化しない限り、違いはありません。テキストセグメント)。Linux用の本当にティーン向けのELF実行可能ファイルの作成に関するWhirlwindチュートリアルを参照してください。
Peter Cordes

1
おそらく、C#を考えているかstd::vector、デバッグモードでコンパイルしています。C ++配列はそのようなものではありません。コンパイラはコンパイル時にデータをチェックできますが、追加の強化オプションを有効にしない限り、実行時のチェックはありません。たとえば、int array[]argの最初の1024要素をインクリメントする関数を参照してください。asm出力には実行時チェックがありません:godbolt.org/g/w1HF5t。取得されるのはのポインタのみでrdi、サイズ情報はありません。これは、1024よりも小さい配列でそれを呼び出すことはありませんで未定義の動作を避けるために、プログラマ次第です
ピーター・コルド

1
あなたが話していることは何もプレーンなC ++配列ではありません(で割り当て、newで手動で削除delete、境界チェックなし)。あなたは、することができます(ほとんどのソフトウェアのような)くだらない肥大化したASM /機械語コードを生成するためにC ++を使用し、それはプログラマのせいではなく、C ++の。allocaスタックスペースを配列として割り当てることもできます。
Peter Cordes

1
gcc.godbolt.orgの例をリンクしg++ -O3て、プレーン配列の境界チェックコードを生成するか、他のことを行ってください。C ++は、それがはるかに簡単に肥大化したバイナリを生成することができる(実際には、あなたは注意する必要がありませんあなたは、パフォーマンスを目指している場合に)、それは文字通り避けられません。C ++がどのようにasmにコンパイルされるかを理解していれば、手動で作成するよりも多少劣るが、手動で管理するよりも大きなスケールでインライン化と定数伝​​播を行うコードを取得できます。
Peter Cordes

-3

コンパイラが大量のOOサポートコードを生成する場合、アセンブリはより高速になる可能性があります。

編集:

反対投票者へ:OPは、「C ++に集中して、アセンブリ言語を忘れるべきですか?」と書いた。そして私は私の答えを待ちます。特にメソッドを使用する場合は、OOが生成するコードを常に監視する必要があります。アセンブリ言語を忘れないということは、OOコードが生成するアセンブリを定期的に確認することを意味します。これは、優れたパフォーマンスのソフトウェアを作成するために必要だと思います。

実際、これはオブジェクト指向だけでなく、すべてのコンパイル可能なコードに関係します。


2
-1:OO機能が使用されていないようです。あなたの議論は「コンパイラが100万のNOPを追加した場合、アセンブリもより高速になる可能性がある」と同じです。
シェールド

私は不明確でした、これは実際にはCの質問です。C ++コンパイラ用のCコードを作成する場合、C ++コードを作成していないため、オブジェクト指向のものはありません。実際のC ++で記述を開始したら、オブジェクト指向の要素を使用して、コンパイラーがオブジェクト指向のサポートコードを生成しないようにするには、十分な知識が必要です。
Olof Forshell、2012年

あなたの答えは質問についてではありませんか?(また、明確には答え、ないコメントで行くコメントはありません予告、通知、または歴史を持ついつでも削除することができます。。
ダックMooing

1
オブジェクト指向の「サポートコード」が正確に何を意味するのかわからない。もちろん、RTTIなどを大量に使用する場合、コンパイラーはそれらの機能をサポートするために多くの追加命令を作成する必要があります–しかし、RTTIの使用を承認するのに十分な高レベルの問題は複雑すぎて、アセンブリで書き込み可能ではありません。もちろん、できることは、OOとして抽象外部インターフェースのみを記述し、パフォーマンスが最適化された純粋な手続き型コードにディスパッチすることが重要な場合です。ただし、アプリケーションによっては、C、Fortran、CUDA、または仮想継承のない単純なC ++の方が、ここでのアセンブリよりも優れている場合があります。
leftaroundabout

2
いいえ、可能性は低いです。C ++にはオーバーヘッドゼロのルールと呼ばれるものがあり、これはほとんどの場合に当てはまります。OOの詳細-最終的にコードの可読性が向上し、コード品質が向上し、コーディング速度が向上し、堅牢性が向上することがわかります。また、組み込みの場合-C ++を使用すると、より多くの制御が可能になるため、組み込み+ OO Javaの方法ではコストがかかります。
Zane
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.