優れた設計でスタックスペースを確保するにはどうすればよいでしょうか?
質問
RTOS を備えた RAM が制限された組み込みマイクロコントローラー用に C でプログラミングしています。
私は定期的にコードを短い関数に分割しますが、関数を呼び出すたびに多くのスタック メモリが必要になります。すべてのタスクにはスタックが必要であり、これはプロジェクト内で重要なメモリを消費するものの 1 つです。
コードを整理して読みやすくしつつ、メモリを保持する代替手段はあるでしょうか?
解決
呼び出しスタックをよりフラットにするようにしてください。 a()
電話をかける b()
どちらが呼びますか c()
どちらが呼びますか d()
, 、 持っている a()
電話 b()
, c()
, 、 そして d()
自体。
関数が 1 回だけ参照される場合は、その関数をマークします inline
(コンパイラがこれをサポートしていると仮定します)。
他のヒント
スタックの使用には 3 つのコンポーネントがあります。
- 関数呼び出しの戻りアドレス
- 関数呼び出しパラメータ
- 自動(ローカル)変数
スタックの使用量を最小限に抑える鍵は、パラメーターの受け渡しと自動変数を最小限に抑えることです。実際の関数呼び出し自体のスペース消費はかなり最小限です。
パラメーター
パラメーターの問題に対処する 1 つの方法は、多数のパラメーターの代わりに (ポインターを介して) 構造体を渡すことです。
foo(int a, int b, int c, int d)
{
...
bar(int a, int b);
}
代わりにこれを実行してください:
struct my_params {
int a;
int b;
int c;
int d;
};
foo(struct my_params* p)
{
...
bar(p);
};
この戦略は、多くのパラメーターを渡す場合に適しています。パラメータがすべて異なる場合は、うまく機能しない可能性があります。最終的には、さまざまなパラメーターを含む大規模な構造が渡されることになります。
自動変数 (ローカル変数)
これはスタック領域を最も多く消費する傾向があります。
- 配列がキラーです。ローカル関数で配列を定義しないでください。
- ローカル変数の数を最小限に抑えます。
- 必要最小限のタイプを使用してください。
- 再入性が問題にならない場合は、モジュールの静的変数を使用できます。
すべてのローカル変数をローカル スコープからモジュール スコープに移動しただけでは、スペースは節約されていないことに注意してください。スタック領域をデータセグメント領域と交換しました。
一部の RTOS は、スレッドごとに「グローバル」ストレージを割り当てるスレッド ローカル ストレージをサポートしています。これにより、タスクごとに複数の独立したグローバル変数を使用できるようになりますが、コードがそれほど単純ではなくなります。
多くのメイン メモリを確保できるが、スタックがほんのわずかしかない場合は、静的割り当てを評価することをお勧めします。
C では、関数内で宣言されたすべての変数は「自動的に管理」され、スタック上に割り当てられます。
宣言を「静的」として修飾すると、宣言はスタックではなくメイン メモリに保存されます。これらは基本的にグローバル変数のように動作しますが、グローバル変数の使いすぎに伴う悪い習慣を避けることができます。スタックへの負担を軽減するために、大きくて寿命の長いバッファ/変数を静的として宣言することは良い例となります。
アプリケーションがマルチスレッドである場合、または再帰を使用している場合、これはうまく機能しない、またはまったく機能しないことに注意してください。
最適化、特に積極的なインライン化をオンにします。コンパイラはメソッドをインライン化して呼び出しを最小限に抑えることができる必要があります。使用するコンパイラと最適化スイッチに応じて、一部のメソッドを次のようにマークします。 inline
役立つかもしれません (または無視されるかもしれません)。
GCC を使用する場合は、「-finline-functions」 (または -O3) フラグと、場合によっては「 -finline-limit=n」フラグを追加してみてください。
組み込みセットアップでコードのスタック要件を評価するためにどこかで読んだ 1 つのトリックは、開始時にスタック スペースを既知のパターン (16 進数の DEAD が私のお気に入り) で埋めて、システムをしばらく実行させるというものです。
通常の実行後、スタック領域を読み取り、動作中に置換されなかったスタック領域の量を確認します。実行されていない可能性があるすべてのあいまいなコード パスに対処できるように、少なくとも 150% を残すように設計します。
ローカル変数の一部をグローバル変数に置き換えることはできますか?特に配列はスタックを消費する可能性があります。
この状況で、機能間のいくつかのグローバル間でいくつかのグローバルを共有できる場合、メモリフットプリントを減らすことができる可能性があります。
トレードオフ コストは、複雑さが増し、関数間の望ましくない副作用のリスクが高まることと、メモリ フットプリントが小さくなる可能性があることです。
関数にはどのような種類の変数がありますか?どのようなサイズと制限について話しているのでしょうか?
コンパイラと、最適化オプションの積極性によっては、関数呼び出しごとにスタックが使用されます。したがって、まず最初に、関数呼び出しの深さを制限する必要があるでしょう。一部のコンパイラでは、単純な関数に対して分岐ではなくジャンプを使用するため、スタックの使用量が削減されます。明らかに、関数を直接呼び出すのではなく、アセンブラ マクロを使用して関数にジャンプすることで、同じことを行うことができます。
他の回答で述べたように、インライン化は利用可能なオプションの 1 つですが、コード サイズが大きくなります。
スタックを消費するもう 1 つの領域は、ローカル パラメーターです。この領域はある程度制御できます。(ファイルレベル) 静的を使用すると、静的 RAM 割り当てを犠牲にしてスタック割り当てを回避できます。グローバルも同様です。
(本当に) 極端なケースでは、スタック上のローカル変数の代わりに、固定数のグローバル変数を一時ストレージとして使用する関数の規則を思いつくことができます。難しいのは、同じグローバルを使用する関数が同時に呼び出されないようにすることです。(したがって慣例です)
スタック領域を確保する必要がある場合は、より優れたコンパイラを使用するか、より多くのメモリを使用する必要があります。
通常、ソフトウェアは成長するもの (新機能など) なので、スタック領域を確保する方法を考えてプロジェクトを開始する必要がある場合、最初から失敗する運命にあります。
はい、RTOS はタスク スタックの使用のために RAM を実際に消費する可能性があります。私の経験では、RTOS の新しいユーザーは、必要以上に多くのタスクを使用する傾向があります。
RTOS を使用する組み込みシステムにとって、RAM は貴重品になる可能性があります。RAM を節約するには、単純な機能の場合、協調的なマルチタスク設計を使用して、ラウンドロビン方式で実行する 1 つのタスク内に複数の機能を実装することが効果的です。したがって、タスクの総数が減ります。
おそらくここには存在しない問題を想像していると思います。ほとんどのコンパイラは、スタック上に自動変数を「割り当てる」とき、実際には何も行いません。
スタックは「main()」が実行される前に割り当てられます。関数a()から関数b()を呼び出すと、aが使用する最後の変数の直後の記憶領域のアドレスがb()に渡されます。b() が関数 c() を呼び出した場合、これが b() のスタックの開始点となり、c のスタックは b() で定義された最後の自動変数の後に開始されます。
スタック メモリはすでに存在して割り当てられており、初期化は行われず、関与する処理はスタック ポインタを渡すことだけであることに注意してください。
これが問題になるのは、3 つの関数すべてが大量のストレージを使用し、スタックが 3 つの関数すべてのメモリを収容する必要がある場合だけです。大量のストレージを割り当てる関数はコールスタックの一番下に置くようにしてください。それらから別の関数を呼び出さないでください。
メモリ制限システムのもう 1 つの手法は、関数のメモリを大量に消費する部分を独立した自己完結型関数に分割することです。