質問
組み込みシステムでは C++ のどの機能を避けるべきですか?
回答を次のような理由で分類してください。
- メモリ使用量
- コードサイズ
- スピード
- 携帯性
編集:回答の範囲を制御するターゲットとして 64k RAM を備えた ARM7TDMI を使用してみましょう。
解決
RTTI と例外処理:
- コードサイズが大きくなる
- パフォーマンスの低下
- 多くの場合、より安価なメカニズムやより優れたソフトウェア設計に置き換えることができます。
テンプレート:
- コードサイズが問題になる場合は注意してください。ターゲット CPU に命令キャッシュがない、または非常に小さい命令キャッシュしかない場合も、パフォーマンスが低下する可能性があります。(テンプレートは注意せずに使用するとコードが肥大化する傾向があります)。otoh の賢いメタプログラミングにより、コードサイズも削減できます。彼に関して明確な答えはありません。
仮想関数と継承:
- これらは私にとっては問題ありません。私は埋め込みコードのほぼすべてを C で作成しています。だからといって、仮想関数を模倣するために関数ポインター テーブルを使用することは止められません。それらがパフォーマンス上の問題になることはありませんでした。
他のヒント
特定の機能を避ける選択は、常に、動作の定量的分析に基づいて行う必要があります。 あなたの ソフトウェア、オン あなたの ハードウェア、付き あなたの 制約の下で選択されたツールチェーン あなたの ドメインが伴います。C++ 開発には、確かなデータではなく迷信や古代の歴史に基づいた「してはいけない」常識がたくさんあります。残念ながら、これにより、かつてどこかの誰かが問題を抱えていた機能の使用を避けるために、多くの追加の回避策コードが作成されることがよくあります。
何を避けるべきかについての最も一般的な答えは、おそらく例外です。ほとんどの実装では、かなり大きな静的メモリ コスト、または実行時メモリ コストがかかります。また、リアルタイム性の保証が難しくなる傾向があります。
見て ここ 組み込み C++ 用に書かれたコーディング標準の非常に良い例です。
文書「情報技術 - プログラミング言語、その環境、システムソフトウェアインターフェイス - C ++パフォーマンスに関する技術レポート" には、組み込みデバイス向けの C++ でのプログラミングに関する有益な情報も記載されています。
それは興味深い読み物です 理論的根拠 早い段階で 組み込み C++ 標準
これを参照してください 記事 EC++でも同様です。
Embedded C++ std は C++ の適切なサブセットでした。追加はありません。次の言語機能が削除されました。
- 多重継承
- 仮想基本クラス
- 実行時の型情報 (typeid)
- 新しいスタイルキャスト(static_cast、dynamic_cast、reintrepret_cast、const_cast)
- 可変型修飾子
- 名前空間
- 例外
- テンプレート
に記載されています ウィキページ Bjarne Stroustrupは(EC ++ STDの)、「私の知る限り、EC ++は死んでいる(2004年)であり、そうでなければそうではないなら」と言っています。 Stroustrupは推奨され続けます 書類 Prakashの回答で参照されています。
ARM7 を使用し、外部 MMU がないことを前提とすると、動的メモリ割り当ての問題のデバッグが難しくなる可能性があります。「new / delete / free / malloc の賢明な使用」をガイドラインのリストに追加したいと思います。
ARM7TDMI を使用している場合は、 避ける アライメントされていないメモリアクセス 何としても.
基本的な ARM7TDMI コアにはアライメント チェックがないため、アライメントされていない読み取りを実行すると、回転されたデータが返されます。一部の実装には、 ABORT
ただし、これらの実装がない場合、アライメントされていないアクセスによるバグを見つけるのは非常に困難です。
例:
const char x[] = "ARM7TDMI";
unsigned int y = *reinterpret_cast<const unsigned int*>(&x[3]);
printf("%c%c%c%c\n", y, y>>8, y>>16, y>>24);
- x86/x64 CPU では、「7TDM」と表示されます。
- SPARC CPU では、バス エラーでコアがダンプされます。
- ARM7TDMI CPU では、変数「x」が 32 ビット境界でアライメントされていると仮定して、「7ARM」または「ITDM」のような出力が行われる可能性があります (「x」がどこにあるか、および使用されているコンパイラ オプションによって異なります)。 、など)、リトルエンディアン モードを使用しています。これは未定義の動作ですが、希望どおりに動作しないことがほぼ保証されています。
ほとんどのシステムでは使用したくない 新しい / 消去 独自のマネージド ヒープから取得する独自の実装でそれらをオーバーライドしていない限り。はい、それはうまくいきますが、メモリに制約のあるシステムを扱っていることになります。
これには厳格なルールがあるとは言えません。それはアプリケーションに大きく依存します。組み込みシステムは通常次のとおりです。
- 利用可能なメモリ量がより制限される
- 多くの場合、より遅いハードウェアで実行されます
- ハードウェアに近い傾向があります。レジスタ設定をいじるなど、何らかの方法でそれを操作します。
ただし、他の開発と同様に、指定または導き出された要件に対して、言及したすべての点のバランスを取る必要があります。
コードの肥大化に関しては、犯人の可能性が非常に高いと思います。 列をなして テンプレートよりも。
例えば:
// foo.h
template <typename T> void foo () { /* some relatively large definition */ }
// b1.cc
#include "foo.h"
void b1 () { foo<int> (); }
// b2.cc
#include "foo.h"
void b2 () { foo<int> (); }
// b3.cc
#include "foo.h"
void b3 () { foo<int> (); }
リンカーはおそらく、「foo」のすべての定義を単一の翻訳単位にマージします。したがって、「foo」のサイズは他の名前空間関数のサイズと変わりません。
リンカーがこれを行わない場合は、明示的なインスタンス化を使用してこれを行うことができます。
// foo.h
template <typename T> void foo ();
// foo.cc
#include "foo.h"
template <typename T> void foo () { /* some relatively large definition */ }
template void foo<int> (); // Definition of 'foo<int>' only in this TU
// b1.cc
#include "foo.h"
void b1 () { foo<int> (); }
// b2.cc
#include "foo.h"
void b2 () { foo<int> (); }
// b3.cc
#include "foo.h"
void b3 () { foo<int> (); }
ここで次のことを考えてみましょう。
// foo.h
inline void foo () { /* some relatively large definition */ }
// b1.cc
#include "foo.h"
void b1 () { foo (); }
// b2.cc
#include "foo.h"
void b2 () { foo (); }
// b3.cc
#include "foo.h"
void b3 () { foo (); }
コンパイラーが「foo」をインライン化すると決定すると、「foo」の 3 つの異なるコピーが作成されることになります。テンプレートが表示されません。
編集: InSciTek Jeff による上記のコメントより
のみ使用されることがわかっている関数に対して明示的なインスタンス化を使用すると、未使用の関数をすべて確実に削除することもできます (これにより、テンプレートを使用しない場合に比べて実際にコード サイズが削減される可能性があります)。
// a.h
template <typename T>
class A
{
public:
void f1(); // will be called
void f2(); // will be called
void f3(); // is never called
}
// a.cc
#include "a.h"
template <typename T>
void A<T>::f1 () { /* ... */ }
template <typename T>
void A<T>::f2 () { /* ... */ }
template <typename T>
void A<T>::f3 () { /* ... */ }
template void A<int>::f1 ();
template void A<int>::f2 ();
ツールチェーンが完全に壊れていない限り、上記のコードは「f1」と「f2」のコードのみを生成します。
time 関数は通常、(書き換えない限り) OS に依存します。独自の関数を使用する (特に RTC がある場合)
コード用の十分なスペースがある限り、テンプレートを使用しても問題ありません。それ以外の場合は使用しないでください。
例外は移植性があまり高くありません
printf 関数 しないでください バッファへの書き込みは移植可能ではありません (printf を使用して FILE* に書き込むには、何らかの方法でファイルシステムに接続する必要があります)。sprintf、snprintf、str* 関数 (strcat、strlen) と、もちろんそれらの Wide char 対応関数 (wcslen...) のみを使用してください。
速度が問題になる場合は、STL ではなく独自のコンテナを使用する必要があるかもしれません (たとえば、キーが等しいことを確認する std::map コンテナは、 2 (はい 2) 「less」演算子との比較 ( a [less than] b == false && b [less than] a == false は a == b を意味します)。「less」は、std::map クラスによって受け取られる唯一の比較パラメータです (それだけではありません)。これにより、重要なルーチンでパフォーマンスが低下する可能性があります。
テンプレート、例外によりコード サイズが増加します (これは確実です)。コードが大きくなると、パフォーマンスにさえ影響が出る場合があります。
メモリ割り当て関数は、多くの点で OS に依存しているため (特にスレッド セーフなメモリ割り当てを扱う場合)、おそらく書き直す必要があります。
malloc は _end 変数 (通常はリンカー スクリプトで宣言されます) を使用してメモリを割り当てますが、これは「不明な」環境ではスレッド セーフではありません。
時々使用する必要があります 親指 アームモードではなく。パフォーマンスを向上させることができます。
したがって、64k メモリの場合、いくつかの優れた機能 (STL、例外など) を備えた C++ は過剰になる可能性があると言えます。私なら間違いなくCを選びます。
GCC ARM コンパイラと ARM 独自の SDT の両方を使用した場合、次のようなコメントが得られます。
ARM SDTはよりタイトで高速なコードを生成しますが、 とても 高価(> 1席あたり5,000ユーロ!)。私の以前の仕事では、このコンパイラを使用しましたが、大丈夫でした。
GCCアームツールは非常にうまく機能し、それは私が自分のプロジェクト(GBA/DS)で使用するものです。
これにより、コードサイズが大幅に削減されるため、「サム」モードを使用します。ARM の 16 ビット バス バージョン (GBA など) では、速度の面でも利点があります。
C ++開発の場合、64Kは非常に小さいです。その環境でC&Assemblerを使用します。
このような小さなプラットフォームでは、スタックの使用量に注意する必要があります。再帰、大規模な自動 (ローカル) データ構造などを避けてください。ヒープ使用量も問題になります (new、malloc など)。C を使用すると、これらの問題をより詳細に制御できるようになります。
組み込み開発または特定の組み込みシステムを対象とした開発環境を使用している場合、すでにいくつかのオプションが制限されているはずです。ターゲットのリソース機能に応じて、前述の項目の一部 (RTTI、例外など) がオフになります。サイズやメモリ要件が増加することを念頭に置くよりも、この方が簡単な方法です (ただし、いずれにせよ頭の中で理解しておく必要があります)。
組み込みシステムの場合、明らかに異常な実行コストがかかるものは避けたいと思うでしょう。いくつかの例:例外、および RTTI (含める ダイナミックキャスト そして タイプID).
組み込みプラットフォームのコンパイラでどの機能がサポートされているかを確認し、プラットフォームの特性も理解してください。たとえば、TI の CodeComposer コンパイラは、自動テンプレートのインスタンス化を行いません。その結果、STL のソートを使用したい場合は、5 つの異なるものを手動でインスタンス化する必要があります。また、ストリームもサポートしていません。
別の例としては、浮動小数点演算をハードウェアでサポートしていない DSP チップを使用している可能性があります。つまり、float または double を使用するたびに、関数呼び出しのコストが発生します。
要約すると、組み込みプラットフォームとコンパイラについて知っておくべきことをすべて理解すれば、どの機能を避けるべきかがわかります。
ATMega GCC 3 に関して私を驚かせた 1 つの特別な問題:仮想 ember 関数をクラスの 1 つに追加したとき、仮想デストラクターを追加する必要がありました。その時点で、リンカーは演算子 delete(void *) を要求しました。なぜそうなるのかわかりませんが、その演算子に空の定義を追加すると問題が解決しました。
例外のコストはコードによって異なることに注意してください。私がプロファイリングした 1 つのアプリケーション (ARM968 上の比較的小規模なアプリケーション) では、例外サポートによって実行時間が 2 % 増加し、コード サイズが 9.5 KB 増加しました。このアプリケーションでは、何か重大な問題が発生した場合にのみ例外がスローされました。実際には決してありません -- これにより、実行時間のオーバーヘッドが非常に低く抑えられました。