When is assembly faster than C? [closed] Ask Question

When is assembly faster than C? [closed] Ask Question

One of the stated reasons for knowing assembler is that, on occasion, it can be employed to write code that will be more performant than writing that code in a higher-level language, C in particular. However, I've also heard it stated many times that although that's not entirely false, the cases where assembler can actually be used to generate more performant code are both extremely rare and require expert knowledge of and experience with assembly.

This question doesn't even get into the fact that assembler instructions will be machine-specific and non-portable, or any of the other aspects of assembler. There are plenty of good reasons for knowing assembly besides this one, of course, but this is meant to be a specific question soliciting examples and data, not an extended discourse on assembler versus higher-level languages.

Can anyone provide some specific examples of cases where assembly will be faster than well-written C code using a modern compiler, and can you support that claim with profiling evidence? I am pretty confident these cases exist, but I really want to know exactly how esoteric these cases are, since it seems to be a point of some contention.

ベストアンサー1

Here is a real world example: Fixed point multiplies on old compilers.

These don't only come handy on devices without floating point, they shine when it comes to precision as they give you 32 bits of precision with a predictable error (float only has 23 bit and it's harder to predict precision loss). i.e. uniform absolute precision over the entire range, instead of close-to-uniform relative precision (float).


Modern compilers optimize this fixed-point example nicely, so for more modern examples that still need compiler-specific code, see


C doesn't have a full-multiplication operator (2N-bit result from N-bit inputs). The usual way to express it in C is to cast the inputs to the wider type and hope the compiler recognizes that the upper bits of the inputs aren't interesting:

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

組み込み関数を使用すると、C コンパイラが何が起こっているかを理解できるように関数を書き直すことができます。これにより、コードのインライン化、レジスタの割り当て、共通部分式の削除、定数の伝播も実行できるようになります。この方法により、手書きのアセンブラ コードに比べてパフォーマンスが大幅に向上します。

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

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のソース+アセンブリを参照Godbolt コンパイラ エクスプローラ(残念ながら、単純な純粋な C バージョンから不正なコードを生成するほど古いコンパイラはありません。)


現代のCPUは、C言語にはまったくない演算子、例えばpopcnt最初または最後のセットビットを見つけるためのビットスキャンなどを実行できます。(POSIXには関数がありますが、そのセマンティクスはx86 /ffs()と一致しません。bsfbsrhttps://en.wikipedia.org/wiki/Find_first_set)。

一部のコンパイラは、整数内のセットされたビットの数をカウントするループを認識し、それをpopcnt命令にコンパイルすることがあります (コンパイル時に有効になっている場合)。ただし、SSE4.2 対応のハードウェアのみを対象としている場合は、GNU C または x86 で使用する方がはるかに信頼性が高くなります__builtin_popcnt_mm_popcnt_u32から<immintrin.h>

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

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


組み込み関数や手書きのアセンブリのもう1つの主要な領域は、SIMD命令による手動ベクトル化です。コンパイラは、のような単純なループではそれほど悪くありませんdst[i] += src[i] * 10.0;が、より複雑なものになると、うまく機能しなかったり、自動ベクトル化を行わなかったりします。たとえば、次のようなものはほとんど得られません。SIMD を使用して atoi を実装するにはどうすればよいですか?スカラー コードからコンパイラによって自動的に生成されます。

おすすめ記事