アセンブラを知っている理由の1つは、場合によっては、高レベル言語(特にC)でコードを記述するよりもパフォーマンスの高いコードを記述するために使用できることです。ただし、それは完全に偽ではありませんが、アセンブラーが実際にを使用してより高性能なコードを生成できる場合は非常にまれであり、アセンブリ。
この質問は、アセンブラー命令がマシン固有で移植不可能であるという事実、またはアセンブラーの他の側面のいずれにも入らない。もちろん、このこと以外にもアセンブリを知っているのには十分な理由がありますが、これは例やデータを求める特定の質問であることを意図しています。
誰かが特定の例最新のコンパイラを使用して適切に記述されたCコードよりもアセンブリが速い場合のケースを提供できますか?また、その証拠をプロファイリング証拠でサポートできますか?私はこれらのケースが存在することをかなり確信していますが、これらのケースがどの程度難解であるかを正確に知りたいのです。
実際の例を次に示します。古いコンパイラでの固定小数点の乗算。
これらは浮動小数点を持たないデバイスで便利なだけでなく、予測可能なエラーで32ビットの精度を提供するため、精度に関しては輝いています(浮動小数点は23ビットのみであり、精度の損失を予測することは困難です)。つまり、均一に近い相対精度(float
name__)ではなく、範囲全体にわたる均一絶対精度。
最新のコンパイラはこの固定小数点の例を最適化するため、コンパイラ固有のコードが必要な最新の例については、
uint64_t
を使用するポータブルバージョンは、64ビットCPUでの最適化に失敗するため、効率的なコードを生成するには組み込み関数または__int128
が必要です64ビットシステム。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コンパイラが何が起こっているかを理解できるように関数を書き換えることができます。これにより、コードのインライン化、レジスタの割り当て、共通部分式の削除、定数の伝播を行うことができます。このように手書きのアセンブラコードよりもパフォーマンスが向上します[huge]。
参照用: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バージョンでも組み込み関数なしでこれを行うことができます。)
Source + asm for x86(32ビット)およびARM on The Godbolt compiler Explorer を参照してください。 (残念なことに、単純な純粋なCバージョンから不正なコードを生成するほど古いコンパイラはありません。)
最新のCPUは、Cにat allの演算子がありません。たとえば、popcnt
name__や、最初または最後の設定ビットを見つけるためのビットスキャンなどです。 (POSIXにはffs()
関数がありますが、そのセマンティクスはx86 bsf
name__/bsr
name__と一致しません。 https://en.wikipedia.org/wiki/Find_first_set を参照してください)。
一部のコンパイラは、整数のセットビット数をカウントするループを認識してpopcnt
name__命令にコンパイルすることがあります(コンパイル時に有効になっている場合)が、GNU Cで__builtin_popcnt
を使用する方がはるかに信頼できます。またはx86では、SSE4.2でハードウェアのみをターゲットにしている場合: _mm_popcnt_u32
FROM <immintrin.h>
。
または、C++では、std::bitset<32>
に割り当てて、.count()
を使用します。 (これは、言語が標準ライブラリを介してpopcountの最適化された実装を移植可能に公開する方法を発見した場合です。常に正しいものにコンパイルされ、ターゲットがサポートするものを利用できます。) https://en.wikipedia.org/wiki/Hamming_weight#Language_support 。
同様に、ntohl
name__は、一部のC実装でbswap
name__(エンディアン変換用のx86 32ビットバイトスワップ)にコンパイルできます。
組み込み関数または手書きasmのもう1つの主要な領域は、SIMD命令を使用した手動ベクトル化です。コンパイラはdst[i] += src[i] * 10.0;
のような単純なループで悪くはありませんが、多くの場合、うまくいかないか、事態が複雑になったときに自動ベクトル化を行いません。たとえば、スカラーコードからコンパイラによって自動的に生成される SIMDを使用してatoiを実装する方法 のようなものを取得することはほとんどありません。
何年も前に、私は誰かにCでプログラムするように教えていました。運動はグラフィックを90度回転させることでした。彼は、主に乗算や除算などを使用していたため、完了するまでに数分かかるソリューションを返しました。
私は彼にビットシフトを使用して問題を再現する方法を示しました。処理時間は、彼が持っていた非最適化コンパイラで約30秒に短縮されました。
最適化コンパイラを取得したばかりで、同じコードでグラフィックが5秒以内に回転しました。コンパイラーが生成しているアセンブリー・コードを見て、そこから私が見たものから、アセンブラーを書く日々が終わったと判断しました。
コンパイラが浮動小数点コードを見るときはいつでも、手書きバージョンのほうが速くなります。主な理由は、コンパイラが堅牢な最適化を実行できないことです。 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と交換しましたが、タイミングに違いはありませんでした。私は、メモリに制限された側面が他の最適化よりも小さいと思う。
うーん、私はコードのわずかに異なるバージョンを実行していましたが、間違った方法で数値を出力しました(つまり、Cが高速でした!)結果を修正および更新しました。
特定の例やプロファイラーの証拠を与えることなく、コンパイラー以上の知識がある場合は、コンパイラーよりも優れたアセンブラーを作成できます。
一般的な場合、最新のCコンパイラは、問題のコードを最適化する方法について多くのことを知っています。プロセッサパイプラインの動作方法を知っている、人間よりも速く命令を並べ替えることができるなど、基本的には同じですボードゲームなどで最高の人間のプレーヤーと同等かそれ以上の性能を備えたコンピューター。これは、問題の領域内の検索をほとんどの人間よりも速くできるからです。理論的には特定のケースでコンピューターと同じように実行できますが、確かに同じ速度で実行することはできないため、いくつかのケースで実行不可能になります(つまり、アセンブラーでのいくつかのルーチン)。
一方で、コンパイラがそれほど多くの情報を持っていない場合があります-私は主に、コンパイラが知識を持っていない外部ハードウェアのさまざまな形式で作業しているときに言います。主な例は、おそらくデバイスドライバーです。アセンブラーは、問題のハードウェアに関する人間の親密な知識と組み合わせると、Cコンパイラーが実行できるよりも良い結果が得られます。
上記の段落で私が話している特別な目的の命令について言及している人もいます-コンパイラが知識を制限している、またはまったく知識を持たない可能性があり、人間がより高速なコードを書くことを可能にします。
私の仕事では、アセンブリを知って使用する3つの理由があります。重要度の高い順に:
デバッグ-バグや不完全なドキュメントを含むライブラリコードを頻繁に取得します。アセンブリレベルでステップインすることで、それが何をしているのかを把握します。私はこれを週に1回程度しなければなりません。また、C/C++/C#の慣用的なエラーを目で確認できない問題をデバッグするためのツールとしても使用します。アセンブリを見るとそれを超えます。
最適化-コンパイラーは最適化でかなりうまく機能しますが、私はほとんどとは異なる球場でプレーします。通常、次のようなコードで始まる画像処理コードを作成します。
for (int y=0; y < imageHeight; y++) {
for (int x=0; x < imageWidth; x++) {
// do something
}
}
「何かをする」部分は、通常、数百万回(つまり、3〜30回)発生します。その「何かをする」段階でサイクルを削ることにより、パフォーマンスの向上は非常に大きくなります。私は通常そこから始めません-私は通常、最初に動作するコードを書くことから始め、それからCを自然に良くなるようにリファクタリングするために最善を尽くします(より良いアルゴリズム、ループの負荷の軽減など)。私は通常、何が起こっているかを見るためにアセンブリを読む必要があり、めったにそれを書く必要はありません。多分2、3ヶ月ごとにこれをします。
言語が私にさせない何かをする。これらには、プロセッサアーキテクチャと特定のプロセッサ機能の取得、CPUにないフラグへのアクセス(C、キャリーフラグへのアクセスをCに許可してほしい)などが含まれます。
特別な目的の命令セットを使用する場合のみ、コンパイラはサポートしません。
複数のパイプラインと予測分岐を備えた最新のCPUの計算能力を最大化するには、a)人間が書くことがほぼ不可能b)維持がさらに困難になるようにアセンブリプログラムを構築する必要があります。
また、より優れたアルゴリズム、データ構造、メモリ管理により、アセンブリで実行できるマイクロ最適化よりも少なくとも1桁高いパフォーマンスが得られます。
Cは8ビット、16ビット、32ビット、64ビットのデータの低レベル操作に「近い」が、Cでサポートされていない数学演算がいくつかあり、特定のアセンブリ命令でエレガントに実行できることが多いセット:
固定小数点乗算:2つの16ビット数の積は32ビット数です。しかし、Cの規則では、2つの16ビット数の積は16ビット数であり、2つの32ビット数の積は32ビット数であり、どちらも下半分であるとされています。 16x16乗算または32x32乗算の半分topが必要な場合は、コンパイラーでゲームをプレイする必要があります。一般的な方法は、必要以上のビット幅にキャストし、乗算し、シフトダウンし、キャストバックすることです。
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乗算の上位半分を取得し、マシンのネイティブの16x16multiplyで正しいことを実行しようとしていることを認識するのに十分賢いかもしれません。または、製品が16ビットしか必要ないため、32x32乗算を実行するためにライブラリ呼び出しが必要な場合もありますが、C標準では自分を表現する方法が提供されていません。
特定のビットシフト操作(回転/キャリー):
// 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)があります:Nビットのチャンク(8、16、32、64、128など)を取り、全体をシフトします1だけ正しいこと(上記のアルゴリズムを参照)、結果のキャリーが1の場合、多項式を表すビットパターンでXORになります。
そうは言っても、パフォーマンスに大きな制約がない限り、これらの手法に頼ることはありません。他の人が言ったように、アセンブリはCコードよりも文書化/デバッグ/テスト/保守がはるかに困難です。パフォーマンスの向上には、いくつかの重大なコストが伴います。
edit:3.アセンブリではオーバーフロー検出が可能です(実際にはCではできません)。これにより、いくつかのアルゴリズムがはるかに簡単になります。
短い答え?時々。
技術的には、すべての抽象化にはコストがかかり、プログラミング言語はCPUの動作方法の抽象化です。 Cは非常に近いです。数年前、UNIXアカウントにログオンして次のような幸運のメッセージを受け取ったとき(そのようなことが人気だったとき)に大声で笑ったことを覚えています。
Cプログラミング言語-アセンブリ言語の柔軟性とアセンブリ言語のパワーを組み合わせた言語。
Cは移植可能なアセンブリ言語のようなものだからです。
アセンブリ言語は、どのように記述しても実行されることに注意してください。ただし、Cとそれが生成するアセンブリ言語の間にコンパイラがあり、それは非常に重要ですCコードの速さは、コンパイラの性能と非常に関係があります
Gccが登場したとき、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が支配する傾向がありますが)など)など。または単にキックのために(これはやや自虐的です)。
答えではないポイントを1つ。
プログラムを作成したことがない場合でも、少なくとも1つのアセンブラー命令セットを知っていると便利です。これは、より多くのことを知り、より良くなるための終わりのない探求者のプログラマの一部です。また、ソースコードを持っていないフレームワークに足を踏み入れて、少なくとも何が起こっているのかを大まかに把握している場合にも役立ちます。また、JavaByteCodeと.Net ILはどちらもアセンブラーに似ているため、理解するのに役立ちます。
少量のコードまたは長い時間があるときに質問に答えるため。組み込みチップでの使用に最も役立ちます。組み込みチップでは、チップの複雑さが低く、これらのチップを対象とするコンパイラーの競争が悪く、人間に有利にバランスをとることができます。また、制限されたデバイスの場合、多くの場合、コンパイラに指示するのが難しい方法でコードサイズ/メモリサイズ/パフォーマンスをトレードオフしています。例えばこのユーザーアクションは頻繁に呼び出されないため、コードサイズが小さく、パフォーマンスが低下しますが、このように見える他の関数は毎秒使用されるため、コードサイズが大きくなり、パフォーマンスが向上します。これは、熟練したアセンブリプログラマが使用できる一種のトレードオフです。
また、Cコンパイルでコードを作成し、生成されたアセンブリを調べてから、Cコードを変更するか、アセンブリとして調整して維持できる、多くの中間点があることを付け加えます。
私の友人は、現在小さな電気モーターを制御するためのチップであるマイクロコントローラーに取り組んでいます。彼は低レベルcとアセンブリの組み合わせで働いています。彼はかつて仕事での良い一日を教えてくれました。メインループを48命令から43命令に減らしました。彼はまた、コードが256kチップを満たすように成長し、ビジネスが新しい機能を必要としているという選択肢に直面しています
アセンブリの作成に飛び込む必要性を一度も感じたことのない、ポートフォリオや言語、プラットフォーム、アプリケーションの種類が豊富な商用開発者として追加したいと思います。私はそれについて得た知識を常に高く評価しています。そして時々デバッグされました。
「なぜアセンブラーを学ぶべきか」という質問にはるかに答えていることは知っていますが、それはいつ速いかよりも重要な質問だと思います。
もう一度試してみましょうアセンブリについて考える必要があります
アセンブリを生成されたコンパイラと比較して、どちらが高速/小型/高性能であるかを確認してください。
デビッド。
あなたのオタクの喜びのためにもう適用されないかもしれないユースケース:Amigaでは、CPUとグラフィックス/オーディオチップはRAM(RAM具体的にする)。そのため、RAM(またはそれ以下)が2MBしかない場合、複雑なグラフィックスを表示し、サウンドを再生するとCPUのパフォーマンスが低下します。
アセンブラーでは、グラフィック/オーディオチップが内部でビジーであるとき(つまり、バスが空いているとき)にのみCPUがRAMにアクセスしようとするような巧妙な方法でコードをインターリーブできます。したがって、命令を並べ替えること、CPUキャッシュの巧妙な使用、バスタイミング、すべてのコマンドの時間を計る必要があったり、さまざまなNOPをあちこちに挿入したりする必要があったため、高級言語では不可能だった効果を達成できます互いのレーダーからチップ。
これは、CPUのNOP(操作なし-何もしない)命令が実際にアプリケーション全体の実行を高速化できるもう1つの理由です。
[編集]もちろん、このテクニックは特定のハードウェア設定に依存します。多くのAmigaゲームがより高速なCPUに対応できなかった主な理由は次のとおりです。命令のタイミングがずれていました。
誰もこれを言っていないことに驚いています。 strlen()
関数は、Assemblyで記述されている場合、はるかに高速です! 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のWordを使用すると、前のCルーチンが8倍高速になります!
何年も前だったので具体的な例を挙げることはできませんが、手書きのアセンブラがどのコンパイラよりも優れている場合がたくさんありました。理由:
呼び出し規約から逸脱して、レジスタに引数を渡すことができます。
レジスタの使用方法を慎重に検討し、変数をメモリに保存しないようにすることができます。
ジャンプテーブルのようなものについては、インデックスを境界チェックする必要を避けることができます。
基本的に、コンパイラーは最適化のかなり良い仕事をします、そしてそれはほとんど常に「十分」ですが、いくつかの状況(グラフィックレンダリングのような)であなたがすべての単一のサイクルに心から支払う場合、あなたはコードを知っているのでショートカットを取ることができます、コンパイラは安全な側にある必要があるためできませんでした。
実際、グラフィックレンダリングコードでは、継続的な意思決定を回避するために、線描画やポリゴン塗りつぶしルーチンなどのルーチンがスタック上でマシンコードの小さなブロックを実際に生成し、そこで実行するコードを聞いたことがあります。線のスタイル、幅、パターンなどについて.
とは言っても、コンパイラーにしたいことは、私にとっては良いアセンブリー・コードを生成することですが、あまり賢くないことではありません。実際、Fortranについて私が嫌いなことの1つは、Fortranがコードを「最適化」するためにスクランブルすることです。
通常、アプリにパフォーマンスの問題がある場合、それは無駄な設計によるものです。最近では、アプリ全体が既にその寿命の1インチ以内に調整されていて、まだ十分に速くなく、タイトな内部ループですべての時間を費やしていない限り、パフォーマンスのためにアセンブラーをお勧めしません。
追加:アセンブリ言語で書かれたアプリをたくさん見てきましたが、C、Pascal、Fortranなどの言語と比較した場合の主な速度の利点は、アセンブラーでコーディングする際にプログラマーがはるかに慎重だったためです。彼または彼女は、言語に関係なく、1日に約100行のコードを記述し、3または400命令に相当するコンパイラ言語で記述します。
SIMD命令を使用したマトリックス演算は、おそらくコンパイラー生成コードよりも高速です。
私の経験からのいくつかの例:
Cからアクセスできない命令へのアクセス。たとえば、多くのアーキテクチャ(x86-64、IA-64、DEC Alpha、64ビットMIPSまたはPowerPCなど)は、64ビットx 64ビットの乗算をサポートし、128ビットの結果を生成します。 GCCは最近、そのような指示へのアクセスを提供する拡張機能を追加しましたが、その前にアセンブリが必要でした。また、この命令にアクセスすると、RSAのようなものを実装するときに64ビットCPUで大きな違いが生じる可能性があります。パフォーマンスが4倍向上することもあります。
CPU固有のフラグへのアクセス。私によく噛まれたのはキャリーフラグです。複数精度の加算を行う場合、CPUキャリービットにアクセスできない場合は、代わりに結果を比較してオーバーフローしたかどうかを確認する必要があります。さらに悪いことに、これはデータアクセスの点で非常に連続的であり、最新のスーパースカラープロセッサのパフォーマンスを低下させます。このような数千の整数を連続して処理する場合、addcを使用できることは大きなメリットです(キャリービットの競合にもスーパースカラーの問題がありますが、最新のCPUはそれをうまく処理します)。
SIMD。自動ベクトル化コンパイラーでも比較的簡単なケースしか実行できないため、残念ながら、SIMDのパフォーマンスを向上させたい場合は、残念ながらコードを直接記述する必要があります。もちろん、アセンブリの代わりに組み込み関数を使用することもできますが、組み込み関数のレベルに到達したら、コンパイラをレジスタアロケータおよび(名目上)命令スケジューラとして使用するだけで、基本的にアセンブリを記述します。 (私はコンパイラが関数プロローグやその他のものを生成できるため、SIMDの組み込み関数を使用する傾向があるため、関数呼び出し規約などのABIの問題に対処することなくLinux、OS X、およびWindowsで同じコードを使用できますが、他のそれよりも、SSE組み込み関数は実際にはあまり良いものではありません-Altivecのものはあまり経験がありませんが、より良いようです)。 (現在の)ベクトル化コンパイラーが理解できないものの例として、 bitslicing AES または SIMDエラー修正 について読んでください-アルゴリズムを分析し、そのようなコードを生成しますが、このようなスマートコンパイラは、既存の(せいぜい)から少なくとも30年離れているように感じます。
一方、マルチコアマシンと分散システムは、パフォーマンスの最大のメリットの多くを他の方向にシフトしました。アセンブリで内部ループを記述すると、さらに20%、または複数のコアで実行することで300%、または10000%マシンのクラスター全体でそれらを実行します。そしてもちろん、MLやScalaなどの高レベル言語では、Cやasmよりも高レベルの最適化(先物、メモ化など)がはるかに簡単であり、多くの場合、パフォーマンスが大幅に向上します。 。したがって、いつものように、トレードオフが必要です。
画像は何百万ものピクセルで構成されている場合があるため、画像で遊ぶときのようなタイトなループ。限られた数のプロセッサレジスタを最大限に活用する方法を考え出し、理解することで違いが生まれます。これが実際のサンプルです:
http://danbystrom.se/2008/12/22/optimizing-away-ii/
それから、多くの場合、プロセッサーには、コンパイラーが悩むにはあまりにも特殊な難解な命令がありますが、アセンブラー・プログラマーがそれらをうまく利用できる場合があります。 XLAT命令を例にとります。ループandでテーブル検索を行う必要がある場合、本当に素晴らしいです。テーブルは256バイトに制限されています!
更新:ああ、一般的にループについて話すとき、最も重要なことを考えてみてください:コンパイラは、多くの場合、一般的なケースになる反復回数の手がかりを持っていません!プログラマーだけが、ループが何度も繰り返されることを知っているため、追加の作業を行ってループの準備をすることが有益であること、または実際にループが何度も繰り返されてセットアップが実際に反復よりも長くかかる場合期待した。
あなたが思うよりも頻繁に、C標準がそう言っているからといって、Cはアセンブリコーダーの観点からは不必要と思われることをする必要があります。
たとえば、整数プロモーション。 Cでchar変数をシフトしたい場合、通常はコードが実際にそれを行う、単一ビットシフトを行うと予想されます。
ただし、標準では、シフトの前にintに符号拡張を行い、その後結果をcharに切り捨てて、ターゲットプロセッサのアーキテクチャに応じてコードを複雑にする可能性があるコンパイラを強制します。
コンパイラが生成するものの逆アセンブリを見ていなければ、よく書かれたCコードが本当に速いかどうかは実際にはわかりません。多くの場合、あなたはそれを見て、「よく書かれた」は主観的であることがわかります。
そのため、これまでで最速のコードを取得するためにアセンブラーで記述する必要はありませんが、まったく同じ理由でアセンブラーを知ることは確かに価値があります。
アセンブラが高速である一般的なケースは、スマートアセンブリプログラマがコンパイラの出力を見て、「これがパフォーマンスのクリティカルパスであり、これをより効率的に書くことができる」と言ってから、そのアセンブラを微調整するか書き換えるときだと思いますゼロから。
すべてはワークロードに依存します。
日常の操作では、CとC++は問題ありませんが、アセンブリを実行する必要があるかなりの作業負荷(ビデオ(圧縮、解凍、画像効果など)を含む変換)があります。
また、通常、これらの種類の操作に合わせて調整されたCPU固有のチップセット拡張(MME/MMX/SSE/whatever)の使用も伴います。
割り込みごとに192または256ビットで、50マイクロ秒ごとに行われる必要があるビットの転置の操作があります。
固定マップ(ハードウェア制約)によって発生します。 Cを使用すると、作成に約10マイクロ秒かかりました。このマップの特定の機能、特定のレジスタキャッシュ、およびビット指向操作の使用を考慮して、これをアセンブラーに変換しました。実行にかかった時間は3.5マイクロ秒未満でした。
すべての回答(30以上)を読みましたが、簡単な理由は見つかりませんでした: Intel®64およびIA-32アーキテクチャ最適化リファレンスマニュアル を読んで練習した場合、アセンブラはCより高速です、 アセンブリが遅くなる理由は、そのような遅いアセンブリを書く人が最適化マニュアルを読まなかったからです。。
Intel 80286の古き良き時代では、各命令はCPUサイクルの固定カウントで実行されていましたが、1995年にリリースされたPentium Pro以降、Intelプロセッサは複雑なパイプライン処理:アウトオブオーダー実行とレジスタ名変更を利用してスーパースカラーになりました。それ以前には、1993年に製造されたPentiumにはUおよびVパイプラインがありました。互いに依存していなければ1クロックサイクルで2つの単純な命令を実行できるデュアルパイプライン。しかし、これはPentium Proに登場した「アウトオブオーダー実行とレジスタ名の変更」と比較するものではなく、現在ほとんど変更されていません。
簡単に説明すると、最速のコードは、命令が以前の結果に依存しない場所です。 (movzxで)レジスタ全体を常にクリアするか、代わりにadd rax, 1
を使用するか、inc rax
を使用して、フラグなどの以前の状態への依存関係を削除する必要があります。
時間が許せば、アウトオブオーダー実行とレジスタの名前の変更に関する詳細を読むことができます。インターネットには多くの情報があります。
分岐予測、ロードユニットとストアユニットの数、マイクロオペレーションを実行するゲートの数など、その他の重要な問題もありますが、考慮すべき最も重要なことは、アウトオブオーダー実行です。
ほとんどの人はアウトオブオーダー実行について単純に気づいていないため、80286のようにアセンブリプログラムを記述します。コンテキストに関係なく、命令の実行に一定の時間がかかることを期待しています。 Cコンパイラーはアウトオブオーダー実行を認識し、コードを正しく生成します。そのため、このような気づかない人々のコードは遅くなりますが、気付くとコードは速くなります。
Walter BrightによるImmutableとPurityの最適化 これはプロファイルテストではありませんが、手書きASMとコンパイラ生成ASMの違いの良い例を示しています。 Walter Brightは最適化コンパイラーを書いているので、彼の他のブログ記事を見る価値があるかもしれません。
LInux Assembly howto 、この質問をし、Assemblyを使用することの長所と短所を示します。
簡単な答え... 知っているアセンブリまあ(別名彼のそばに参照があり、あらゆる小さなプロセッサキャッシュやパイプライン機能などを利用している)が保証されていますanyコンパイラよりもはるかに高速なコードを生成できます。
しかし、最近の違いは、典型的なアプリケーションでは重要ではありません。
gccは広く使用されているコンパイラになりました。一般的な最適化はそれほど良くありません。アセンブラーを記述する平均的なプログラマーよりもはるかに優れていますが、実際のパフォーマンスについてはそれほど良くありません。生成するコードには単純に信じられないほどのコンパイラがあります。したがって、一般的な答えとして、コンパイラの出力にアクセスし、パフォーマンスのためにアセンブラを微調整したり、ルーチンをゼロから書き直したりできる場所がたくさんあります。
Longpoke、唯一の制限があります:時間。コードへのすべての変更を最適化し、レジスタの割り当てに時間を費やし、わずかなスピルアウェイを最適化するリソースがない場合は、コンパイラが常に勝ちます。コードを修正し、再コンパイルして測定します。必要に応じて繰り返します。
また、高レベルの面でも多くのことができます。また、結果のアセンブリを検査することで、コードががらくたであることを印象付けることができますが、実際には、あなたが思っているよりも速く実行されます。例:
int y = data [i]; //ここで何かをします。call_function(y、...);
コンパイラーはデータを読み取り、それをスタックにプッシュ(スピル)し、後でスタックから読み取り、引数として渡します。音がシテ?それは実際には非常に効果的なレイテンシー補正であり、実行時間の短縮につながります。
//最適化されたバージョンcall_function(data [i]、...); //結局それほど最適化されていません..
最適化されたバージョンのアイデアは、レジスターのプレッシャーを減らし、流出を防ぐことでした。しかし、実際には、「シッティ」バージョンの方が高速でした!
アセンブリコードを見て、指示を見て、結論を出すだけです。指示が増えれば遅くなりますが、それは誤解です。
ここで注意すべきことは、多くのアセンブリの専門家思考彼らは多くを知っていますが、ほとんど知っていません。ルールもアーキテクチャごとに変わります。たとえば、常に最速である銀の弾丸のx86コードはありません。最近では、経験則に従うことをお勧めします。
また、コンパイラーに頼りすぎて、考え抜かれていないC/C++コードを「理論的に最適な」コードに魔法のように変換することは希望的観測です。この低レベルで「パフォーマンス」を重視する場合は、使用するコンパイラとツールチェーンを知っている必要があります。
C/C++のコンパイラーは、一般に、副次式の順序を並べ替えるのにあまり適していません。これは、関数が最初に副作用を引き起こすためです。関数型言語はこの警告に悩まされませんが、現在のエコシステムにうまく適合しません。コンパイラ/リンカー/コードジェネレーターによって操作の順序を変更できるようにする、精度を緩和したルールを許可するコンパイラーオプションがあります。
このトピックは少し行き詰まっています。ほとんどの場合、それは関連性がなく、残りは、とにかくすでに何をしているかを知っています。
要するに、「あなたが何をしているかを理解する」ということは、あなたが何をしているのかを知ることとは少し異なります。
実行時にマシンコードを作成しますか?
私の兄弟(2000年頃)は、実行時にコードを生成することで、非常に高速なリアルタイムレイトレーサーを実現しました。詳細は思い出せませんが、オブジェクトをループするメインモジュールがあり、各オブジェクトに固有のマシンコードを準備して実行していました。
しかし、時間が経つにつれて、この方法は新しいグラフィックスハードウェアによって排除され、役に立たなくなりました。
現在、ピボットテーブル、ドリル、オンザフライの計算などのビッグデータ(数百万のレコード)の一部の操作は、この方法で最適化できると考えています。問題は:努力する価値はありますか?
Assemblyの最も有名なスニペットの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設定でターゲットとする実際のアーキテクチャを利用することもできます。
適切なプログラマーがいる場合、アセンブラープログラムは、Cの対応するプログラムよりも常に(少なくともわずかに)速くすることができます。アセンブラーの命令を少なくとも1つ取り出すことができなかったCプログラムを作成することは困難です。
PolyPascalのCP/M-86バージョン(Turbo Pascalの兄弟)の可能性の1つは、「biosから出力文字への出力」機能を、本質的には機械語ルーチンに置き換えることでした。 x、y、およびそこに配置する文字列が与えられました。
これにより、以前よりもはるかに高速に画面を更新することができました!
バイナリにはマシンコード(数百バイト)を埋め込む余地があり、他にも何かがあったので、可能な限り圧縮することが不可欠でした。
画面は80x25であったため、両方の座標がそれぞれ1バイトに収まるため、両方が2バイトのWordに収まることがわかりました。これにより、1回の加算で両方の値を同時に操作できるため、必要な計算をより少ないバイトで実行できました。
私の知る限り、レジスタ内の複数の値をマージし、それらに対してSIMD命令を実行し、後でそれらを再び分割できるCコンパイラはありません(とにかく、マシン命令が短くなるとは思いません)。
http://cr.yp.to/qhasm.html には多くの例があります。
質問は少し誤解を招く恐れがあります。答えは投稿自体にあります。特定の問題に対して、コンパイラによって生成されるものよりも高速に実行されるアセンブリソリューションを作成することは常に可能です。コンパイラの制限を克服するには、アセンブリの専門家である必要があります。経験豊富なアセンブリプログラマは、経験の浅い人によって書かれたものよりも高速に実行されるプログラムをHLLで書くことができます。真実は、コンパイラーによって生成されたものよりも速く実行するアセンブリー・プログラムをいつでも書くことができるということです。
質問は非常に非特異的であるため、これに具体的に答えることは非常に困難です。正確に「最新のコンパイラ」とは何ですか?
理論的には、ほとんどすべての手動アセンブラー最適化はコンパイラーでも実行できます。実際にis完了かどうかは、特定のコンパイラーの特定のバージョンについてのみとは言えません。多くの場合、特定のコンテキストで副作用なしで適用できるかどうかを判断するのに多大な労力が必要になるため、コンパイラの作成者は気にしません。
プロセッサ速度がMHzで測定され、画面サイズが1メガピクセル未満であった日では、表示を高速化するためのよく知られたトリックは、ループを展開することでした:画面の各スキャンラインの書き込み操作。ループインデックスを維持するオーバーヘッドを回避しました!画面の更新の検出と相まって、非常に効果的でした。
これはCコンパイラではできないことです...(速度またはサイズの最適化を選択できることがよくありますが、前者は同様のトリックを使用していると思います。)
アセンブリ言語でWindowsアプリケーションを書くのを楽しんでいる人がいることは知っています。彼らは、彼らがより速く(証明するのが難しい)、より小さい(実際に!)と主張しています。
明らかに、それは楽しいことですが、おそらくGUI操作の場合、おそらく時間を浪費します(もちろん、学習目的を除きます!)。 、慎重に記述されたアセンブリコードによって最適化できます。
実際、大規模なプログラムモードで大規模なプログラムを構築できます。セガメントは64kbコードに制限される場合がありますが、多くのセガメントを書くことができます。ASMは古い言語であるため、人々はASMに対して議論を行います。それが、PCにメモリを詰め込む理由です。ASMで見つけられる唯一の欠点は、プロセッサベースであるため、Intelアーキテクチャ用に作成されたほとんどのプログラムは、AMDアーキテクチャでは実行されない可能性が高いことです。 CがASMよりも高速であるため、ASMよりも高速な言語はなく、ASMはプロセッサレベルで実行できない多くのCおよび他のHLLを実行できます。 ASMは習得が難しい言語ですが、一度習得すると、HLLがあなたよりも上手に翻訳することはできません。 HLLの「Do to you」コードの一部だけを見て、それが何をしているのかを理解できた場合、ASMを使用しない人が増え、アセンブリが更新されなくなった理由を疑問に思うでしょう(とにかく一般公開されています)。したがって、CはASMより高速ではありません。経験のあるC++プログラマーでさえ、ASMのコードチャンクを使用して記述し、そこにC++コードを追加して速度を向上させています。他の言語また、一部の人々は時代遅れであるか、おそらくは役に立たないと考える神話は時々神話です。たとえば、PhotoshopはPascal/ASMで書かれ、ソースの最初のリリースは技術史博物館に提出され、paintshop proはPythonで書かれていますが、 TCLとASM ...「高速で優れた画像プロセッサはASMです。Photoshopはデルファイにアップグレードされたかもしれませんが、今でもPascalです。速度の問題はPascalから来ていますが、これは私たちのやり方が好きだからです。プログラムは見た目であり、現在の動作ではありません。私が取り組んでいる純粋なASMでPhotoshop Cloneを作成し、かなりうまくいきたいと思います。コード、解釈、範囲、書き換えなどではありません...ただのコードプロセスを完了します。
あなたが与えられた命令セットに対してコンパイラよりも優れているとき、私は言うだろう。だから私は一般的な答えはないと思う
最近では、Cコードを非常に最適化するインテルC++などのコンパイラーを考えると、コンパイラーの出力と競合することは非常に困難です。