アセンブリはCより速いですか?


475

アセンブラーを知っている理由の1つは、場合によっては、高水準言語(特にC)でコードを記述するよりもパフォーマンスの高いコードを記述するために使用できることです。ただし、これは完全に誤りではありませんが、実際にアセンブラを使用してよりパフォーマンスの高いコードを生成できるケースは非常にまれであり、アセンブリに関する専門知識と経験が必要であると何度も述べたと聞いています。

この質問は、アセンブラー命令がマシン固有で移植不可能であるという事実、またはアセンブラーの他の側面のいずれにも当てはまりません。もちろん、これ以外にもアセンブリを理解することには十分な理由がありますが、これは例やデータを求める特定の質問であり、アセンブラと高水準言語についての幅広い談話ではありません。

最新のコンパイラを使用して適切に記述されたCコードよりもアセンブリが高速になる場合の具体的な例を誰かが提供できますか?プロファイリングの証拠でその主張をサポートできますか?私はこれらのケースが存在することを確信していますが、いくつかの論争のポイントであると思われるので、これらのケースがどれほど難解であるかを正確に知りたいです。


17
実際、コンパイルされたコードを改善することは非常に簡単です。アセンブリ言語とCに関する確かな知識を持つ人なら誰でも、生成されたコードを調べることでこれを確認できます。簡単なのは、コンパイルされたバージョンで使い捨てレジスタが不足したときに最初に落ちるパフォーマンスの問題です。大規模なプロジェクトでは、平均してコンパイラーは人間よりはるかに優れていますが、適切なサイズのプロジェクトでは、コンパイルされたコードでパフォーマンスの問題を見つけることは難しくありません。
old_timer

14
実際、短い答えは次のとおりです。アセンブラは常に Cの速度と同じかそれよりも高速です。理由は、Cなしでアセンブリを作成できるが、アセンブリなしでCを作成できないことです(バイナリ形式では、以前は「マシンコード」と呼ばれる日)。そうは言っても、長い答えは次のとおりです。Cコンパイラは、通常は考えないことについて最適化し、「考える」のが上手なので、それは実際にはスキルに依存しますが、通常はCコンパイラに勝つことができます。それはまだ考えてアイデアを得ることができないソフトウェアだけです。マクロを使用していて忍耐力がある場合は、ポータブルアセンブラを作成することもできます。

11
この質問への回答は「意見に基づく」必要があることに強く反対します。彼らは非常に客観的である可能性があります。これは、それぞれが長所を持ち、引き合うお気に入りのペット言語のパフォーマンスを比較しようとするようなものではありません。これは、コンパイラが私たちをどれだけ遠ざけることができるか、そしてどの点から引き継ぐべきかを理解することの問題です。
jsbueno

21
私のキャリアの早い段階で、私はソフトウェア会社で多くのCおよびメインフレームアセンブラーを書いていました。私の仲間の1つは、「アセンブラー純粋主義者」(すべてがアセンブラでなければなりません)と呼んでいたものでした。そのため、アセンブラで書くよりもCで高速に実行できる特定のルーチンを書くことができました。勝った。けれどもそれを締めくくるために、私が勝った後、私は彼に2回目の賭けが欲しいと言いました-私はアセンブラーで以前の賭けで彼を打ち負かしたCプログラムよりも速い何かを書くことができると言いました。私もそれを勝ち取り、そのほとんどが何よりもプログラマーのスキルと能力に起因することを証明しました。
Valerie R

3
脳に-O3フラグがない限り、Cコンパイラに最適化を任せたほうがいいでしょう:-)
paxdiablo

回答:


272

これが実際の例です:固定小数点は古いコンパイラで乗算されます。

これらは、浮動小数点のないデバイスで便利なだけでなく、予測可能なエラーで32ビットの精度を提供するため、精度に関して優れています(浮動小数点は23ビットしかなく、精度の低下を予測することは困難です)。つまり、均一に近い相対精度ではなく、範囲全体で均一な絶対精度精度(float)。


最新のコンパイラーはこの固定小数点の例を適切に最適化しているため、コンパイラー固有のコードを必要とする最新の例については、


Cには完全乗算演算子がありません(Nビット入力からの2Nビットの結果)。Cでそれを表現する通常の方法は、入力をより広い型にキャストし、コンパイラーが入力の上位ビットが興味深いものではないことを認識することを期待することです。

// on a 32-bit machine, int can hold 32-bit fixed-point integers.
int inline FixedPointMul (int a, int b)
{
  long long a_long = a; // cast to 64 bit.

  long long product = a_long * b; // perform multiplication

  return (int) (product >> 16);  // shift by the fixed point bias
}

このコードの問題は、C言語で直接表現できない処理を行うことです。2つの32ビット数値を乗算して64ビットの結果を取得し、その結果、中央の32ビットを返します。ただし、Cではこの乗算は存在しません。できることは、整数を64ビットにプロモートし、64 * 64 = 64乗算を行うことだけです。

ただし、x86(およびARM、MIPSなど)は、単一の命令で乗算を実行できます。一部のコンパイラは、この事実を無視して、ランタイムライブラリ関数を呼び出して乗算を行うコードを生成していました。多くの場合、16シフトはライブラリルーチンによって行われます(x86もこのようなシフトを実行できます)。

したがって、乗算のために1つまたは2つのライブラリー呼び出しが残っています。これは深刻な結果をもたらします。シフトが遅くなるだけでなく、レジスターは関数呼び出し全体で保存する必要があり、インライン化とコード展開の助けにもなりません。

(インライン)アセンブラーで同じコードを書き換えると、速度が大幅に向上します。

これに加えて、ASMの使用は問題を解決するための最良の方法ではありません。ほとんどのコンパイラでは、Cでそれらを表現できない場合に、一部のアセンブラ命令を組み込み形式で使用できます。たとえば、VS.NET2008コンパイラは、32 * 32 = 64ビットmulを__emulとして、64ビットシフトを__ll_rshiftとして公開します。

組み込み関数を使用すると、Cコンパイラが何が起こっているのかを理解できるように関数を書き換えることができます。これにより、コードをインライン化し、レジスタを割り当て、共通の部分式を削除し、定数の伝播を行うこともできます。あなたは巨大になるでしょうやり方という手書きのアセンブラコードよりもパフォーマンスの改善を。

参考:VS.NETコンパイラの固定小数点mulの最終結果は次のとおりです。

int inline FixedPointMul (int a, int b)
{
    return (int) __ll_rshift(__emul(a,b),16);
}

固定小数点除算のパフォーマンスの違いはさらに大きくなります。いくつかのasm行を記述することにより、除算の重い固定小数点コードに対して最大10倍の改善がありました。


Visual C ++ 2013を使用すると、両方の方法で同じアセンブリコードが提供されます。

2007年のgcc4.1は、純粋なCバージョンも適切に最適化します。(Godboltコンパイラエクスプローラーには以前のバージョンのgccがインストールされていませんが、おそらく古いGCCバージョンでも組み込み関数なしでこれを行うことができます。)

Godboltコンパイラーエクスプローラーの x86(32ビット)およびARMのソース+ asmを参照してください。(残念ながら、単純な純粋なCバージョンから不良コードを生成するのに十分古いコンパイラーはありません。)


現代のCPUは、Cはのための演算子を持っていないことを行うことができますすべてで、同様popcntまたはビットスキャン最初または最後のセットビットを見つけること。(POSIXにはffs()関数がありますが、そのセマンティクスはx86 bsf/と一致しません。https://en.wikipedia.org/wiki/Find_first_setをbsr参照してください)。

一部のコンパイラーは、整数の設定ビット数をカウントするループを認識し、それをpopcnt命令にコンパイルすることができます(コンパイル時に有効になっている場合)が__builtin_popcnt、GNU C、またはx86で使用する方がはるかに信頼性が高いSSE4.2でハードウェアをターゲットにする:_mm_popcnt_u32から<immintrin.h>

またはC ++では、に割り当ててstd::bitset<32>を使用します.count()。(これは、標準ライブラリを通じてpopcountの最適化された実装を移植可能に公開する方法を言語が見つけた場合であり、常に正しいものにコンパイルされ、ターゲットがサポートするものを利用できます。)httpsも参照してください。://en.wikipedia.org/wiki/Hamming_weight#Language_support

同様に、一部のC実装でntohlコンパイルbswap(エンディアン変換用のx86 32ビットバイトスワップ)できます。


組み込み関数または手書きのasmのもう1つの主要な領域は、SIMD命令による手動のベクトル化です。コンパイラーはdst[i] += src[i] * 10.0;、のような単純なループでは問題ありませんが、状況が複雑になると、多くの場合、問題が発生するか、自動ベクトル化がまったく行われません。たとえば、SIMDを使用してatoiを実装する方法のようなものを取得することはほとんどありませんか?コンパイラによってスカラーコードから自動的に生成されます。


6
{x = c%d; y = c / d;}、コンパイラはそれを単一のdivまたはidivにするのに十分賢いですか?
JensBjörnhager、

4
実際、優れたコンパイラーは、最初の関数から最適なコードを生成します。組み込みやインラインアセンブリソースコードを不明瞭にしても、まったくメリットがないのは、最善の方法ではありません。
怠け者

65
こんにちはSlackerさん、これまでにタイムクリティカルなコードに取り組む必要がなかったと思います...インラインアセンブリは*大きな違いを生むことができます。また、コンパイラにとって、組み込み関数はCの通常の算術演算と同じです。それが組み込み関数のポイントです。欠点に対処する必要なく、アーキテクチャ機能を使用できます。
Nils Pipenbrinck 2010

6
@slacker実際、ここのコードは非常に読みやすくなっています。インラインコードは、メソッドのシグネチャを読み取るのが不安定な1つの一意の操作を実行します。あいまいな命令を使用した場合、コードの可読性が低下するのはゆっくりです。ここで重要なのは、明確に識別可能な操作を1つだけ実行するメソッドがあることです。これは、これらのアトミック関数を読み取り可能なコードを生成するための最良の方法です。ちなみに、これは/ *(a * b)>> 16 * /のような小さなコメントをすぐに説明することはできません。
デレクソン2013

5
公平を期すために、これは少なくとも今日の例では良くないものです。Cコンパイラは、言語が直接提供していない場合でも、長い間32x32-> 64の乗算を行うことができました。32ビットの引数を64ビットにキャストしてから乗算するときに、完全な64ビット乗算を行いますが、32x32-> 64で十分です。私がチェックしたところ、現在のバージョンclang、gcc、MSVCのすべてがこれを正しく行っています。これは新しいことではありません。コンパイラの出力を見て、10年前にこれに気付いたことを覚えています。
BeeOnRope 2018年

143

何年も前に私は誰かにCでプログラムするように教えていました。演習ではグラフィックを90度回転させました。彼は、主に乗算や除算などを使用していたため、完了するまでに数分かかったソリューションを返しました。

ビットシフトを使用して問題を再キャストする方法を彼に示し、処理時間は彼が持っていた非最適化コンパイラーで約30秒になりました。

最適化コンパイラを入手したばかりで、同じコードが5秒未満でグラフィックを回転させました。コンパイラーが生成しているアセンブリー・コードを見て、そこでわかったことから、そこでアセンブラーを書く日々は終わりました。


3
はい、これは1ビットのモノクロシステムでした。具体的には、Atari STのモノクロイメージブロックでした。
リルバーン、2009

16
最適化コンパイラは元のプログラムまたはあなたのバージョンをコンパイルしましたか?
するThorbjörnRavnアンデルセン

どのプロセッサで?8086では、8x8ローテートの最適なコードは、SIを使用して16ビットのデータでDIをロードadd di,di / adc al,al / add di,di / adc ah,ahし、8個の8ビットレジスタすべてについて繰り返し、次に8個すべてのレジスタを再度実行し、手順3全体を繰り返します。より多くの時間、そして最後にax / bx / cx / dxに4ワードを保存します。アセンブラがそれに近づくことは決してありません。
スーパーキャット2018年

1
コンパイラーが8x8ローテートに最適なコードの1つまたは2つの要素内に入る可能性が高いプラットフォームは、実際には考えられません。
スーパーキャット2018年

65

ほとんどの場合、コンパイラーが浮動小数点コードを検出すると、古い不良コンパイラーを使用している場合は手書きバージョンの方が速くなります。(2019の更新:これは、最近のコンパイラーには一般的に当てはまりません。 特にx87以外のものをコンパイルする場合、スカラー計算用のSSE2またはAVX、またはx87とは異なり、フラットFPレジスタセットを使用する非x86を使用すると、コンパイラーの時間が短縮されます。レジスタスタック。)

主な理由は、コンパイラーが堅牢な最適化を実行できないことです。MSDNのこの記事を参照してください件に関する議論については、。これは、アセンブリバージョンがCバージョン(VS2K5でコンパイル)の2倍の速度である例です。

#include "stdafx.h"
#include <windows.h>

float KahanSum(const float *data, int n)
{
   float sum = 0.0f, C = 0.0f, Y, T;

   for (int i = 0 ; i < n ; ++i) {
      Y = *data++ - C;
      T = sum + Y;
      C = T - sum - Y;
      sum = T;
   }

   return sum;
}

float AsmSum(const float *data, int n)
{
  float result = 0.0f;

  _asm
  {
    mov esi,data
    mov ecx,n
    fldz
    fldz
l1:
    fsubr [esi]
    add esi,4
    fld st(0)
    fadd st(0),st(2)
    fld st(0)
    fsub st(0),st(3)
    fsub st(0),st(2)
    fstp st(2)
    fstp st(2)
    loop l1
    fstp result
    fstp result
  }

  return result;
}

int main (int, char **)
{
  int count = 1000000;

  float *source = new float [count];

  for (int i = 0 ; i < count ; ++i) {
    source [i] = static_cast <float> (rand ()) / static_cast <float> (RAND_MAX);
  }

  LARGE_INTEGER start, mid, end;

  float sum1 = 0.0f, sum2 = 0.0f;

  QueryPerformanceCounter (&start);

  sum1 = KahanSum (source, count);

  QueryPerformanceCounter (&mid);

  sum2 = AsmSum (source, count);

  QueryPerformanceCounter (&end);

  cout << "  C code: " << sum1 << " in " << (mid.QuadPart - start.QuadPart) << endl;
  cout << "asm code: " << sum2 << " in " << (end.QuadPart - mid.QuadPart) << endl;

  return 0;
}

そして、デフォルトのリリースビルドを実行している私のPCからのいくつかの番号*

  C code: 500137 in 103884668
asm code: 500137 in 52129147

興味深いことに、ループをdec / jnzと交換しましたが、タイミングに違いはありませんでした。記憶が限られているため、他の最適化よりも小さくなっていると思います。(編集者注:FPレイテンシのボトルネックは、loop。奇数/偶数要素に対して2つのKahan加算を並列に実行し、最後にそれらを追加すると、おそらく2倍高速化できます。 )

おっと、少し異なるバージョンのコードを実行していましたが、間違った方法で数値が出力されました(つまり、Cの方が高速でした!)。結果を修正および更新しました。


20
またはGCCでは、フラグを使用して、コンパイラーの浮動小数点最適化の手を解放することができます(無限またはNaNで何もしないことを約束する限り)-ffast-math-Ofast現在、と同等の最適化レベル-O3 -ffast-mathがありますが、将来的には、コーナーケース(IEEE NaNに依存するコードなど)で誤ったコード生成を引き起こす可能性がある最適化がさらに含まれる可能性があります。
David Stone、

2
ええ、浮動小数点は可換ではありません、コンパイラはあなたが書いたこと、基本的に@DavidStoneが言ったことを正確に行わなければなりません。
アレックティール2014年

2
SSE数学を試しましたか?MSがx86_64で完全にx87を、x86で80ビットlong doubleを完全に破棄した理由の1つはパフォーマンスでした
phuclv 2014年

4
@Praxeolitic:FP addは可換(a+b == b+a)ですが、結合ではありません(操作の並べ替え、したがって中間体の丸めは異なります)。re:このコード:コメントされていないx87とloop命令はfast asmの非常に素晴らしいデモンストレーションだとは思わない。 loopFPレイテンシのため、実際にはボトルネックにはならないようです。彼がFP演算をパイプライン化しているかどうかはわかりません。x87は人間にとって読みにくいものです。fstp results最後の2つのインスツルメントは明らかに最適ではありません。スタックから余分な結果をポップすることは、非ストアで行う方が良いでしょう。同様fstp st(0)IIRC。
Peter Cordes

2
@PeterCordes:加算可換にすることの興味深い結果は、0 + xとx + 0は互いに同等ですが、どちらも常にxと同等ではないということです。
supercat

58

特定の例やプロファイラーの証拠を提供せずに、コンパイラー以上の知識がある場合は、コンパイラーよりも優れたアセンブラーを作成できます。

一般的なケースでは、最新のCコンパイラーは問題のコードを最適化する方法についてより多くのことを知っています。プロセッサーのパイプラインがどのように機能するかを知っている、人間ができるよりも速く命令を並べ替えることができるなど、それは基本的に同じです。ほとんどの人間よりも問題空間内の検索を高速化できるという理由だけで、ボードゲームなどの最高の人間のプレーヤーと同等かそれ以上の優れたコンピューター。理論的には特定のケースでコンピューターと同じように実行できますが、同じ速度で実行することはできないため、いくつかのケースでは実行不可能になります(つまり、コンパイラーが書き込みを行おうとすると、確実にパフォーマンスが向上します。アセンブラーのいくつかのルーチン以上)。

一方、コンパイラーがそれほど多くの情報を持っていない場合もあります。主に、コンパイラーが知らないさまざまな形式の外部ハードウェアで作業する場合です。主な例はおそらくデバイスドライバであり、問​​題のハードウェアに関する人間の親密な知識とアセンブラを組み合わせると、Cコンパイラよりも優れた結果が得られます。

他の人は、私が上記の段落で話している特別な目的の命令に言及しました-コンパイラが命令を制限しているかまったく知らないため、人間がより高速なコードを書くことができるようにする命令。


一般的に、このステートメントは真実です。コンパイラーはDWIWに最適ですが、リアルタイムのパフォーマンスが必須である場合、一部のエッジケースでは手動コーディングアセンブラーが仕事を完了します。
スポールソン2009

1
@Liedman:「人間ができるよりも速く命令を並べ替えることができる」。OCamlは高速であることが知られており、驚くべきことに、そのネイティブコードコンパイラーocamloptはx86での命令のスケジューリングをスキップし、実行時により効率的に並べ替えることができるため、代わりにCPUに任せます。
Jon Harrop、2012年

1
最近のコンパイラーは多くのことを行い、手作業で行うには時間がかかりすぎますが、完璧とは言えません。gccまたはllvmのバグトラッカーで「missed-optimization」バグを検索します。沢山あります。また、asmで書き込む場合、コンパイラーが証明するのが難しい「この入力は負にできません」などの前提条件をより簡単に利用できます。
Peter Cordes

48

私の仕事では、アセンブリを知って使用する3つの理由があります。重要度の高い順に:

  1. デバッグ-バグや不完全なドキュメントがあるライブラリコードをよく入手します。アセンブリレベルで介入することで、それが何をしているのかを理解します。私はこれを週に一回ほどしなければなりません。また、C / C ++ / C#の慣用的なエラーを目で確認できない問題をデバッグするためのツールとしても使用します。アセンブリを見ることはそれを通り過ぎます。

  2. 最適化-コンパイラーは最適化においてかなりうまく機能しますが、私はほとんどとは異なる球場でプレーします。通常は次のようなコードで始まる画像処理コードを記述します。

    for (int y=0; y < imageHeight; y++) {
        for (int x=0; x < imageWidth; x++) {
           // do something
        }
    }

    「何かを行う」は、通常、数百万回(つまり、3〜30回)のオーダーで発生します。その「何かを行う」フェーズでサイクルをこすることにより、パフォーマンスの向上は大幅に拡大されます。私は通常そこから始めません-通常は最初に機能するコードを書くことから始め、次にCをリファクタリングして自然に良くなるように最善を尽くします(より良いアルゴリズム、ループ内の負荷が少ないなど)。私は通常、何が起こっているかを確認するためにアセンブリを読み取る必要があり、それを書く必要はほとんどありません。私はこれを多分2か3か月ごとに行います。

  3. 言語が許さない何かをする。これには、プロセッサアーキテクチャと特定のプロセッサ機能の取得、CPUにないフラグへのアクセス(実際には、Cからキャリーフラグへのアクセスを許可してほしい)などがあります。これは、おそらく1年または2年に1回です。


ループをタイリングしませんか?:-)
Jon Harrop、2012年

1
@plinth:「スクレイピングサイクル」とはどういう意味ですか?
lang2 2013

@ lang2:これは、内部ループで費やされた余分な時間をできるだけ多く取り除くことを意味します-コンパイラーが引き出すことができなかったすべてのこと。インナーなど
台座

1
データを1回だけパスする場合は、ループタイリングは不要のようです。
James M. Lay、2015年

@ JamesM.Lay:すべての要素に1回しか触れない場合、より良いトラバーサル順序で空間的な局所性を得ることができます。(たとえば、キャッシュラインごとに1つの要素を使用して行列の列をループダウンする代わりに、タッチしたキャッシュラインのすべてのバイトを使用します。)
Peter Cordes

42

コンパイラーがサポートしていない特定の目的の命令セットを使用する場合のみ。

複数のパイプラインと予測分岐を備えた最新のCPUの計算能力を最大化するには、a)人間による書き込みがほとんど不可能b)維持がさらに困難になるような方法でアセンブリプログラムを構造化する必要があります。

また、より優れたアルゴリズム、データ構造、およびメモリ管理により、アセンブリで行うことができるマイクロ最適化よりも少なくとも1桁高いパフォーマンスが得られます。


4
+1、最後の文は実際にはこの議論に属していませんが、アセンブラが機能するのは、アルゴリズムなどの可能な改善がすべて実現された後でないと考えられません。
mghie 2009

18
@Matt:手書きのASMは、多くの場合、EEが動作する小さなCPUの一部で非常に優れています。
Zan Lynx、

5
「特定の目的の命令セットを使用する場合のみ」?? おそらく、これまでに手で最適化されたasmコードを記述したことがないでしょう。作業しているアーキテクチャについてある程度親密な知識があれば、コンパイラよりも優れたコード(サイズと速度)を生成できる可能性が高くなります。明らかに、@ mghieがコメントしたように、問題に対応できる最高のアルゴを常にコーディングし始めます。非常に優れたコンパイラであっても、コンパイラを最適なコンパイル済みコードに導くような方法でCコードを記述する必要があります。そうでない場合、生成されたコードは最適ではなくなります。
ysap

2
@ysap-実際のコンピュータ(実は小さなパワーエンベデッドチップではない)では、「最適な」コードは高速ではありません。大規模なデータセットの場合、メモリアクセスとページフォールトによってパフォーマンスが制限されるためです(大規模なデータセットがない場合、これはどちらの方法でも高速になり、最適化する意味がありません)-当時、私はほとんどC#(cではなく)で作業しており、メモリマネージャーの圧縮によるパフォーマンスの向上を実現しています-ガベージコレクション、圧縮、およびJITコンパイルのオーバーヘッドに重みを付けます。
ニール

4
+1(コンパイラー(特にJIT)は、実行されるハードウェアに最適化されている場合、人間よりも優れた機能を果たすことができる)
セバスチャン

38

Cは8ビット、16ビット、32ビット、64ビットデータの低レベル操作に「近い」ものですが、特定のアセンブリ命令でエレガントに実行できることが多い、Cでサポートされていない数学演算がいくつかありますセット:

  1. 固定小数点乗算:2つの16ビット数値の積は32ビット数値です。しかし、Cの規則では、2つの16ビット数の積は16ビット数であり、2つの32ビット数の積は32ビット数である(どちらの場合も下半分)。16x16乗算または32x32乗算の半分が必要な場合は、コンパイラーでゲームをプレイする必要があります。一般的な方法は、必要以上のビット幅にキャストし、乗算し、シフトダウンして、キャストバックすることです。

    int16_t x, y;
    // int16_t is a typedef for "short"
    // set x and y to something
    int16_t prod = (int16_t)(((int32_t)x*y)>>16);`

    この場合、コンパイラーは、16x16乗算の上半分を実際に取得し、マシンのネイティブ16x16乗算で正しいことを実行しようとしていることを認識できるほど賢いかもしれません。または、それは愚かであり、製品の16ビットしか必要としないため、32x32乗算を実行するためにライブラリ呼び出しが必要になる場合があります。

  2. 特定のビットシフト操作(ローテーション/キャリー):

    // 256-bit array shifted right in its entirety:
    uint8_t x[32];
    for (int i = 32; --i > 0; )
    {
       x[i] = (x[i] >> 1) | (x[i-1] << 7);
    }
    x[0] >>= 1;

    これはCであまりエレガントではありませんが、繰り返しになりますが、コンパイラーがあなたのやっていることを実現するのに十分スマートでなければ、コンパイラーは多くの「不要な」作業を行うことになります。多くのアセンブリ命令セットでは、キャリーレジスタの結果を使用して左または右に回転またはシフトできるため、上記の34命令で実行できます。配列の先頭へのポインターのロード、キャリーのクリア、および32 8の実行ポインタで自動インクリメントを使用して、ビットを右シフトします。

    別の例として、アセンブリでエレガントに実行れる線形フィードバックシフトレジスタ(LFSR)があります。アルゴリズム)、結果のキャリーが1の場合、多項式を表すビットパターンでXORします。

そうは言っても、深刻なパフォーマンスの制約がない限り、これらの手法に頼ることはありません。他の人が言ったように、アセンブリはCコードよりも文書化/デバッグ/テスト/保守がはるかに困難です。パフォーマンスの向上には、いくつかの重大なコストが伴います。

編集: 3.オーバーフロー検出はアセンブリで可能です(実際にはCでは実行できません)。これにより、一部のアルゴリズムがはるかに簡単になります。


23

簡潔な答え?時々。

技術的にはすべての抽象化にはコストがかかり、プログラミング言語はCPUの動作方法の抽象化です。Cは非常に近いです。数年前、私は自分のUNIXアカウントにログオンして次のような幸運のメッセージを受け取ったとき(そのようなものが人気だったとき)、大声で笑ったことを覚えています。

Cプログラミング言語-アセンブリ言語の柔軟性とアセンブリ言語の能力を組み合わせた言語。

それが本当であるので、それはおかしいです:Cは移植可能なアセンブリ言語のようなものです。

アセンブリ言語は実行するだけで実行できることは注目に値します。ただし、Cとそれが生成するアセンブリ言語の間にコンパイラーがあり、Cコードの速度がコンパイラーの性能に大きく影響するため、これは非常に重要です。

gccが登場したとき、それが非常に人気になった理由の1つは、多くの商用UNIXフレーバーに同梱されていたCコンパイラよりもはるかに優れていることでした。ANSI C(このK&R Cのゴミはありません)だけでなく、より堅牢で、通常はより優れた(より速い)コードを生成しました。常にではないが、頻繁に。

Cには客観的な標準がないため、Cとアセンブラの速度に関する包括的な規則はないので、これらすべてを説明します。

同様に、アセンブラは、実行しているプロセッサ、システム仕様、使用している命令セットなどによって大きく異なります。歴史的に、CISCとRISCの2つのCPUアーキテクチャファミリがありました。CISCの最大のプレーヤーは、現在もIntel x86アーキテクチャ(および命令セット)です。RISCはUNIXの世界(MIPS6000、Alpha、Sparcなど)を支配していました。CISCは心と心の戦いに勝利しました。

とにかく、私が若い開発者だった頃の人気のある知恵は、手書きのx86はCよりもはるかに高速であることが多いということでした。一方、RISCはコンパイラー向けに設計されているようで、誰も(私は知っていました)Sparcアセンブラーを書いた人はいません。そのような人々は確かに存在していたと思いますが、間違いなく彼らは狂ってしまい、今では制度化されています。

命令セットは、同じプロセッサフ​​ァミリでも重要なポイントです。特定のIntelプロセッサーには、SSEからSSE4までの拡張機能があります。AMDには独自のSIMD命令がありました。Cのようなプログラミング言語の利点は、誰かがライブラリを作成できるため、実行しているプロセッサに合わせて最適化されたことです。それはアセンブラで大変な作業でした。

アセンブラーで行うことができる最適化はまだありますが、コンパイラーが作成することはできません。適切に作成されたアセンブラーのアルゴリズムは、Cと同等かそれ以上です。より大きな問題は、それだけの価値があるかどうかです。

結局のところ、アセンブラは当時の製品であり、CPUサイクルが高価な時代にはより人気がありました。今日、製造に5〜10ドルかかるCPU(Intel Atom)は、だれでも望むことができるほとんどすべてのことを実行できます。最近アセンブラーを書く唯一の本当の理由は、オペレーティングシステムの一部(Linuxカーネルの大部分がCで書かれている場合でも)、デバイスドライバー、場合によっては組み込みデバイス(Cがそこを支配する傾向があるが)も)など。または単にキック(これはややマゾヒスティックです)。


Acornマシン(90年代初頭)での選択言語としてARMアセンブラを使用した多くの人々がいました。IIRCは、小さなリスクの高い命令セットにより、より簡単で楽しいものになったと述べています。しかし、これはCコンパイラがAcornに遅れて到着したためであり、C ++コンパイラが完成していないためだと思います。
アンドリューM

3
「... Cには主観的な基準がないため。」あなたは目的を意味します。
トーマス

@AndrewM:ええ、私は約10年間BASICとARMアセンブラーで混合言語アプリケーションを書きました。その間にCを学びましたが、Cはアセンブラと同じくらい面倒で遅いため、あまり役に立ちませんでした。Norcroftはいくつかの素晴らしい最適化を行いましたが、条件付き命令セットは当時のコンパイラーにとって問題であったと思います。
Jon Harrop、2012年

1
@AndrewM:まあ、実際にはARMは一種のRISCを逆にしたものです。他のRISC ISAは、コンパイラーが使用するものから始めて設計されました。ARM ISAは、CPUが提供するもの(バレルシフター、条件フラグ→すべての命令でそれらを公開しましょう)から設計されているようです。
ninjalj 2013

16

もう適用されないかもしれないがオタクの喜びのための使用例:Amigaでは、CPUとグラフィックス/オーディオチップはRAMの特定の領域(具体的には最初の2MBのRAM)にアクセスするために戦います。したがって、RAMが2MB以下の場合、複雑なグラフィックスとサウンドの再生を表示すると、CPUのパフォーマンスが低下します。

アセンブラでは、グラフィックス/オーディオチップが内部的にビジーなとき(バスが空いているとき)にのみCPUがRAMにアクセスしようとするような巧妙な方法でコードをインターリーブできます。したがって、命令を並べ替え、CPUキャッシュ、バスタイミングを巧みに使用することで、すべてのコマンドの時間を計らなければならず、さまざまな場所にNOPを挿入しなければならなかったため、高レベルの言語では単純に不可能であったいくつかの効果を達成できました。お互いのレーダーからチップ。

これが、CPUのNOP(操作なし-何もしない)命令が実際にアプリケーション全体をより高速に実行できるもう1つの理由です。

[編集]もちろん、テクニックは特定のハードウェア設定に依存します。これが多くのAmigaゲームがより高速なCPUに対応できなかった主な理由でした。命令のタイミングがずれていました。


Amigaには、16 MBのチップRAMがありませんでした。チップセットによっては、512 kBから2 MB程度です。また、多くのAmigaゲームは、あなたが説明したようなテクニックが原因で、より高速なCPUでは動作しませんでした。
bk1e 2009

1
@ bk1e-Amigaはさまざまなモデルのコンピューターを製造しましたが、私の場合、Amiga 500は512KのRAMを1Mに拡張して出荷されました。amigahistory.co.uk/amiedevsys.htmlは、128Meg Ramを備えたamigaです
David Waters

@ bk1e:私は正直しています。私のメモリは失敗するかもしれませんが、チップRAMは最初の24ビットアドレス空間(つまり16MB)に制限されていませんでしたか?そしてFastはその上にマッピングされましたか?
アーロン・ディグラ2009

@Aaron Digulla:ウィキペディアにチップ/高速/低速RAMの違いに関する詳細情報があります:en.wikipedia.org/wiki/Amiga_Chip_RAM
bk1e

@ bk1e:私の間違い。68k CPUには24のアドレスレーンしかなかったので、16MBが頭にありました。
アーロン・ディグラ2009

15

答えではないポイント1。
プログラミングをまったく行っていない場合でも、少なくとも1つのアセンブラー命令セットを知っていると便利です。これは、より多くを知り、より良くなるためのプログラマーの終わりのない探求の一部です。また、フレームワークに足を踏み入れたときに、ソースコードがなく、少なくともおおよその状況がわかっている場合にも役立ちます。また、JavaByteCodeと.Net ILはどちらもアセンブラーに似ているため、理解するのに役立ちます。

コードが少ない場合や時間が長い場合の質問への回答。組み込みチップでの使用に最も役立ちます。これらのチップを対象とするコンパイラーでのチップの複雑さと競争の激しさがバランスを人間に有利に傾ける可能性がある場合です。また、制限付きデバイスの場合、コンパイラーに指示するのが難しい方法でコードサイズ/メモリサイズ/パフォーマンスをトレードオフすることがよくあります。たとえば、このユーザーアクションが頻繁に呼び出されないため、コードサイズが小さく、パフォーマンスが低下しますが、同じように見えるこの他の関数は毎秒使用されるため、コードサイズが大きく、パフォーマンスが向上します。これは、熟練したアセンブリプログラマが使用できるトレードオフのようなものです。

また、Cコンパイルでコードを作成して生成されたアセンブリを確認し、Cコードを変更するか、アセンブリとして微調整して維持することができる多くの中間点があることも付け加えておきます。

私の友人は、現在小型電気モーターを制御するためのチップであるマイクロコントローラーに取り組んでいます。彼は低レベルのcとAssemblyの組み合わせで働いています。彼はかつて私がメインループを48命令から43命令に減らした良い一日について私に話しました。彼はまた、コードが256kチップを満たすように成長し、ビジネスが新しい機能を望んでいるような選択肢に直面していますか?

  1. 既存の機能を削除する
  2. おそらくパフォーマンスを犠牲にして、既存の機能の一部またはすべてのサイズを縮小します。
  3. より高いコスト、より高い電力消費、およびより大きなフォームファクターで、より大きなチップに移行することを推奨します。

かなりのポートフォリオまたは言語、プラットフォーム、アプリケーションの種類を備えた商用開発者として、アセンブリの記述に飛び込む必要性を一度も感じたことはありません。私はそれについて得た知識をいつでも感謝しています。そして、時にはそれにデバッグしました。

「なぜアセンブラを学ぶべきなのか」という質問にはるかに答えたのはわかっていますが、いつそれが速くなるかというより、それがより重要な質問だと思います。

もう一度試してみましょう組み立てについて考えるべきです

  • 低レベルのオペレーティングシステム機能の作業
  • コンパイラに取り組んでいます。
  • 非常に限られたチップ、組み込みシステムなどでの作業

アセンブリを生成されたコンパイラと比較して、どちらがより高速で、より小さく、より優れているかを確認してください。

デビッド。


4
小さなチップ上の組み込みアプリケーションを検討するための+1。ここでソフトウェアエンジニアが多すぎると、組み込みを考慮しないか、それがスマートフォン(32ビット、MB RAM、MBフラッシュ)を意味すると考えます。
マーティン、

1
時間組み込みアプリケーションはその好例です!ハードウェアについての知識が限られているため、コンパイラーがかつて使用していた(そして場合によってはまだ使用していない)奇妙な命令(avr sbiやのような本当に単純な命令であっても)がよくありcbiます。
felixphew

15

誰もこれを言わなかったことに驚いています。strlen()アセンブリで記述された場合、この関数は、はるかに高速です!Cでは、あなたができる最善のことは

int c;
for(c = 0; str[c] != '\0'; c++) {}

アセンブリ中はかなりスピードアップできます:

mov esi, offset string
mov edi, esi
xor ecx, ecx

lp:
mov ax, byte ptr [esi]
cmp al, cl
je  end_1
cmp ah, cl
je end_2
mov bx, byte ptr [esi + 2]
cmp bl, cl
je end_3
cmp bh, cl
je end_4
add esi, 4
jmp lp

end_4:
inc esi

end_3:
inc esi

end_2:
inc esi

end_1:
inc esi

mov ecx, esi
sub ecx, edi

長さはecxです。これは一度に4文字を比較するので、4倍速くなります。eaxとebxの上位ワードを使用すると、前のCルーチンより8倍速くなります。


3
これはstrchr.nfshost.com/optimized_strlen_functionにあるものとどのように比較しますか?
ninjalj

@ninjalj:それらは同じものです:) Cでこの方法で実行できるとは思いませんでした。少し改善できると思います
BlackBear

Cコードの各比較の前には、ビットごとのAND演算がまだあります。コンパイラーはそれを高バイト数と低バイト数の比較に削減するのに十分賢いかもしれませんが、私はそれにお金を賭けません。実際には(word & 0xFEFEFEFF) & (~word + 0x80808080)、ワード内のすべてのバイトがゼロ以外の場合、ゼロであるプロパティに基づいたより高速なループアルゴリズムがあります。
user2310967 14

@MichaWiedenmann true、axの2つの文字を比較した後でbxをロードする必要があります。ありがとう
BlackBear 2017年

14

SIMD命令を使用した行列演算は、おそらくコンパイラが生成したコードよりも高速です。


一部のコンパイラー(私が正しく覚えていれば、VectorC)はSIMDコードを生成するため、それでもおそらくアセンブリー・コードを使用するための引数ではありません。
OregonGhost 2009

コンパイラはSSE対応コードを作成するため、その引数は真ではありません
vartec

5
これらの状況の多くでは、アセンブリの代わりにSSE組み込み関数を使用できます。これにより、コードの移植性が高まり(gccビジュアルc ++、64ビット、32ビットなど)、レジスタの割り当てを行う必要がなくなります。
Laserallan 2009

1
確かにそうですが、質問では、Cの代わりにアセンブリをどこで使用すればよいかは尋ねられませんでした。Cコンパイラがより優れたコードを生成しない場合に、それは言いました。直接SSE呼び出しまたはインラインアセンブリを使用していないCソースを想定しました。
Mehrdad Afshari

9
Mehrdadは正しいです。SSEを正しく理解することはコンパイラにとって非常に困難であり、明らかな(人間にとって)状況でさえ、ほとんどのコンパイラはそれを採用していません。
Konrad Rudolph、

13

何年も前だったので具体的な例を示すことはできませんが、手書きのアセンブラがどのコンパイラよりも優れている場合がたくさんありました。理由:

  • レジスタに引数を渡して、呼び出し規約から逸脱することができます。

  • レジスタの使用方法を注意深く検討し、変数をメモリに格納しないようにすることができます。

  • ジャンプテーブルなどの場合、インデックスの境界チェックを行う必要を回避できます。

基本的に、コンパイラーは最適化のかなり良い仕事をします、そしてそれはほとんど常に「十分」ですが、あなたがコードを知っているので、あなたがすべての単一のサイクルに大金を払っているいくつかの状況(グラフィックスレンダリングのような)でショートカットをとることができます。 、それは安全側でなければならないのでコンパイラができなかった場所。

実際、グラフィックスレンダリングコードのいくつかを聞いたことがあります。これは、線描画ルーチンやポリゴン塗りつぶしルーチンなどのルーチンが、実際にスタック上にマシンコードの小さなブロックを生成し、そこで実行して、継続的な意思決定を回避していることです。線のスタイル、幅、パターンなどについて

とは言っても、コンパイラーにしてほしいのは、良いアセンブリコードを生成することですが、あまり賢くはありません。実際、私がFortranで嫌いなことの1つは、コードを「最適化」しようとしてコードをスクランブルすることです。

通常、アプリにパフォーマンスの問題がある場合、それは無駄な設計が原因です。最近では、アプリ全体がその寿命の1インチ以内で既に調整されていて、まだ十分に速くなく、タイトな内部ループですべての時間を費やしていない限り、アセンブラーのパフォーマンスはお勧めしません。

追加:アセンブリ言語で書かれた多くのアプリを見てきましたが、C、Pascal、Fortranなどの言語に対する主な速度の利点は、プログラマがアセンブラでコーディングするときにはるかに注意していたためです。彼または彼女は、言語に関係なく、1日あたり約100行のコードを、3または400の命令に相当するコンパイラー言語で記述します。


8
+1:「呼び出し規約から逸脱する可能性があります」。C / C ++コンパイラーは、複数の値を返すのが難しい傾向があります。多くの場合、呼び出し側スタックが連続したブロックを構造体に割り当て、呼び出し先がそれを埋めるために参照を渡したsretフォームを使用します。複数の値をレジスターに返す方が数倍高速です。
Jon Harrop、2012年

1
@ジョン:C / C ++コンパイラは、関数がインライン化されたときに問題なく動作します(非インライン化関数はABIに準拠する必要があります。これはCおよびC ++の制限ではなく、リンクモデルです)
Ben Voigt


2
そこにインライン化された関数呼び出しはありません。
Ben Voigt 2014

13

私の経験からのいくつかの例:

  • Cからアクセスできない命令へのアクセス。たとえば、多くのアーキテクチャ(x86-64、IA-64、DEC Alpha、64ビットMIPSまたはPowerPCなど)は、128ビットの結果を生成する64ビット×64ビットの乗算をサポートしています。GCCは最近、そのような指示へのアクセスを提供する拡張機能を追加しましたが、その前にアセンブリが必要でした。また、RSAのようなものを実装する場合、この命令にアクセスすると64ビットCPUに大きな違いが生じる可能性があります。パフォーマンスが4倍向上する場合もあります。

  • CPU固有のフラグへのアクセス。私をよく噛んだのはキャリーフラグです。倍精度の加算を行う場合、CPUキャリービットにアクセスできない場合は、結果を比較してオーバーフローが発生しているかどうかを確認する必要があります。さらに悪いことに、データアクセスの点でかなりシリアルであり、最新のスーパースカラープロセッサのパフォーマンスが低下します。数千のそのような整数を続けて処理する場合、addcを使用できることは大きな勝利です(キャリービットの競合にもスーパースカラーの問題がありますが、最近のCPUはかなりうまく処理します)。

  • SIMD。自動ベクトル化コンパイラーでも比較的単純なケースしか実行できないため、SIMDのパフォーマンスを向上させたい場合は、残念ながら多くの場合、直接コードを記述する必要があります。もちろん、アセンブリの代わりに組み込み関数を使用することもできますが、組み込み関数レベルになったら、基本的にとにかくアセンブリを作成し、コンパイラをレジスタアロケータおよび(名目上)命令スケジューラとして使用します。(コンパイラが関数プロローグを生成できるため、SIMDに組み込み関数を使用する傾向がありますが、関数呼び出し規約などのABI問題に対処する必要なく、Linux、OS X、およびWindowsで同じコードを使用できますそれよりも、SSE組み込みは本当にあまり良くありません-私はそれらの多くの経験を持っていませんがAltivec組み込みはより良く見えます)。ビットスライスAESまたはSIMDエラー修正 -アルゴリズムを分析してそのようなコードを生成できるコンパイラーを想像できますが、そのようなスマートコンパイラーは既存の(最高で)30年以上離れているように思えます。

一方、マルチコアマシンと分散システムは、パフォーマンスの最大のメリットの多くを他の方向にシフトしました。アセンブリの内部ループの書き込みをさらに20%高速化するか、複数のコアにまたがって実行すると300%高速化します。マシンのクラスター全体でそれらを実行します。もちろん、高レベルの最適化(フューチャー、メモ化など)は、CやasmよりもMLやScalaなどの高レベルの言語で実行する方がはるかに簡単であり、パフォーマンスを大幅に向上させることができます。そのため、いつものように、トレードオフがあります。


2
@Dennisです。「もちろん、アセンブリの代わりに組み込み関数を使用できますが、組み込み関数のレベルになったら、基本的に、コンパイラをレジスタアロケータおよび(公称)命令スケジューラとして使用して、アセンブリを作成します。」
ジャックロイド

また、組み込みベースのSIMDコードは、アセンブラーで記述された同じコードよりも読みにくくなる傾向があります。多くのSIMDコードは、コンパイラ組み込み関数が提供するデータ型を使用するPITAである、ベクター内のデータの暗黙的な再解釈に依存しています。
cmaster-2017年

10

画像は数百万のピクセルで構成されている可能性があるため、画像で遊ぶときのようにタイトなループ。座って、限られた数のプロセッサレジスタを最大限に活用する方法を理解することで、違いが生まれます。これが実際のサンプルです。

http://danbystrom.se/2008/12/22/optimizing-away-ii/

次に、プロセッサーには、コンパイラーが煩わしすぎるには特殊化した難解な命令が含まれていることがよくありますが、アセンブラープログラマーがそれらをうまく利用できる場合もあります。たとえば、XLAT命令を見てみましょう。ループでテーブルルックアップを実行する必要があり、テーブルが256バイトに制限されている!

更新:ああ、一般的にループについて話すときに最も重要なことを考えに来てください:コンパイラーは多くの場合、一般的なケースとなる反復の数を知る手がかりがありません!ループが何回も繰り返されること、したがって追加の作業を伴うループに備えることが有益であること、または実際に設定が反復よりも長くかかるほど数回繰り返される場合は、プログラマーのみが知っています。期待された。


3
プロファイル指定最適化は、ループが使用される頻度に関する情報をコンパイラーに提供します。
Zan Lynx、

10

Cの標準がそう言っているからといって、Cはアセンブリコーダーの観点からは不要だと思われることを、あなたが思うよりも頻繁に行う必要があります。

たとえば、整数の昇格。Cでchar変数をシフトしたい場合、通常はコードが実際にそれを実行することを期待します。これは単一ビットのシフトです。

ただし、標準では、コンパイラーがシフトの前にintに符号拡張し、その後結果をcharに切り捨てることを強制するため、ターゲットプロセッサーのアーキテクチャーによってはコードが複雑になる場合があります。


小さなマイクロ向けの高品質なコンパイラーは、何年にもわたって結果に意味のある影響を与えることができない場合に、値の上部を処理することを回避することができました。プロモーションルールは問題を引き起こしますが、ほとんどの場合、コンパイラーがどのコーナーケースが関連し、関連していないかを知る方法がありません。
supercat

9

コンパイラーが生成するものの逆アセンブリを調べていなければ、実際によく書かれたCコードが本当に速いかどうかはわかりません。多くの場合、それを見て、「よく書かれた」が主観的であることがわかります。

したがって、これまでで最速のコードを取得するためにアセンブラーで記述する必要はありませんが、同じ理由でアセンブラーを知ることは確かに価値があります。


2
「そのため、これまでで最速のコードを取得するためにアセンブラーで作成する必要はありません」さて、ささいなことではない場合でも、コンパイラーが最適なことをするのを見たことはありません。経験豊富な人間は、事実上すべてのケースでコンパイラよりも優れています。したがって、「これまでで最速のコード」を得るためには、アセンブラーで書くことが絶対に必要です。
cmaster-モニカを2017年

@cmaster私の経験では、コンパイラの出力はランダムです。時々、それは本当に良くて最適であり、時々、「このゴミがどうやって放出されたのか」です。
シャープトゥース2017年

9

私はすべての答え(30以上)を読んでいると、単純な理由が見つかりませんでした:あなたが読んで練習している場合、アセンブラは速くCよりもマニュアルインテル®64およびIA-32アーキテクチャー最適化リファレンスをその理由はなぜアセンブリかもしれません遅くなるのは、そのような遅いアセンブリを書く人は、最適化マニュアルを読んでいないということです。

Intel 80286の古き良き時代には、各命令は一定のCPUサイクル数で実行されていましたが、1995年にリリースされたPentium Pro以降、Intelプロセッサは複雑なパイプライン処理を使用してスーパースカラーになりました。その前に、1993年に製造されたPentiumでは、UパイプラインとVパイプラインがありました。互いに依存していなければ、1つのクロックサイクルで2つの単純な命令を実行できるデュアルパイプラインでした。しかし、これは、Pentium Proでのアウトオブオーダー実行とレジスタの名前変更が何であるかを比較するものではなく、最近はほとんど変更されていません。

簡単に説明すると、最も速いコードは、命令が以前の結果に依存しない場所です。たとえば、常に(movzxによって)レジスタ全体をクリアするか、add rax, 1代わりに使用するか、inc rax、フラグの以前の状態への依存を削除する。

時間が許せば、アウトオブオーダー実行とレジスタの名前変更について詳しく読むことができます。インターネットには多くの情報があります。

分岐予測、ロードおよびストアユニットの数、micro-opを実行するゲートの数など、他にも重要な問題がありますが、考慮すべき最も重要なことは、アウトオブオーダー実行です。

ほとんどの人は、アウトオブオーダー実行について単に気づいていないため、80286のようなアセンブリプログラムを記述し、コンテキストに関係なく、命令の実行に一定の時間がかかることを期待しています。Cコンパイラはアウトオブオーダー実行を認識し、コードを正しく生成します。そのため、そのような知らない人のコードは遅くなりますが、気づいたら、コードは速くなります。


8

アセンブラの方が速い一般的なケースは、スマートアセンブリプログラマがコンパイラの出力を見て「これはパフォーマンスのクリティカルパスであり、これをより効率的に書くことができる」と言ったときだと思います。ゼロから。


7

それはすべてワークロードに依存します。

日常の操作では、CとC ++で十分ですが、パフォーマンスを向上させるためにアセンブリをほとんど必要とする特定のワークロード(ビデオ(圧縮、解凍、画像効果など)を伴う変換)があります。

また、通常、これらの種類の操作用に調整されたCPU固有のチップセット拡張機能(MME / MMX / SSE /その他)の使用も含まれます。


6

50マイクロ秒ごとに発生する割り込みごとに192ビットまたは256ビットで実行する必要があるビットの転置の操作があります。

固定マップ(ハードウェア制約)によって発生します。Cを使用すると、作成に約10マイクロ秒かかりました。これをアセンブラーに変換したとき、このマップの特定の機能、特定のレジスターキャッシングを考慮し、ビット指向の操作を使用しました。実行に3.5マイクロ秒もかかりませんでした。


6

Walter BrightよるOptimizing Immutable and Purityは一見の価値があります。これはプロファイルテストではありませんが、手書きのASMとコンパイラー生成のASMの違いの1つの良い例を示しています。Walter Brightは最適化コンパイラーを作成しているため、彼の他のブログ投稿を見る価値があるかもしれません。



5

単純な答え... アセンブリをよく知っている人(別名は彼のそばに参照があり、すべての小さなプロセッサキャッシュやパイプライン機能などを利用しています)は、どのコンパイラよりはるかに高速なコードを生成できることが保証されています。

ただし、最近の違いは、一般的なアプリケーションでは問題になりません。


1
「多くの時間と労力を与えられた」、「メンテナンスの悪夢を作る」と言うのを忘れていました。私の同僚は、OSコードのパフォーマンスが重要なセクションの最適化に取り組んでおり、アセンブリではなくCで作業しました。これにより、妥当な時間内に高レベルの変更がパフォーマンスに与える影響を調査できました。
Artelius

同意する。時間を節約して迅速に開発するために、マクロとスクリプトを使用してアセンブリコードを生成する場合があります。最近のほとんどのアセンブラにはマクロがあります。そうでない場合は、(かなり単純なRegEx)Perlスクリプトを使用して(単純な)マクロプリプロセッサを作成できます。

この。正確に。ドメインの専門家を倒すコンパイラはまだ発明されていません。
cmaster-2017年

4

CP / M-86バージョンのPolyPascal(Turbo Pascalの兄弟)の可能性の1つは、「use-bios-to-output-characters-to-the-screen」機能を機械語ルーチンに置き換えることでした。 x、y、およびそこに配置する文字列が与えられました。

これにより、画面を以前よりもはるかに高速に更新できました。

バイナリにはマシンコード(数百バイト)を埋め込む余地があり、他にもさまざまなものがあったため、できるだけ絞る必要がありました。

画面が80x25だったので、両方の座標がそれぞれ1バイトに収まるため、どちらも2バイトのワードに収まることがわかりました。これにより、1回の加算で両方の値を同時に操作できるため、必要な計算を少ないバイト数で実行できました。

私の知る限り、レジスタ内の複数の値をマージし、それらにSIMD命令を実行して、後で再び分割できるCコンパイラはありません(とにかく、マシン命令が短くなるとは思いません)。


4

より有名なアセンブリスニペットの1つは、Michael Abrashのテクスチャマッピングループからのものです(ここで詳しく説明します)。

add edx,[DeltaVFrac] ; add in dVFrac
sbb ebp,ebp ; store carry
mov [edi],al ; write pixel n
mov al,[esi] ; fetch pixel n+1
add ecx,ebx ; add in dUFrac
adc esi,[4*ebp + UVStepVCarry]; add in steps

現在、ほとんどのコンパイラは、高度なCPU固有の命令を組み込み関数、つまり、実際の命令にコンパイルされる関数として表現しています。MS Visual C ++は、MMX、SSE、SSE2、SSE3、およびSSE4の組み込み関数をサポートしているため、プラットフォーム固有の命令を利用するためにアセンブリにドロップダウンすることについて心配する必要はほとんどありません。Visual C ++は、適切な/ ARCH設定を使用して、ターゲットにしている実際のアーキテクチャを利用することもできます。


さらに良いことに、これらのSSE組み込み関数はIntelによって指定されているため、実際にはかなり移植可能です。
James

4

適切なプログラマーが与えられれば、アセンブラープログラムは常に(少なくともわずかに)Cの対応物よりも高速にできます。アセンブラの命令を1つも取り出せないCプログラムを作成するのは難しいでしょう。


これはもう少し正しいでしょう:「...である重要な Cプログラムを作成するのは難しいでしょう」あるいは、「...で実際の Cプログラムを見つけるのは難しいでしょう」ポイントは、コンパイラが最適な出力を生成するための簡単なループがあります。それにもかかわらず、良い答えです。
cmaster-モニカを2017年


4

gccは広く使用されているコンパイラになりました。一般にその最適化はそれほど良くありません。アセンブラーを作成する平均的なプログラマーよりもはるかに優れていますが、実際のパフォーマンスについては、それほど良くありません。生成するコードが単純に信じられないコンパイラがあります。一般的な答えとして、コンパイラーの出力に進み、パフォーマンスのためにアセンブラーを調整したり、ルーチンを最初から書き直したりできる場所がたくさんあります。


8
GCCは非常にスマートな「プラットフォームに依存しない」最適化を行います。ただし、特定の命令セットを最大限に活用することはあまり得意ではありません。そのような移植可能なコンパイラにとって、それは非常に良い仕事をします。
Artelius 2009年

2
同意した。その移植性、入ってくる言語、出て行くターゲットは素晴らしいです。その移植性があることは、1つの言語またはターゲットで本当に上手になることを妨げる可能性があります。したがって、特定のターゲットを特定の最適化するために、人間がより良い機会を得る機会があります。
old_timer 2009年

+1:GCCは確かに高速なコードを生成するのに競争力がありませんが、移植性があるのでそれが確かではありません。LLVMは移植性があり、GCCの4倍の速さでコードを生成するのを見てきました。
Jon Harrop、2012年

私は、GCCが長年にわたって堅実であることに加え、最新のポータブルコンパイラを実行できるほぼすべてのプラットフォームで利用できるので、GCCを好みます。残念ながら、私はLLVM(Mac OS X / PPC)を構築できなかったので、おそらくそれに切り替えることができないでしょう。GCCの優れた点の1つは、GCCでビルドするコードを作成する場合、標準に近づいている可能性が高く、ほぼすべてのプラットフォームでビルドできることです。

4

ロングポーク、制限は1つだけです。時間です。コードへのすべての変更を最適化し、レジスタの割り当てに時間を費やし、流出を最適化するためのリソースがない場合、コンパイラは毎回勝ちます。コードに変更を加え、再コンパイルして測定します。必要に応じて繰り返します。

また、ハイレベル側では多くのことができます。また、結果のアセンブリを検査すると、コードがくだらないことがIMPRESSIONに示される可能性がありますが、実際には、思ったよりも速く実行されます。例:

int y = data [i]; //ここでいくつかのことを行います。call_function(y、...);

コンパイラーはデータを読み取り、それをスタックにプッシュ(スピル)し、後でスタックから読み取って引数として渡します。シテ?これは実際には非常に効果的な遅延補償であり、実行時間を短縮する可能性があります。

//最適化されたバージョンcall_function(data [i]、...); //結局のところ、それほど最適化されていません。

最適化されたバージョンのアイデアは、レジスターの圧力を軽減し、流出を回避することでした。しかし、実際には、「たわごと」バージョンの方が高速でした。

アセンブリコードを見て、命令を見て、結論を出すだけです。命令が多いほど、遅くなりますが、それは誤判断です。

ここで注意すべきことは、多くの組立専門家彼らは多くを知っていると思っていますが、ほとんど知っていません。ルールもアーキテクチャによって異なります。たとえば、常に最速である銀の弾丸のx86コードはありません。これらの日は経験則によって行く方が良いです:

  • メモリが遅い
  • キャッシュが速い
  • より良いキャッシュを使用してみてください
  • どのくらいの頻度で見逃しますか?遅延補償戦略はありますか?
  • 1つのシングルキャッシュミスに対して10〜100のALU / FPU / SSE命令を実行できます。
  • アプリケーションのアーキテクチャは重要です。
  • ..しかし、問題がアーキテクチャにない場合は役に立ちません

また、コンパイラにあまりにも多くを信頼して、あまり考えられていないC / C ++コードを「理論的に最適な」コードに魔法のように変換することは、希望的な考えです。この低レベルでの「パフォーマンス」を気にする場合は、使用するコンパイラとツールチェーンを知っている必要があります。

C / C ++のコンパイラーは、最初に関数に副作用があるため、一般に副次式の再配列にはあまり向いていません。関数型言語はこの警告に悩まされることはありませんが、現在のエコシステムにはそれほど適合しません。コンパイラ/リンカー/コードジェネレーターによって操作の順序を変更できる、緩和された精度ルールを許可するコンパイラオプションがあります。

このトピックは少し行き止まりです。ほとんどの場合それは関係ありません、そして残りは彼らはとにかく彼らがすでに何をしているかを知っています。

つまり、「何をしているのかを理解する」ということは、何をしているのかを知ることとは少し異なります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.