1. ホーム
  2. c

[解決済み] アセンブリがCより速いのはどんなとき?[クローズド]

2022-03-19 02:07:52

質問

アセンブラを知る理由の1つは、アセンブラを使うことで、上位言語、特にC言語で書くよりもパフォーマンスが高いコードを書ける場合があるからだと言われています。 しかし、それが完全に間違いというわけではないが、アセンブラを使うことでパフォーマンスが向上するケースもあるという話もよく聞く。 実際に より高性能なコードを生成するために使用されることは極めて稀であり、また、アセンブリに関する専門的な知識と経験が必要です。

この質問では、アセンブラの命令がマシン固有で移植不可能であることや、アセンブラの他の側面については触れません。 もちろん、これ以外にもアセンブラを知っている理由はたくさんありますが、これは例やデータを求める具体的な質問であって、アセンブラと高級言語について延々と論じるためのものではありません。

どなたか 具体例 また、その主張をプロファイリングで裏付けることができますか? 私はこのようなケースが存在することに自信を持っていますが、このようなケースがどの程度難解であるかを正確に知りたいのです。

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

ここで実例を紹介します。古いコンパイラでの固定小数点乗算。

これらは浮動小数点のないデバイスで便利なだけでなく、32ビットの精度と予測可能なエラー(floatは23ビットしかなく、精度の低下を予測するのは難しい)を与えるので、精度に関しても輝いています。 アブソリュート の代わりに、全範囲の精度を提供します。 相対 の精度( float ).


最近のコンパイラはこの固定小数点の例をうまく最適化するので、まだコンパイラ固有のコードが必要な最新の例については

  • 64ビット整数乗算の高次部分の取得 : を使用した移植版 uint64_t 32x32 => 64 ビット乗算の場合、64 ビット CPU では最適化に失敗するため、イントリンシックまたは __int128 を使用すると、64ビットシステム上で効率的なコードを作成できます。
  • Windows 32ビット版における _umul128 : MSVC は 32 ビット整数を 64 にキャストして乗算するとき、いつも良い仕事をしないので、 intrinsics はとても役に立ちました。

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、その他)は、1つの命令で乗算を行うことができます。コンパイラの中には、この事実を無視して、ランタイムライブラリ関数を呼び出して乗算を行うコードを生成するものがありました。16進シフトもしばしばライブラリルーチンで行われます(x86もこのようなシフトが可能です)。

つまり、乗算のためだけに1つか2つのライブラリーを呼び出すことになるのです。これは深刻な結果を招きます。シフトが遅くなるだけでなく、関数呼び出しの間、レジスタを保持しなければならず、インライン化やコードアンロールの助けにもならない。

同じコードを(インライン)アセンブラで書き直せば、大幅なスピードアップが可能です。

おまけ:ASMを使うのは問題解決に最適な方法ではありません。例えば、VS.NET2008コンパイラーは、32*32=64ビットのmulを__emulとして、64ビットのshiftを__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 でも、イントリンシックなしでこれを行うことができるでしょう)。

x86(32ビット)およびARM用のソースとasmは以下を参照してください。 ゴッドボルトコンパイラーエクスプローラー . (残念ながら、単純な純粋なCバージョンから悪いコードを生成するほど古いコンパイラはありません)。


最近のCPUは、C言語にはない演算子を使うことができる まったく のように popcnt またはビットスキャンで最初か最後のセットビットを見つける . (POSIXでは ffs() 関数がありますが、そのセマンティクスは x86 の bsf / bsr . 参照 https://en.wikipedia.org/wiki/Find_first_set ).

コンパイラによっては、整数のセットビットの数を数えるループを認識してコンパイルすることがある。 popcnt を使用する方がはるかに確実です。 __builtin_popcnt で、あるいは SSE4.2 のハードウェアだけをターゲットにしているならば x86 で。 _mm_popcnt_u32 から <immintrin.h> .

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

同様に ntohl にコンパイルすることができます。 bswap (エンディアン変換のためのx86 32ビットバイトスワップ)を持ついくつかのCの実装で。


イントリンシックや手書きのASMのもう一つの主要な分野は、SIMD命令による手動ベクトル化です。 のような単純なループであれば、コンパイラは悪くありません。 dst[i] += src[i] * 10.0; しかし、物事がより複雑になると、自動ベクトル化がうまくいかなかったり、まったく行われなかったりすることがよくあります。 たとえば、次のようなものはほとんどないでしょう。 SIMDを使ったatoiの実装方法は? スカラーコードからコンパイラが自動的に生成する。