より良いパフォーマンスを得るために、組み込みソフトウェア用の関数を作成する際の最良のアプローチは何ですか?[閉まっている]


13

マイクロコントローラ用のライブラリをいくつか見てきましたが、それらの機能は一度に1つのことを行います。たとえば、次のようなものです:

void setCLK()
{
    // Code to set the clock
}

void setConfig()
{
    // Code to set the config
}

void setSomethingElse()
{
   // 1 line code to write something to a register.
}

次に、その上に、関数を含むこの1行のコードを使用して他の目的に役立つ他の関数を追加します。例えば:

void initModule()
{
   setCLK();
   setConfig();
   setSomethingElse();
}

確かではありませんが、この方法では、ジャンプの呼び出しが増え、関数が呼び出されるか終了するたびに戻りアドレスをスタックするオーバーヘッドが発生すると思います。そして、それはプログラムの動作を遅くするでしょう?

私は検索しましたが、プログラミングの経験則では、関数は1つのタスクのみを実行する必要があると彼らは言います。

したがって、クロックを設定するInitModule汎用モジュールを直接記述し、必要な構成を追加し、関数を呼び出さずに他のことを行います。組み込みソフトウェアを書くとき、それは悪いアプローチですか?


編集2:

  1. 私はプログラムを最適化しようとしているように多くの人がこの質問を理解しているようです。いいえ、私はそうするつもりはありません。私はコンパイラーにそれを常に任せています。なぜなら、それは私よりも常に優れている(しかしそうではないことを願っています!)からです。

  2. 何らかの初期化コードを表す例を選択したことについて、私はすべて非難しています。この質問には、初期化のために行われた関数呼び出しに関する意図はありません。私の質問は、特定のタスクを無限ループ内で実行する複数行の小さな関数に分割するためインラインは問題外です)、ネストされた関数なしで長い関数を書くよりも利点がありますか?

@Jonk回答で定義されている読みやすさを考慮してください。


28
妥当なコンパイラーが、書かれたままのコードを盲目的に書かれたままバイナリーに変えると信じているなら、あなたは非常に素朴です(in辱するつもりはありません)。ほとんどの最新のコンパイラは、ルーチンのインライン化の改善時期、および変数を保持するためにレジスタとRAMの場所を使用する時期の特定に非常に優れています。最適化の2つのルールに従います。1)最適化しない。2)まだ最適化しないでください。コードを読みやすく保守しやすくし、動作中のシステムをプロファイリングした後にのみ最適化します。
akohlsmith

10
@akohlsmith IIRC 最適化の3つのルールは次のとおりです。1)しないでください!2)いいえ、そうではありません!あなたがいる場合3)最適化だけにして、その後、最初のプロファイルとしなければならない - Michael_A._Jackson
esoterik

3
ただ、その「時期尚早の最適化は諸悪の根源である(またはそのほとんど少なくとも)プログラミングにおける」覚えて- クヌース
MawgはREINSTATEモニカ言う

1
@Mawg:有効な言葉は時期尚早です。(その論文の次の段落で説明します。文字通り次の文:「その重要な3%で機会を逃してはなりません。」)必要になるまで最適化しないでください。プロファイリングするものがあるまでは少しですが、たとえば仕事に露骨に間違ったツールを使用するなど、悲観的な見方をしないでください。
cHao

1
@Mawg最適化に関連する回答やフィードバックを受け取った理由がわかりません。単語に言及したことがないため、それを行うつもりです。問題は、より良いパフォーマンスを達成するために、組み込みプログラミングで関数を記述する方法についてです。
マニーヤック

回答:


28

おそらく、コードは起動時に1回しか実行されないため、この例ではパフォーマンスは問題になりません。

私が使用する経験則:可能な限り読みやすいコードを作成し、コンパイラーが適切に魔法を実行していないことに気付いた場合にのみ最適化を開始します。

ISRでの関数呼び出しのコストは、ストレージとタイミングの点で、起動中の関数呼び出しのコストと同じになる場合があります。ただし、そのISR中のタイミング要件ははるかに重要な場合があります。

さらに、他の人がすでに気づいているように、関数呼び出しのコスト(および「コスト」の意味)は、プラットフォーム、コンパイラ、コンパイラ最適化設定、およびアプリケーションの要件によって異なります。8051とcortex-m7、およびペースメーカーとライトスイッチには大きな違いがあります。


6
IMOの2番目の段落は太字で上部にある必要があります。適切なアルゴリズムとデータ構造をすぐに選択することには何の問題もありませんが、実際のボトルネックであることがわかっていない限り、関数呼び出しのオーバーヘッドを心配することは間違いなく時期尚早な最適化であり、避けるべきです。
ファンドモニカの訴訟

11

私が考えることができる利点はありません(ただし、下部のJasonSのメモを参照)。1行のコードを関数またはサブルーチンとしてラップします。おそらく、関数に「読み取り可能な」名前を付けることができることを除いて。ただし、行にコメントを付けることもできます。また、関数内のコード行をラップすると、コードメモリ、スタック領域、および実行時間がかかるため、ほとんどの場合、非生産的。教育状況ですか?理にかなっているかもしれません。しかし、それは生徒のクラス、事前の準備、カリキュラム、教師によって異なります。たいてい、それは良い考えではないと思います。しかし、それは私の意見です。

それは私たちに最終的な結果をもたらします。あなたの幅広い質問領域は、何十年もの間、いくつかの議論の問題であり、今日までいくつかの議論の問題のままです。ですから、少なくともあなたの質問を読んだとき、私は意見に基づいた質問であるように思われます(あなたが尋ねたように)。

あなたが状況についてより詳細になり、あなたがプライマリとして保持した目的を慎重に説明することになった場合、それは意見に基づくものから離れることができます。測定ツールを適切に定義するほど、回答の客観性が高まります。


大まかに言えば、あなたがのために次の操作を行いたい任意のコーディング。(以下では、すべてが目標を達成するさまざまなアプローチを比較していると仮定します。明らかに、必要なタスクを実行できないコードは、作成方法に関係なく成功するコードよりも悪いです。)

  1. アプローチについて一貫性を保ち、コードを読んでいる別の人がコーディングプロセスへのアプローチ方法を理解できるようにします。一貫性のないことは、おそらく最悪の犯罪です。他の人にとってそれが難しくなるだけでなく、数年後にコードに戻るのが難しくなります。
  2. 可能な限り、さまざまな機能セクションの初期化を順序に関係なく実行できるように、物事を調整してください。順序付けが必要な場合、2つの関連性の高いサブ関数の密接な結合が原因である場合、両方の単一の初期化を検討して、害を及ぼすことなく並べ替えることができます。それが不可能な場合は、初期化順序の要件を文書化します。
  3. カプセル化された知識可能であれば、を1か所にます。定数は、コード内のすべての場所に複製しないでください。ある変数について解く方程式は、ただ1つの場所に存在する必要があります。等々。さまざまな場所で必要な動作を実行する行のセットをコピーして貼り付けることに気付いた場合は、その知識を1か所に取り込み、必要な場所で使用する方法を検討してください。たとえば、特定の方法で歩いてしなければならないツリー構造を持っている場合は、行いませんは、ツリーノードをループする必要があるすべての場所でツリーウォーキングコードを複製します。代わりに、ツリーウォークメソッドを1か所でキャプチャして使用します。このように、ツリーが変更され、ウォーキング方法が変更された場合、心配する場所は1つだけで、残りのコードはすべて「正しく機能します」。
  4. すべてのルーチンを他のルーチンから呼び出される矢印でつなげた巨大な平らな紙に広げると、多くの矢印を持つルーチンの「クラスター」がアプリケーションに表示されます。グループ間ではなく、少数の矢印のみ。したがって、密接に結合されたルーチンの自然な境界と、密接に結合されたルーチンの他のグループ間に疎結合された接続が存在します。この事実を使用して、コードをモジュールに編成します。これにより、コードの見た目の複雑さが大幅に制限されます。

上記は、一般的にすべてのコーディングについて当てはまります。パラメーター、ローカルまたは静的グローバル変数などの使用については説明しませんでした。理由は、組み込みプログラミングの場合、アプリケーション空間に極端で非常に重要な新しい制約が課せられることが多いためです。とにかく、それはここでは起きていません。

これらの制約は、次のいずれか(およびそれ以上)になります。

  • 極小のRAMを備え、I / Oピン数がほとんどない非常に原始的なMCUを必要とする厳しいコスト制限。これらには、まったく新しいルールセットが適用されます。たとえば、コードスペースがあまりないため、アセンブリコードを記述する必要があります。ローカル変数の使用はコストと時間がかかりすぎるため、静的変数のみを使用する必要があります。(たとえば、一部のMicrochip PICパーツ)サブルーチン戻りアドレスを格納するハードウェアレジスタが4つしかないため、サブルーチンの過剰な使用を避ける必要がある場合があります。そのため、コードを劇的に「平坦化」する必要があります。等。
  • MCUのほとんどを起動およびシャットダウンするために慎重に作成されたコードを必要とし、フルスピードで実行する場合のコードの実行時間に厳しい制限を課す厳しい電力制限。繰り返しますが、これにはいくつかのアセンブリコーディングが必要になる場合があります。
  • 厳しいタイミング要件。たとえば、オープンドレイン0の送信が1の送信とまったく同じサイクル数をとる必要があることを確認しなければならない場合があります。また、この同じラインのサンプリングも実行する必要がありました。このタイミングに対する正確な相対位相で。つまり、ここではCを使用できません。この保証を行う唯一の方法は、アセンブリコードを慎重に作成することです。(それでも、すべてのALU設計で常にではありません。)

等々。(生命にかかわる医療機器の配線コードにも独自の世界があります。)

ここでの結果は、組み込みコーディングは、多くの場合、自由に使えるものではなく、ワークステーションでコーディングするのと同じようにコーディングできることです。多種多様な非常に困難な制約には、しばしば厳しい競争上の理由があります。そして、これらは強く、より反論も伝統株式の答え。


読みやすさに関しては、読みながら学習できる一貫した方法で記述されている場合、コードは読みやすいことがわかります。そして、コードを難読化する意図的な試みがない場合。実際には、それほど多くは必要ありません。

読み取り可能なコードは非常に効率的であり、前述の要件をすべて満たすことができます。主なことは、コーディングするときに、アセンブリレベルまたはマシンレベルで記述する各コード行が何を生成するかを完全に理解することです。C ++ は、C ++コードの同一のスニペットが大幅に異なるパフォーマンスを持つマシンコードの異なるスニペットを実際に生成する多くの状況があるため、ここでプログラマに深刻な負担をかけます。しかし、一般的に、Cは大部分が「見たものが得たもの」言語です。その点でより安全です。


JasonSごとの編集:

私は1978年からC、1987年頃からC ++を使用しており、メインフレーム、ミニコンピューター、および(ほとんど)組み込みアプリケーションの両方で両方を使用した経験が豊富です。

ジェイソンは、「インライン」を修飾子として使用することについてコメントを出します。(私の視点では、これは比較的新しい機能です。なぜなら、CとC ++を使用すると、おそらく私の人生の半分以上は存在しなかったからです。)インライン関数を使用すると、コード)非常に実用的。また、可能な場合は、コンパイラが適用できる型付けのためにマクロを使用するよりもはるかに優れています。

しかし、制限もあります。1つ目は、コンパイラーに頼って「ヒントを得る」ことができないことです。それはそうかもしれないし、そうでないかもしれない。そして、ヒントを受け取らない正当な理由があります。(関数のアドレスが取得された場合に明白な例については、これは必要で関数のインスタンス化や...コールが必要になります電話をかけるには、アドレスの使用を。コードは、その後インライン化することはできません。)があります他の理由も同様です。コンパイラには、ヒントの処理方法を判断するためのさまざまな基準があります。そしてプログラマーとして、これはあなたがしなければならないことを意味しますコンパイラのその側面について学習するか、欠陥のあるアイデアに基づいて決定を下す可能性が高いです。そのため、コードの作成者だけでなく、読者だけでなく、コードを他のコンパイラに移植する予定の人にも負担がかかります。

また、CおよびC ++コンパイラは個別のコンパイルをサポートしています。これは、プロジェクトに関連する他のコードをコンパイルすることなく、CまたはC ++コードの一部をコンパイルできることを意味します。コードをインライン化するには、コンパイラーがそうすることを選択する可能性があると仮定すると、「スコープ内」の宣言が必要なだけでなく、定義も必要です。通常、プログラマは、「インライン」を使用している場合にこれが当てはまることを確認するように働きます。しかし、間違いが忍び込むのは簡単です。

一般的に、適切だと思う場所でもインラインを使用しますが、私はそれを信頼できないと思いがちです。パフォーマンスが重要な要件であり、OPが既に「機能的な」ルートに行ったときにパフォーマンスが大幅に低下したことを明確に書いていると思う場合、コーディングプラクティスとしてインラインに依存することを避け、代わりに、わずかに異なるが完全に一貫したコード記述パターンに従います。

「インライン」と、別のコンパイル手順の「スコープ内」にある定義に関する最後の注意。リンク段階で作業を実行することは可能です(常に信頼できるとは限りません)。これは、C / C ++コンパイラがオブジェクトファイルに十分な詳細を埋め込んで、リンカーが「インライン」リクエストに対応できるようにする場合にのみ発生します。私は個人的に、この機能をサポートするリンカーシステム(Microsoftの外部)を経験していません。しかし、それは起こる可能性があります。繰り返しますが、それを信頼すべきかどうかは状況に依存します。しかし、良い証拠に基づいて別の方法で知っている場合を除き、私は通常、これはリンカにシャベルされていないと仮定します。そして、私がそれに頼るなら、それは目立つ場所で文書化されるでしょう。


C ++

関心のある方のために、組み込みアプリケーションをすぐに入手できるにもかかわらず、組み込みアプリケーションをコーディングする際にC ++にかなり注意を払っている理由の例を次に示します。私はすべての組み込みC ++プログラマーが風邪を知る必要があると思ういくつかの用語を投げます:

  • 部分テンプレート特化
  • vtables
  • 仮想ベースオブジェクト
  • アクティベーションフレーム
  • アクティベーションフレームの巻き戻し
  • コンストラクターでのスマートポインターの使用とその理由
  • 戻り値の最適化

これはほんの短いリストです。これらの用語についてのすべてをまだ知らない場合、およびそれらをリストした理由(そして、ここにリストしなかった他の多くの理由)は、プロジェクトのオプションでない限り、組み込み作業にC ++を使用することをお勧めします。

C ++例外セマンティクスを簡単に見て、フレーバーを取得してみましょう。

AB

A

   .
   .
   foo ();
   String s;
   foo ();
   .
   .

A

B

C ++コンパイラは、foo()の最初の呼び出しを確認し、foo()が例外をスローした場合に、通常のアクティベーションフレームの巻き戻しを許可します。つまり、C ++コンパイラは、例外処理に関連するフレーム展開プロセスをサポートするために、この時点で余分なコードが必要ないことを知っています。

ただし、Stringが作成されると、C ++コンパイラは、後で例外が発生した場合、フレームの巻き戻しを許可する前に適切に破棄する必要があることを認識します。したがって、foo()の2番目の呼び出しは、最初の呼び出しと意味的に異なります。foo()の2回目の呼び出しが例外をスローする場合(例外を実行する場合もしない場合もあります)、コンパイラーは、通常のフレームの巻き戻しを行う前にStringの破壊を処理するように設計されたコードを配置する必要があります。これは、foo()の最初の呼び出しに必要なコードとは異なります。

(この問題を制限するためにC ++に追加の装飾を追加することは可能です。しかし、実際には、C ++を使用するプログラマーは、記述するコードの各行の意味をはるかに意識する必要があります。)

Cのmallocとは異なり、C ++の新機能は例外を使用して、未加工のメモリ割り当てを実行できないことを通知します。「dynamic_cast」も同様です。(C ++の標準的な例外については、Stroustrupの第3版、C ++プログラミング言語、384ページおよび385ページを参照してください。)コンパイラは、この動作を無効にすることができます。しかし、一般に、生成されたコード内の適切に形成された例外処理プロローグとエピローグにより、例外が実際に発生しない場合や、コンパイルされている関数に実際に例外処理ブロックがない場合でも、オーバーヘッドが発生します。(Stroustrupはこれを公に嘆きました。)

部分的なテンプレートの特殊化がない場合(すべてのC ++コンパイラがサポートしているわけではありません)、テンプレートを使用すると、組み込みプログラミングに大きな打撃を与える可能性があります。これがないと、コードブルームは深刻なリスクであり、フラッシュ内の小さなメモリの組み込みプロジェクトを殺す可能性があります。

C ++関数がオブジェクトを返すと、名前のないコンパイラ一時ファイルが作成され、破棄されます。一部のC ++コンパイラは、ローカルオブジェクトの代わりにオブジェクトコンストラクターがreturnステートメントで使用される場合に効率的なコードを提供できるため、1つのオブジェクトによる構築と破棄の必要性が減少します。しかし、すべてのコンパイラがこれを行うわけではなく、多くのC ++プログラマーは、この「戻り値の最適化」を認識していません。

オブジェクトコンストラクターに単一のパラメーター型を指定すると、C ++コンパイラーは、プログラマーにとってまったく予期しない方法で2つの型間の変換パスを見つけることができます。この種の「スマート」動作はCの一部ではありません。

スローされたオブジェクトは、オブジェクトの「動的タイプ」ではなく、catch句の「静的タイプ」を使用してコピーされるため、ベースタイプを指定するcatch句は、スローされた派生オブジェクトを「スライス」します。例外の悲惨さのまれではないソース(埋め込みコードに例外を追加する余裕さえあると感じるとき)

C ++コンパイラは、意図しない結果を伴うコンストラクタ、デストラクタ、コピーコンストラクタ、および代入演算子を自動的に生成できます。この詳細を使用して施設を取得するには時間がかかります。

派生オブジェクトの配列を基本オブジェクトの配列を受け入れる関数に渡すと、コンパイラー警告が生成されることはほとんどありませんが、ほとんどの場合、不正な動作が発生します。

C ++は、オブジェクトコンストラクターで例外が発生した場合、部分的に構築されたオブジェクトのデストラクターを呼び出さないため、コンストラクターで例外が発生した場合、コンストラクターで構築されたフラグメントが適切に破棄されることを保証するために、通常、「スマートポインター」が必要です。(Stroustrup、ページ367および368を参照)。これは、C ++で適切なクラスを記述する際の一般的な問題ですが、Cには構築および破棄のセマンティクスが組み込まれていないため、Cではもちろん回避されます。構築を処理する適切なコードの記述オブジェクト内のサブオブジェクトのこととは、C ++のこの独特のセマンティック問題に対処しなければならないコードを書くことを意味します。言い換えると、C ++のセマンティック動作を「書き回し」ます。

C ++は、オブジェクトパラメータに渡されたオブジェクトをコピーできます。たとえば、次のフラグメントでは、呼び出し「rA(x);」C ++コンパイラーはパラメーターpのコンストラクターを呼び出し、オブジェクトxをパラメーターpに転送するためにコピーコンストラクターを呼び出し、関数rAの戻りオブジェクト(名前のない一時的な)の別のコンストラクターを呼び出します。パラメータpからコピー。さらに悪いことに、クラスAが構築を必要とする独自のオブジェクトを持っている場合、これは悲惨に望遠鏡になります。(Cプログラマーはそのような便利な構文を持たず、一度にすべての詳細を表現する必要があるため、ACプログラマーはこのゴミのほとんどを回避し、手で最適化します。)

    class A {...};
    A rA (A p) { return p; }
    // .....
    { A x; rA(x); }

最後に、Cプログラマー向けの短いメモ。longjmp()は、C ++では移植性のある動作をしません。(一部のCプログラマーは、これを一種の「例外」メカニズムとして使用します。)一部のC ++コンパイラーは、longjmpの取得時に実際にクリーンアップを試行しますが、その動作はC ++では移植できません。コンパイラが構築されたオブジェクトをクリーンアップすると、移植性がなくなります。コンパイラがそれらをクリーンアップしない場合、longjmpの結果としてコードが構築されたオブジェクトのスコープを離れ、動作が無効である場合、オブジェクトは破壊されません。(foo()でlongjmpを使用しても範囲が失われない場合、動作は問題ないかもしれません。)これは、C組み込みプログラマーによってあまり使用されることはありませんが、使用する前にこれらの問題を自覚する必要があります。


4
一度だけ使用されるこの種の関数は、関数呼び出しとしてコンパイルされることはありません。コードは呼び出しなしで単にそこに配置されます。
ドリアン

6
@Dorian-特定のコンパイラの特定の状況下であなたのコメントは真実かもしれません。関数がファイル内で静的である場合、コンパイラにはコードをインラインにするオプションがあります。外部から見える場合、実際に呼び出されなくても、関数を呼び出し可能にする方法が必要です。
uɐɪ

1
@jonk-適切な答えで言及していないもう1つのトリックは、拡張されたインラインコードとして初期化または構成を実行する単純なマクロ関数を記述することです。これは、RAM /スタック/関数呼び出しの深さが制限されている非常に小さなプロセッサで特に役立ちます。
uɐɪ

@ʎəʞouɐɪはい、Cでマクロについて説明するのを逃しました。これらはC ++で非推奨になりましたが、その点に関する説明が役立つ場合があります。それについて書くのに役立つ何かを考え出すことができれば、私はそれに取り組むかもしれません。
18

1
@jonk-私はあなたの最初の文にはまったく同意しません。inline static void turnOnFan(void) { PORTAbits &= ~(1<<8); }多くの場所で呼ばれているような例は完璧な候補です。
ジェイソンS

8

1)最初に読みやすさと保守性のためのコード。コードベースの最も重要な側面は、適切に構造化されていることです。うまく書かれたソフトウェアは、エラーが少ない傾向があります。数週間/月/年で変更が必要になる場合があります。コードが読みやすい場合は、非常に役立ちます。または、誰かが変更を加える必要があるかもしれません。

2)1回実行されるコードのパフォーマンスはそれほど重要ではありません。パフォーマンスではなくスタイルに注意

3)タイトなループ内のコードでさえ、何よりもまず正確である必要があります。パフォーマンスの問題に直面した場合は、コードが正しくなったら最適化してください。

4)最適化する必要がある場合は、測定する必要があります!あなたが考えるか、誰かがあなたにそれstatic inlineがコンパイラへのただの推薦であると言うかどうかは関係ありません。コンパイラが何をするのかを見てみる必要があります。また、インライン化によってパフォーマンスが改善されたかどうかも測定する必要があります。組み込みシステムでは、コードサイズも測定する必要があります。コードメモリは通常かなり制限されているためです。これは、エンジニアリングと推測を区別する最も重要なルールです。それを測定しなければ、助けにはなりませんでした。エンジニアリングは測定中です。科学はそれを書き留めています;)


2
それ以外の点で優れた投稿に対して私が持っている唯一の批判は、ポイント2)です。初期化コードのパフォーマンスが無関係であることは事実ですが、組み込み環境ではサイズが重要になる場合があります。(しかし、その点1を上書きしません。あなたがする必要がある場合はサイズのために最適化を開始-前ではなく)
マーティン・ボナーはモニカサポート

2
初期化コードのパフォーマンスは、最初は無関係かもしれません。低電力モードを追加し、ウェイクアップイベントを処理するために迅速に回復したい場合は、関連性が高くなります。
berendi

5

関数が1か所でのみ呼び出されると(他の関数内でも)、コンパイラーは実際に関数を呼び出す代わりに、常にその場所にコードを配置します。関数が多くの場所で呼び出される場合、少なくともコードサイズの観点から関数を使用することは理にかなっています。

コードをコンパイルすると、代わりに複数の呼び出しがなくなり、読みやすさが大幅に向上します。

また、たとえば、メインのcファイルにない他のADC関数と同じライブラリにADC initコードを保持する必要があります。

多くのコンパイラでは、速度またはコードサイズの最適化のさまざまなレベルを指定できるため、多くの場所で呼び出される小さな関数がある場合、関数は「インライン化」され、呼び出しではなくコピーされます。

速度の最適化は可能な限り多くの場所で関数をインライン化しますが、コードサイズの最適化は関数を呼び出しますが、関数が1つの場所でのみ呼び出されると、常に「インライン化」されます。

このようなコード:

function_used_just_once{
   code blah blah;
}
main{
  codeblah;
  function_used_just_once();
  code blah blah blah;
{

にコンパイルされます:

main{
 code blah;
 code blah blah;
 code blah blah blah;
}

呼び出しを使用せずに。

そして、あなたの質問への答えは、あなたの例などでは、コードの可読性はパフォーマンスに影響しません。速度やコードサイズに大きな影響はありません。コードを読みやすくするためだけに複数の呼び出しを使用するのが一般的です。最後に、それらはインラインコードとしてコンパイルされます。

上記の文が、Microchip XCxxの無料版のような不自由な無料版コンパイラに対しては無効であることを指定するように更新します。この種の関数呼び出しは、Microchipが有料版がどれだけ優れているかを示す金鉱であり、これをコンパイルすると、ASMでCコードとまったく同じ呼び出しが見つかります。

また、インライン関数へのポインターを使用することを期待する愚かなプログラマーのためではありません。

これは一般的なC C ++やプログラミングのセクションではなく、エレクトロニクスのセクションです。問題は、適切なコンパイラーがデフォルトで上記の最適化を行うマイクロコントローラープログラミングについてです。

まれに、まれなケースではこれが真実ではない可能性があるため、ダウン投票を停止してください。


15
コードがインラインになるかどうかは、コンパイラベンダーの実装固有の問題です。inlineキーワードを使用しても、インラインコードは保証されません。これはコンパイラーへのヒントです。確かに優れたコンパイラーは、知っていれば一度だけ使用される関数をインライン化します。ただし、スコープ内に「揮発性」オブジェクトがある場合、通常はそうしません。
ピータースミス

9
この答えは真実ではありません。@PeterSmithが言うように、C言語の仕様によれば、コンパイラにはコードをインライン化するオプションがありますが、インライン化されない場合があり、多くの場合インライン化されません。世界には非常に多くのさまざまなターゲットプロセッサ向けの非常に多くの異なるコンパイラが存在するため、この回答で一種のブランケットステートメントを作成し、すべてのコンパイラがオプションがない場合にのみコードをインラインに配置できると想定しています。
uɐɪ

2
@ʎəʞouɐɪあなたは不可能なまれなケースを指摘しており、そもそも関数を呼び出さないことは悪い考えです。OPで指定された簡単な例で実際にcallを使用するほど愚かなコンパイラを見たことはありません。
ドリアン

6
これらの関数が一度だけ呼び出される場合、関数呼び出しの最適化はほとんど問題ではありません。セットアップ中に、システムはクロックサイクルごとに本当に引き戻す必要がありますか?どこでも最適化を行う場合と同様に、読み取り可能なコードを記述し、プロファイリングが必要であることを示している場合にのみ最適化します
Baldrickk

5
@MSaltersここでコンパイラが何をするかは気にしません-プログラマーがどのようにアプローチするかということです。質問に見られるように、初期化を分割してもパフォーマンスヒットはないか、無視できる程度です。
Baldrickk

2

まず第一に、最良または最悪はありません。それはすべて意見の問題です。これは非効率的であることは非常に正しいです。最適化することもしないこともできます。場合によります。通常、これらのタイプの関数、クロック、GPIO、タイマーなどは、別々のファイル/ディレクトリに表示されます。コンパイラは通常、これらのギャップを越えて最適化することができませんでした。私が知っているが、このようなものには広く使用されていないものがあります。

単一ファイル:

void dummy (unsigned int);

void setCLK()
{
    // Code to set the clock
    dummy(5);
}

void setConfig()
{
    // Code to set the configuration
    dummy(6);
}

void setSomethingElse()
{
   // 1 line code to write something to a register.
    dummy(7);
}

void initModule()
{
   setCLK();
   setConfig();
   setSomethingElse();
}

デモンストレーション用のターゲットとコンパイラの選択。

Disassembly of section .text:

00000000 <setCLK>:
   0:    e92d4010     push    {r4, lr}
   4:    e3a00005     mov    r0, #5
   8:    ebfffffe     bl    0 <dummy>
   c:    e8bd4010     pop    {r4, lr}
  10:    e12fff1e     bx    lr

00000014 <setConfig>:
  14:    e92d4010     push    {r4, lr}
  18:    e3a00006     mov    r0, #6
  1c:    ebfffffe     bl    0 <dummy>
  20:    e8bd4010     pop    {r4, lr}
  24:    e12fff1e     bx    lr

00000028 <setSomethingElse>:
  28:    e92d4010     push    {r4, lr}
  2c:    e3a00007     mov    r0, #7
  30:    ebfffffe     bl    0 <dummy>
  34:    e8bd4010     pop    {r4, lr}
  38:    e12fff1e     bx    lr

0000003c <initModule>:
  3c:    e92d4010     push    {r4, lr}
  40:    e3a00005     mov    r0, #5
  44:    ebfffffe     bl    0 <dummy>
  48:    e3a00006     mov    r0, #6
  4c:    ebfffffe     bl    0 <dummy>
  50:    e3a00007     mov    r0, #7
  54:    ebfffffe     bl    0 <dummy>
  58:    e8bd4010     pop    {r4, lr}
  5c:    e12fff1e     bx    lr

これは、ここでの答えのほとんどがあなたに言っていることであり、あなたは素朴であり、これはすべて最適化され、機能は削除されているということです。これらはデフォルトでグローバルに定義されているため、削除されません。この1つのファイルの外で必要でない場合は削除できます。

void dummy (unsigned int);

static void setCLK()
{
    // Code to set the clock
    dummy(5);
}

static void setConfig()
{
    // Code to set the configuration
    dummy(6);
}

static void setSomethingElse()
{
   // 1 line code to write something to a register.
    dummy(7);
}

void initModule()
{
   setCLK();
   setConfig();
   setSomethingElse();
}

インライン化されるとすぐにそれらを削除します。

Disassembly of section .text:

00000000 <initModule>:
   0:    e92d4010     push    {r4, lr}
   4:    e3a00005     mov    r0, #5
   8:    ebfffffe     bl    0 <dummy>
   c:    e3a00006     mov    r0, #6
  10:    ebfffffe     bl    0 <dummy>
  14:    e3a00007     mov    r0, #7
  18:    ebfffffe     bl    0 <dummy>
  1c:    e8bd4010     pop    {r4, lr}
  20:    e12fff1e     bx    lr

しかし現実は、チップベンダーまたはBSPライブラリを使用する場合です。

Disassembly of section .text:

00000000 <_start>:
   0:    e3a0d902     mov    sp, #32768    ; 0x8000
   4:    eb000010     bl    4c <initModule>
   8:    eafffffe     b    8 <_start+0x8>

0000000c <dummy>:
   c:    e12fff1e     bx    lr

00000010 <setCLK>:
  10:    e92d4010     push    {r4, lr}
  14:    e3a00005     mov    r0, #5
  18:    ebfffffb     bl    c <dummy>
  1c:    e8bd4010     pop    {r4, lr}
  20:    e12fff1e     bx    lr

00000024 <setConfig>:
  24:    e92d4010     push    {r4, lr}
  28:    e3a00006     mov    r0, #6
  2c:    ebfffff6     bl    c <dummy>
  30:    e8bd4010     pop    {r4, lr}
  34:    e12fff1e     bx    lr

00000038 <setSomethingElse>:
  38:    e92d4010     push    {r4, lr}
  3c:    e3a00007     mov    r0, #7
  40:    ebfffff1     bl    c <dummy>
  44:    e8bd4010     pop    {r4, lr}
  48:    e12fff1e     bx    lr

0000004c <initModule>:
  4c:    e92d4010     push    {r4, lr}
  50:    ebffffee     bl    10 <setCLK>
  54:    ebfffff2     bl    24 <setConfig>
  58:    ebfffff6     bl    38 <setSomethingElse>
  5c:    e8bd4010     pop    {r4, lr}
  60:    e12fff1e     bx    lr

あなたは間違いなくオーバーヘッドを追加し始めますが、これはパフォーマンスとスペースに顕著なコストがかかります。各機能の小ささに応じて、それぞれ数パーセントから5パーセント。

とにかくこれが行われるのはなぜですか?その一部は、教授がコードのグレーディングを容易にするために教えているルールのセットです。関数はページに収まらなければなりません(紙に印刷したとき)マイクロコントローラのファミリが数十ある場合、その一部は周辺機器を共有し、一部は共有しません。ファミリ間で異なる3つまたは4つの異なるUARTフレーバー、異なるGPIO、SPIコントローラなどがあります。汎用のgpio_init()関数を使用できます。 get_timer_count()など。また、これらの抽象化をさまざまな周辺機器に再利用します。

これは、主に保守性とソフトウェア設計のケースになり、ある程度の可読性が得られます。保守性、読みやすさ、パフォーマンスをすべて備えているわけではありません。3つすべてではなく、一度に1つまたは2つしか選択できません。

これは非常に意見に基づいた質問であり、上記はこれができる3つの主要な方法を示しています。どのパスがベストであるかについては、厳密に意見です。すべての作業を1つの機能で実行していますか?意見に基づく質問で、パフォーマンスに傾く人もいれば、モジュール性と読みやすさのバージョンをBESTと定義する人もいます。多くの人が読みやすさと呼ぶ興味深い問題は非常に苦痛です。コードを「見る」には、一度に50〜10,000個のファイルを開いて、何が起こっているのかを確認するために、実行順序で関数を線形に表示する必要があります。私は読みやすさの反対を見つけましたが、他の人は各アイテムがスクリーン/エディターウィンドウに収まり、呼び出されている関数を記憶した後、および/またはポップアウトできるエディターを持っていると全体的に消費できるので読みやすくなりますプロジェクト内の各機能。

さまざまなソリューションを見るとき、それは別の大きな要因です。テキストエディタ、IDEなどは非常にパーソナルであり、vi対Emacsを超えています。プログラミングの効率性は、使用しているツールに慣れていて効率的であれば、1日/月あたりの行数が増えます。ツールの機能は、そのツールのファンがコードを記述する方法に意図的に依存する場合としない場合があります。その結果、1人がこれらのライブラリを作成している場合、プロジェクトはある程度これらの習慣を反映しています。チームであっても、開発主任または上司の習慣/好みがチームの残りの部分に強制さ​​れる場合があります。

多くの個人的な好みが埋め込まれたコーディング標準、非常に宗教的なvi対Emacs、タブ対スペース、ブラケットの並び方など。これらは、ライブラリがある程度設計される方法に影響します。

あなたはあなたのものをどのように書くべきですか?あなたが望むが、それが機能する場合、実際に間違った答えはありません。確かに悪いコードまたは危険なコードがありますが、必要に応じて保守できるように記述されている場合、設計目標を達成し、パフォーマンスが重要な場合は読みやすさと保守性をあきらめます。コードの1行がエディターウィンドウの幅に収まるように短い変数名が好きですか?または、混乱を避けるために長すぎる説明的な名前を使用しますが、ページに1行表示できないため、読みやすさが低下します。今では、フローをいじって視覚的に分割されています。

打席で初めてホームランを打つつもりはありません。スタイルを完全に定義するには数十年かかる場合があります。同時に、その間にあなたのスタイルは変化し、しばらく一方に傾いてから、もう一方に傾くかもしれません。

最適化しない、最適化しない、時期尚早な最適化が多く聞かれます。しかし、示されているように、最初からこのような設計はパフォーマンスの問題を引き起こし、その後、実行するために最初から再設計するのではなく、その問題を解決するためのハックを見始めます。コンパイラーが何をするのかという恐怖に基づいてコンパイラーを操作しようとすることができる単一行のコードの数行の状況があることに同意します(経験を積むと、この種のコーディングが簡単で自然になり、コンパイラーがコードをどのようにコンパイルするのかを知っているときに最適化を行います)、サイクルスティーラーが実際にどこにあるのかを確認してから攻撃します。

また、ユーザー向けのコードをある程度設計する必要があります。これがあなたのプロジェクトである場合、あなたは唯一の開発者です。あなたが望むものは何でも。ライブラリを配布または販売しようとする場合、コードを他のすべてのライブラリ、小さな関数、長い関数名、長い変数名を持つ数百から数千のファイルのように見せたいと思うでしょう。可読性の問題とパフォーマンスの問題にもかかわらず、IMOを使用すると、より多くの人がそのコードを使用できるようになります。


4
本当に?どのような「ターゲット」と「コンパイラ」を使用しますか?
ドリアン

それは32/64ビットARM8のように見えます。おそらく、ラズベリーPIから、そして通常のマイクロコントローラーからでしょう。質問の最初の文を読みましたか?
ドリアン

コンパイラは未使用のグローバル関数を削除しませんが、リンカーは削除します。適切に構成および使用されている場合、実行可能ファイルには表示されません。
berendi

ファイルギャップ全体で最適化できるコンパイラを疑問に思っている場合:IARコンパイラはマルチファイルコンパイル(呼び出し方法)をサポートしており、ファイル間の最適化が可能です。すべてのc / cppファイルを一度に投げると、単一の関数mainを含む実行可能ファイルになります。パフォーマンスの利点は非常に大きい場合があります。
アーセナル

3
@Arsenalもちろん、gccは、適切に呼び出された場合、コンパイル単位間でもインライン化をサポートします。gcc.gnu.org/onlinedocs/gcc/Optimize-Options.htmlを参照し、-fltoオプションを探してください。
ピーター-モニカの復活

1

非常に一般的なルール-コンパイラはあなたよりも最適化できます。もちろん、非常にループ集約的なことをしている場合は例外がありますが、全体的に速度またはコードサイズの最適化が必要な場合は、コンパイラを賢明に選択してください。


悲しいことに、今日のほとんどのプログラマーに当てはまります。
ドリアン

0

それは確かにあなた自身のコーディングスタイルに依存します。世に出ている一般的なルールの1つは、変数名と関数名はできるだけ明確で、わかりやすいものにする必要があるということです。関数にサブコールまたはコード行を追加するほど、その1つの関数の明確なタスクを定義することが難しくなります。あなたの例では、ものinitModule()初期化してサブルーチンを呼び出す関数があり、それがクロックを設定する、設定を設定します。関数の名前を読むだけでそれを知ることができます。サブルーチンのすべてのコードをinitModule()直接配置すると、関数が実際に何をするのかわかりにくくなります。しかし、多くの場合、これは単なるガイドラインです。


お返事ありがとうございます。パフォーマンスに必要な場合はスタイルを変更するかもしれませんが、ここでの質問はコードの可読性がパフォーマンスに影響するかどうかです。
マニーヤック

関数呼び出しは呼び出しまたはjmpコマンドになりますが、それは私の意見ではリソースのごくわずかな犠牲です。設計パターンを使用すると、実際のコードに到達する前に、十数層の関数呼び出しが発生することがあります。
po.pe

@Humpawumpa -関数呼び出しのダース層は無視でき犠牲ではありません、あなたはRAMの唯一の256または64バイトで、マイクロコントローラのために書いている場合は、それだけではできません
uɐɪ

はい、しかし、これらは両極端です...通常、256バイト以上あり、十数個のレイヤーを使用しています-うまくいけば。
po.pe

0

関数が実際に非常に小さなことを1つだけ行う場合は、作成することを検討してくださいstatic inline

Cファイルの代わりにヘッダーファイルに追加し、単語static inlineを使用して定義します。

static inline void setCLK()
{
    //code to set the clock
}

ここで、3行を超えるなど、関数がさらにわずかに長い場合は、回避することをお勧めします static inlineて.cファイルに追加することをます。結局のところ、組み込みシステムのメモリは限られているため、コードサイズをあまり大きくしたくないのです。

また、で関数を定義しfile1.cてから使用する場合file2.c、コンパイラは自動的にインライン化しません。ただし、関数file1.hとして定義すると、static inlineコンパイラがインライン化する可能性があります。

これらのstatic inline関数は、高性能プログラミングに非常に役立ちます。コードパフォーマンスが3倍以上になることがよくあります。


「3行以上など」-行数はそれとは関係ありません。インラインコストは、それと関係があります。インライン化に最適な20行関数と、インライン化が恐ろしい3行関数(たとえば、functionB()を3回呼び出すfunctionA()、functionC()を3回呼び出すfunctionB()、他のいくつかのレベル)。
ジェイソンS

また、で関数を定義しfile1.cてから使用する場合file2.c、コンパイラは自動的にインライン化しません。 -fltogccまたはclangなどを参照してください。
berendi

0

マイクロコントローラ用の効率的で信頼性の高いコードを記述しようとすると、コンパイラ固有のディレクティブを使用したり、多くの最適化を無効にしたりしない限り、特定のセマンティクスを確実に処理できないコンパイラがあります。

たとえば、割り込みサービスルーチン[タイマーティックなどによって実行される]を備えたシングルコアシステムがある場合:

volatile uint32_t *magic_write_ptr,magic_write_count;
void handle_interrupt(void)
{
  if (magic_write_count)
  {
    magic_write_count--;
    send_data(*magic_write_ptr++)
  }
}

バックグラウンドの書き込み操作を開始する関数を記述するか、完了するまで待機する必要があります。

void wait_for_background_write(void)
{
  while(magic_write_count)
    ;
}
void start_background_write(uint32_t *dat, uint32_t count)
{
  wait_for_background_write();
  background_write_ptr = dat;
  background_write_count = count;
}

そして、次を使用してそのようなコードを呼び出します:

uint32_t buff[16];

... write first set of data into buff
start_background_write(buff, 16);
... do some stuff unrelated to buff
wait_for_background_write();

... write second set of data into buff
start_background_write(buff, 16);
... etc.

残念ながら、完全な最適化を有効にすると、gccやclangなどの「賢い」コンパイラーは、最初の書き込みセットがプログラムのオブザーバブルに影響を与えることはできないため、それらを最適化することができます。icc割り込みを設定して完了を待機する動作が揮発性の書き込みと揮発性の読み取りの両方を含む場合(この場合のように)、このような品質のコンパイラはこれを行う傾向がありませんが、iccは組み込みシステムではあまり人気がありません。

この規格は実装品質の問題を意図的に無視し、上記の構成を処理できる合理的な方法がいくつかあると考えています。

  1. 質の高い実装 ハイエンドの数値計算などのフィールド専用、そのようなフィールド用に記述されたコードに上記のような構造が含まれないことが合理的に期待できます。

  2. 高品質の実装では、 volatileオブジェクトを、外界に見えるオブジェクトにアクセスするアクションをトリガーするかのようにことができます。

  3. 組み込みシステムでの使用を目的としたシンプルだが適切な品質の実装は、「インライン」とマークされていない関数へのすべての呼び出しを、外界に公開されているオブジェクトにアクセスする可能性があるものvolatileとして扱います。 2。

標準は、上記のアプローチのどれが品質の実装に最も適切であるかを示唆することも、「適合」実装が特定の目的に使用できるほど十分に良い品質であることを要求することも試みません。そのため、gccやclangなどの一部のコンパイラでは、このパターンを使用するコードは、多くの最適化を無効にしてコンパイルする必要があります。

場合によっては、I / O関数が別のコンパイルユニットにあり、コンパイラが選択肢を持たないことを確認する以外に、外部の世界にさらされたオブジェクトの任意のサブセットにアクセスする可能性があると仮定することは、合理的な最小のgccおよびclangで確実に動作するコードを記述するエビルの方法。ただし、そのような場合の目的は、不必要な関数呼び出しの余分なコストを回避することではなく、必要なセマンティクスを取得する代わりに不必要なコストを受け入れることです。


「I / O関数が別のコンパイルユニットにあることを確認する」...これらのような最適化の問題を防ぐ確実な方法ではありません。少なくともLLVMと私は、GCCがプログラム全体の最適化を多くの場合実行すると考えているため、IO関数が別個のコンパイル単位にある場合でも、IO関数をインライン化することを決定できます。
ジュール

@Jules:すべての実装が組み込みソフトウェアの作成に適しているわけではありません。プログラム全体の最適化を無効にすることは、gccまたはclangをその目的に適した高品質の実装として強制する最も安価な方法です。
-supercat

@Jules:Aより高品質な実装はプログラミング組み込みまたはシステムのために意図を完全に無効プログラム全体の最適化をすることなく、その目的に適している意味を持つように設定可能でなければなりません(例えば治療するためのオプションを持つことではvolatile潜在的にトリガ、彼らがかもしれないかのようにアクセス他のオブジェクトへの任意のアクセス)、しかし、何らかの理由でgccとclangは実装品質の問題を役に立たない振る舞いへの招待として扱います。
-supercat

1
「最高品質」の実装でさえ、バグのあるコードを修正しません。場合はbuff宣言されていないvolatile、それは揮発性の変数として扱われることはありません明らかに、後に使用されていない場合は、それへのアクセスは完全にアウト並べ替えまたは最適化することができます。ルールは簡単です。通常のプログラムフロー(コンパイラから見た)の外部でアクセスされる可能性のあるすべての変数をとしてマークしますvolatile。の内容はbuff割り込みハンドラーでアクセスされていますか?はい。それからする必要がありますvolatile
berendi

@berendi:コンパイラは、規格が要求する以上の保証を提供でき、品質の高いコンパイラはそれを提供します。組み込みシステムで使用するための高品質な独立型の実装により、プログラマはミューテックス構造を合成できます。これは基本的にコードが行うことです。ときはmagic_write_countゼロで、ストレージはメインラインが所有しています。ゼロ以外の場合、割り込みハンドラーが所有します。作るbuff揮発することが必要になり、それが使用して動作するすべての関数のどこかvolatileはるかにコンパイラを持つよりも、最適化を損なう-qualifiedポインタを、...
supercat
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.