組み込みシステムでは、C ++のどの機能を避ける必要がありますか?
次のような理由で回答を分類してください。
- メモリ使用量
- コードサイズ
- 速度
- 移植性
編集:回答の範囲を制御するためのターゲットとして64kramを備えたARM7TDMIを使用してみましょう。
RTTI と例外処理:
テンプレート:
仮想関数と継承:
特定の機能を回避することを選択する場合は、常に、ドメインに伴う制約の下で、ハードウェア上で、選択したツールチェーンを使用してソフトウェアの動作を定量的に分析する必要があります。C++ 開発には、堅実なデータではなく迷信や古代の歴史に基づいた「してはいけない」通念がたくさんあります。残念ながら、これにより多くの場合、どこかで誰かがかつて問題を抱えていた機能を使用しないようにするために、多くの追加の回避策コードが作成されます。
何を避けるべきかについての最も一般的な答えは、おそらく例外です。ほとんどの実装では、かなり大きな静的メモリ コスト、またはランタイム メモリ コストが発生します。また、リアルタイム保証を難しくする傾向もあります。
埋め込み c++ 用に記述されたコーディング標準のかなり良い例については、こちらを参照してください。
初期の組み込み C++ 標準の理論的根拠の興味深い読み物です。
EC++ に関するこの記事も参照してください。
Embedded C++ std は、C++ の適切なサブセットでした。つまり、何も追加されていません。次の言語機能が削除されました。
Bjarne Stroustrup が (EC++ std について) 言っていることがwiki ページに記されています。Stroustrup は、Prakash の回答で参照されているドキュメントを推奨し続けています。
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);
ARM7 を使用し、外部 MMU を持っていないと仮定すると、動的メモリ割り当ての問題はデバッグが難しくなる可能性があります。ガイドラインのリストに「new / delete / free / malloc の賢明な使用」を追加します。
ほとんどのシステムでは、独自のマネージヒープからプルする独自の実装でオーバーライドしない限り、new / deleteを使用したくありません。はい、動作しますが、メモリに制約のあるシステムを扱っています。
時間関数は通常OSに依存します(書き直さない限り)。独自の関数を使用する(特にRTCがある場合)
テンプレートは、コード用の十分なスペースがある限り使用できます。それ以外の場合は使用しないでください。
例外もあまり移植性がありません
バッファに書き込まないprintf関数は移植性がありません(printfを使用してFILE *に書き込むには、何らかの方法でファイルシステムに接続する必要があります)。sprintf、snprintf、およびstr *関数(strcat、strlen)と、もちろんそれらのワイド文字の対応物(wcslen ...)のみを使用してください。
速度が問題になる場合は、STLではなく独自のコンテナを使用する必要があります(たとえば、キーが等しいことを確認するためのstd :: mapコンテナは、「less」演算子([less than] )と2回(yes 2)比較します。 b == false && b [less than] a == false mean a == b)。'less'は、std :: mapクラスが受け取る唯一の比較パラメーターです(これだけではありません)。これにより、パフォーマンスが低下する可能性があります。重要なルーチンで。
テンプレート、例外によりコードサイズが大きくなっています(これは確かです)。より大きなコードを使用すると、パフォーマンスにさえ影響する場合があります。
メモリ割り当て関数は、多くの点でOSに依存しているため(特にスレッドセーフのメモリ割り当てを処理する場合)、おそらく書き直す必要があります。
mallocは_end変数(通常はリンカースクリプトで宣言されます)を使用してメモリを割り当てますが、これは「不明な」環境ではスレッドセーフではありません。
ArmモードではなくThumbを使用する必要がある場合があります。パフォーマンスを向上させることができます。
したがって、64kメモリの場合、いくつかの優れた機能(STL、例外など)を備えたC++はやり過ぎになる可能性があります。私は間違いなくCを選びます。
コードの膨張に関しては、犯人はテンプレートよりもインラインである可能性がはるかに高いと思います。
例えば:
// 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ジェフからの上記のコメントから
使用されることがわかっている関数の明示的なインスタンス化を使用して、未使用の関数をすべて削除することもできます(これにより、テンプレート以外の場合と比較して、実際にコードサイズが小さくなる可能性があります)。
// 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」のコードのみを生成します。
GCC ARM コンパイラと ARM 独自の SDT の両方を使用したので、次のコメントがあります。
ARM SDT は、よりタイトで高速なコードを生成しますが、非常に高価です (1 シートあたり 5,000 ユーロ以上!)。私の前の仕事では、このコンパイラを使用していましたが、問題ありませんでした。
ただし、GCC ARM ツールは非常にうまく機能し、私自身のプロジェクト (GBA/DS) で使用しています。
コードサイズを大幅に削減するため、「サム」モードを使用してください。ARM の 16 ビット バス バリアント (GBA など) では、速度の利点もあります。
64k は、C++ 開発にとって非常に小さいです。その環境ではCとアセンブラーを使用します。
このような小さなプラットフォームでは、スタックの使用に注意する必要があります。再帰、大規模な自動 (ローカル) データ構造などは避けてください。ヒープの使用も問題になります (new、malloc など)。C を使用すると、これらの問題をより詳細に制御できます。
これには厳格な規則があるとは言いませんでした。それはあなたのアプリケーションに大きく依存します。組み込みシステムは通常、次のとおりです。
ただし、他の開発と同様に、言及したすべてのポイントと、与えられた/派生した要件とのバランスを取る必要があります。
組み込みプラットフォームのコンパイラでサポートされている機能を確認し、プラットフォームの特性も確認してください。たとえば、TI の CodeComposer コンパイラは、テンプレートの自動インスタンス化を行いません。その結果、STL のソートを使用する場合は、5 つの異なるものを手動でインスタンス化する必要があります。また、ストリームもサポートしていません。
もう 1 つの例は、浮動小数点演算をハードウェアでサポートしていない DSP チップを使用している場合です。つまり、float や double を使用するたびに、関数呼び出しのコストが発生します。
要約すると、組み込みプラットフォームとコンパイラについて知っておくべきことをすべて理解すれば、避けるべき機能がわかります。
例外のコストはコードによって異なることに注意してください。私がプロファイリングした 1 つのアプリケーション (ARM968 上の比較的小さなアプリケーション) では、例外サポートによって実行時間が 2% 増加し、コード サイズが 9.5 KB 増加しました。このアプリケーションでは、重大な問題が発生した場合 (つまり、実際には発生しない場合) にのみ例外がスローされ、実行時間のオーバーヘッドが非常に低く抑えられました。
ATMega GCC 3.something で私を驚かせた 1 つの特定の問題: クラスの 1 つに仮想 ember 関数を追加したときに、仮想デストラクタを追加する必要がありました。その時点で、リンカーは演算子 delete(void *) を要求しました。なぜそれが起こるのかわかりません。その演算子に空の定義を追加すると、問題が解決しました。
組み込み開発または特定の組み込みシステムを対象とした開発環境を使用している場合、すでにいくつかのオプションが制限されているはずです。ターゲットのリソース機能に応じて、前述の項目の一部 (RTTI、例外など) がオフになります。これは、何がサイズやメモリ要件を増加させるかを念頭に置くよりも、より簡単な方法です (ただし、いずれにせよ、それを頭で理解する必要があります)。
組み込みシステムの場合、明らかに異常なランタイム コストがかかるものは避けたいと思うでしょう。いくつかの例: 例外とRTTI ( dynamic_castとtypeidを含めるため)。