数年前、私は比較的上級の組み込みCプログラマーのポジションの候補者にインタビューしているパネルにいました。
私が尋ねた標準的な質問の1つは、最適化手法についてでした。候補者の中には答えがなかったので、とても驚きました。
では、後世のためのリストをまとめるために、Cプログラムを最適化するときに通常使用する手法と構成は何ですか?
速度とサイズの両方の最適化に対する回答が受け入れられました。
数年前、私は比較的上級の組み込みCプログラマーのポジションの候補者にインタビューしているパネルにいました。
私が尋ねた標準的な質問の1つは、最適化手法についてでした。候補者の中には答えがなかったので、とても驚きました。
では、後世のためのリストをまとめるために、Cプログラムを最適化するときに通常使用する手法と構成は何ですか?
速度とサイズの両方の最適化に対する回答が受け入れられました。
まず最初に-あまりにも早く最適化しないでください。コードのチャンクを注意深く最適化することに時間を費やして、それが予定されていると思っていたボトルネックではないことを見つけることは珍しくありません。または、別の言い方をすれば、「速くする前に、機能させる」ということです。
コードを最適化する前に、アルゴリズムを最適化するためのオプションがあるかどうかを調べてください。コードを最適化するよりも、貧弱なアルゴリズムを最適化することでパフォーマンスの向上を見つける方が簡単です。その後、アルゴリズムを変更したときにコードを破棄するだけです。
そして、そもそもなぜ最適化する必要があるのかを理解してください。何を達成しようとしていますか?たとえば、あるイベントへの応答時間を改善しようとしている場合、タイムクリティカルな領域を最小限に抑えるために実行の順序を変更する機会があるかどうかを確認します。たとえば、外部割り込みへの応答を改善しようとする場合、イベント間のデッドタイムに準備をすることができますか?
コードを最適化する必要があると判断したら、どのビットを最適化しますか?プロファイラーを使用します。最も頻繁に使用される領域に(最初に)注意を向けます。
では、それらの領域について何ができるでしょうか。
上記のオプションのほとんどは、悪影響を与えることなく通常の練習の一部として使用できます。ただし、実際に最高のパフォーマンスを発揮しようとしている場合は、次のようにします。-エラーチェックを(安全に)無効にできる場所を調査します。推奨されていませんが、スペースとサイクルを節約できます。-コードの一部をアセンブラーで手作りします。もちろん、これはコードの移植性がなくなったことを意味しますが、それが問題にならない場合は、ここで節約できる可能性があります。ただし、自由に使用できるレジスタへのデータの移動やレジスタからのデータの移動には時間がかかる可能性があることに注意してください(つまり、コンパイラのレジスタ使用量を満たすため)。また、コンパイラはそれ自体でかなり良い仕事をしているはずだということにも注意してください。(もちろん例外もあります)
他の誰もが言ったように:プロファイル、プロファイル プロファイル。
実際のテクニックについては、まだ言及されていないと思います:
ホット データとコールド データの分離: CPU のキャッシュ内にとどまることは非常に重要です。これを行う 1 つの方法は、データ構造を頻繁にアクセスされる (「ホット」) セクションとめったにアクセスされない (「コールド」) セクションに分割することです。
例: 次のような顧客の構造があるとします。
struct Customer
{
int ID;
int AccountNumber;
char Name[128];
char Address[256];
};
Customer customers[1000];
ここで、ID と AccountNumber には頻繁にアクセスしたいが、名前と住所にはあまりアクセスしたくないと仮定しましょう。あなたがすることは、それを2つに分割することです:
struct CustomerAccount
{
int ID;
int AccountNumber;
CustomerData *pData;
};
struct CustomerData
{
char Name[128];
char Address[256];
};
CustomerAccount customers[1000];
このように、「customers」配列をループしている場合、各エントリはわずか 12 バイトであるため、より多くのエントリをキャッシュに収めることができます。レンダリング エンジンの内部ループのような状況に適用できれば、これは大きなメリットとなります。
私のお気に入りのテクニックは、優れたプロファイラーを使用することです。ボトルネックがどこにあるかを示す良いプロファイルがなければ、トリックやテクニックは役に立ちません。
私が遭遇した最も一般的なテクニックは次のとおりです。
一般的な推奨事項に関しては、それらのほとんどはすでに鳴らされています:
低レベルの最適化の場合:
ヒープの使用は避けてください。同じサイズのオブジェクトには、obstacksまたはpool-allocatorを使用します。寿命の短い小さなものをスタックに置きます。allocaはまだ存在しています。
時期尚早の最適化はすべての悪の根源です!;)
私のアプリケーションは通常、設計上あまり CPU 時間を必要としないため、ディスク上とメモリ内のバイナリのサイズに注目しています。私が主に行っていることは、静的にサイズ設定された配列を探し、それらを動的に割り当てられたメモリに置き換えることです。後でメモリを解放するという追加の努力に値します。バイナリのサイズを削減するために、コンパイル時に初期化される大きな配列を探し、初期化を実行時に行います。
char buf[1024] = { 0, };
/* becomes: */
char buf[1024];
memset(buf, 0, sizeof(buf));
これにより、バイナリの .DATA セクションから 1024 のゼロバイトが削除され、代わりに実行時にスタック上にバッファーが作成され、ゼロで埋められます。
編集:そうそう、私は物事をキャッシュするのが好きです。これは C 固有のものではありませんが、キャッシュする内容によっては、パフォーマンスが大幅に向上する可能性があります。
PS: リストが完成したらお知らせください。非常に興味があります。;)
0 との比較は別のより高速なアセンブラ コマンドで実装されることが多いため、特にループでは、可能であれば、任意の数値ではなく 0 と比較してください。
たとえば、可能であれば、次のように記述します。
for (i=n; i!=0; --i) { ... }
それ以外の
for (i=0; i!=n; ++i) { ... }
まとめにくい…
データ構造:
アルゴリズム:
低レベル:
最も重要なこと: 早期に測定する、頻繁に測定し、決して仮定をしない、プロファイラーによって取得されたデータに基づいて思考と最適化を行う ( PTUを使用してください)。
別のヒントとして、パフォーマンスはアプリケーションの成功の鍵であり、設計時に考慮する必要があり、明確なパフォーマンス目標を設定する必要があります。
これは網羅的なものではありませんが、興味深いベースを提供するはずです。
言及されていない別のこと:
基本/一般:
実際に役立ったいくつかのこと:
サイズ/メモリの選択:
速度を選択します (注意してください):
最近では、最適化で最も重要なことは次のとおりです。
コードのコピー アンド ペースト (ループのアンローリングなど) を含む最適化や、手作業によるループの並べ替えを気にしないでください。通常、コンパイラはこれを実行するよりも優れた仕事をしますが、ほとんどのコンパイラはそれを元に戻すほど賢くありません。
コード実行のプロファイルを収集すると、そこまでの道のりの50%が得られます。他の50%は、これらのレポートの分析を扱っています。
さらに、GCCまたはVisualC ++を使用する場合は、「プロファイルガイド付き最適化」を使用できます。この場合、コンパイラーは以前の実行から情報を取得し、命令を再スケジュールしてCPUをより高速にします。
インライン関数!ここでプロファイリングファンに触発されて、私は私のアプリケーションをプロファイリングし、MP3フレームでビットシフトを行う小さな関数を見つけました。これは、私のアプリケーションのすべての関数呼び出しの約90%を実行するので、インラインで作成しました。プログラムは、以前のCPU時間の半分を使用するようになりました。
私が作業したほとんどの組み込みシステムにはプロファイリングツールがなかったので、プロファイラーを使用すると言うのは良いことですが、あまり実用的ではありません。
速度最適化の最初のルールは- クリティカルパスを見つけることです。
通常、このパスはそれほど長くなく、それほど複雑ではないことがわかります。これを最適化する方法を一般的な方法で言うのは難しいですが、それはあなたが何をしているか、そして何をする力があるかに依存します。たとえば、通常はクリティカルパスでmemcpyを避けたいので、DMAを使用したり最適化したりする必要がありますが、hwにDMAがない場合はどうでしょうか。memcpyの実装が、書き直さない場合に最適かどうかを確認してください。
組み込みでは動的割り当てをまったく使用しないでください。ただし、何らかの理由で使用する場合は、クリティカルパスで使用しないでください。
スレッドの優先順位を正しく整理します。正しいのは本当の問題であり、明らかにシステム固有です。
非常にシンプルなツールを使用して、タイムスタンプとインデックスを格納するボトルネックのシンプルなマクロを分析します。ケースの90%で実行される数は、あなたが時間を費やす場所を見つけることはほとんどありません。
そして最後は 、非常に重要なコードレビューです。ほとんどの場合、コードレビュー中のパフォーマンスの問題を非常に効果的な方法で回避します:)
さらに、パフォーマンスを測定する必要があります。
場合によっては、より多くのスペースと速度のどちらを求めているかを決定する必要があり、ほとんど逆の最適化につながります。たとえば、スペースを最大限に活用するには、#pragma pack(1) などの構造体をパックし、構造体でビット フィールドを使用します。速度を上げるには、プロセッサの設定に合わせてパックし、ビットフィールドを避けます。
もう 1 つのトリックは、realloc を介して配列を拡大するための適切なサイズ変更アルゴリズムを選択することです。または、特定のアプリケーションに基づいて独自のヒープ マネージャーを作成することをお勧めします。コンパイラに付属しているものがすべてのアプリケーションにとって最善の解決策であると思い込まないでください。
If someone doesn't have an answer to that question, it could be they don't know much.
It could also be that they know a lot. I know a lot (IMHO :-), and if I were asked that question, I would be asking you back: Why do you think that's important?
The problem is, any a-priori notions about performance, if they are not informed by a specific situation, are guesses by definition.
I think it is important to know coding techniques for performance, but I think it is even more important to know not to use them, until diagnosis reveals that there is a problem and what it is.
Now I'm going to contradict myself and say, if you do that, you learn how to recognize the design approaches that lead to trouble so you can avoid them, and to a novice, that sounds like premature optimization.
To give you a concrete example, this is a C application that was optimized.
素晴らしいリスト。上記のリストに記載されていないヒントを 1 つだけ追加します。場合によっては、最小限のコストで大幅な最適化を実現できます。
バイパスリンカー
main.c と lib.c などの 2 つのファイルに分割されたアプリケーションがある場合、多くの場合、\#include "lib.c"main.c に a を追加するだけでリンカを完全にバイパスし、コンパイラの最適化をより効率的に行うことができます。
ファイル間の依存関係を最適化しても同じ効果が得られますが、変更のコストは通常高くなります。
より効率的なアルゴリズムを使用して最適化することをお勧めします。後付けで行うのではなく、最初からそのようにコーディングすることをお勧めします。コンパイラは、ターゲット プロセッサについてユーザーよりも多くのことを知っているため、小さなことの詳細はコンパイラに任せます。
1 つには、ループを使用して検索することはめったになく、アイテムをハッシュテーブルに追加してから、ハッシュテーブルを使用して結果を検索します。
たとえば、検索する文字列があり、50 個の可能な値があるとします。したがって、50 個の strcmps を実行する代わりに、50 個の文字列すべてをハッシュテーブルに追加し、それぞれに一意の番号を付けます (これを行う必要があるのは 1 回だけです)。次に、ハッシュテーブルでターゲット文字列を検索し、50 個すべてのケース (または関数ポインターを持つ) を持つ 1 つの大きなスイッチを取得します。
一般的な入力セット (css rules など) を使用して検索する場合、高速コードを使用して可能な唯一のソリューションを追跡し、それらを反復して一致を見つけます。一致が得られたら、結果を (キャッシュとして) ハッシュテーブルに保存し、後で同じ入力セットを取得した場合にキャッシュ結果を使用します。
コードを高速化するための主なツールは次のとおりです。
hashtable - 迅速な検索と結果のキャッシュ用
qsort - 私が使用する唯一のソートです
bsp - エリアに基づいて物事を検索するため (地図のレンダリングなど)