Arduinoではmalloc()とfree()の使用は本当に悪い考えですか?


49

Arduinoの世界では、malloc()との使用はfree()非常にまれです。純粋なAVR Cではるかに頻繁に使用されますが、それでも注意が必要です。

それは、使用には本当に悪い考えであるmalloc()free()のArduinoと?


2
あなたはそれ以外の場合は本当に速いメモリが不足します、そして、あなたが使用しますどのくらいのメモリを知っていればあなたにも静的にとにかくそれを割り当てるかもしれない
ラチェットフリーク

1
それが悪いかどうかはわかりませんが、ほとんどのスケッチでRAMを使い果たすことはほとんどなく、フラッシュと貴重なクロックサイクルの無駄であるため、使用されていないと思います。また、スコープについても忘れないでください(ただし、そのスペースがすべての変数に割り当てられているかどうかはわかりません)。
匿名のペンギン14年

4
いつものように、正しい答えは「依存します」です。ダイナミックアロケーションが適切かどうかを確認するのに十分な情報を提供していません。
WineSoaked 14年

回答:


40

組み込みシステムの私の一般的なルールmalloc()は、プログラムの開始時に大きなバッファーのみを一度だけにすることです(例:)setup()。問題は、メモリの割り当てと割り当て解除時に発生します。長時間の実行セッションでは、空きメモリの合計がリクエストに十分すぎる場合でも、メモリが断片化し、最終的に十分な空き領域がないために割り当てが失敗します。

(歴史的な観点、興味がない場合はスキップ):ローダーの実装に応じて、ランタイム割り当てとコンパイル時割り当て(初期化されたグローバル)の唯一の利点は、hexファイルのサイズです。すべての揮発性メモリを備えた市販のコンピューターで組み込みシステムを構築した場合、プログラムはしばしばネットワークまたは計装コンピューターから組み込みシステムにアップロードされ、アップロード時間が問題になることがありました。画像からゼロでいっぱいのバッファを除外すると、時間を大幅に短縮できます。)

組み込みシステムで動的なメモリ割り当てが必要な場合、一般的にmalloc()、またはできれば静的に大きなプールを割り当て、それを固定サイズのバッファー(またはそれぞれ小さなバッファーと大きなバッファーのそれぞれ1つのプール)に分割し、独自の割り当て/そのプールからの割り当て解除。次に、固定バッファサイズまでの任意の量のメモリを要求するたびに、それらのバッファのいずれかが使用されます。呼び出し側の関数は、要求よりも大きいかどうかを知る必要はありません。また、ブロックの分割と再結合を回避することにより、断片化を解決します。もちろん、プログラムに割り当て/割り当て解除のバグがある場合は、メモリリークが発生する可能性があります。


もう1つの歴史的な注意点として、これはすぐにBSSセグメントにつながり、プログラムのロード中にゼロをゆっくりコピーすることなく、プログラムが初期化のために自身のメモリをゼロにすることができました。
rsaxvc

16

通常、Arduinoスケッチを作成するときは、動的割り当て(C ++インスタンスを使用する場合、mallocまたはnewC ++インスタンスを使用する場合)を避け、グローバルstatic変数またはグローバル変数、またはローカル(スタック)変数を使用します。

ダイナミックアロケーションを使用すると、いくつかの問題が発生する可能性があります。

  • メモリリーク(以前に割り当てたメモリへのポインタを紛失した場合、または不要になったときに割り当てられたメモリを解放することを忘れた場合に発生する可能性が高い)
  • ヒープが現在割り当てられている実際のメモリ量よりも大きくなるヒープフラグメンテーション(数回malloc/ free呼び出し後)

私が直面したほとんどの状況では、動的割り当ては不要であるか、次のサンプルコードのようにマクロで回避できます。

MySketch.ino

#define BUFFER_SIZE 32
#include "Dummy.h"

Dummy.h

class Dummy
{
    byte buffer[BUFFER_SIZE];
    ...
};

を使用せずに#define BUFFER_SIZEDummyクラスのbufferサイズを固定しない場合は、次のように動的割り当てを使用する必要があります。

class Dummy
{
    const byte* buffer;

    public:
    Dummy(int size):buffer(new byte[size])
    {
    }

    ~Dummy()
    {
        delete [] bufer;
    }
};

この場合、最初のサンプルよりも多くのオプションがあります(たとえば、それぞれにDummy異なるbufferサイズの異なるオブジェクトを使用します)が、ヒープの断片化の問題がある可能性があります。

インスタンスが削除bufferされると、動的に割り当てられたメモリが確実に解放されるように、デストラクタを使用することに注意してくださいDummy


14

で使用されているアルゴリズムmalloc()をavr-libcから調べましたが、ヒープの断片化の観点から安全な使用パターンがいくつかあるようです。

1.存続期間の長いバッファのみを割り当てる

つまり、プログラムの最初に必要なものをすべて割り当て、決して解放しないでください。もちろん、この場合、静的バッファも使用できます...

2.短命のバッファーのみを割り当てる

意味:他のものを割り当てる前にバッファを解放します。合理的な例は次のようになります。

void foo()
{
    size_t size = figure_out_needs();
    char * buffer = malloc(size);
    if (!buffer) fail();
    do_whatever_with(buffer);
    free(buffer);
}

内部do_whatever_with()にmallocがない場合、またはその関数が割り当てたものをすべて解放する場合、断片化から安全です。

3.常に最後に割り当てられたバッファを解放します

これは、以前の2つのケースの一般化です。ヒープをスタックのように使用する場合(後入れ先出し)は、フラグメントではなくスタックのように動作します。この場合、最後に割り当てられたバッファのサイズを変更しても安全であることに注意してくださいrealloc()

4.常に同じサイズを割り当てます

これは断片化を防ぐことはできませんが、ヒープが最大使用サイズより大きくならないという意味で安全です。すべてのバッファのサイズが同じである場合、バッファの1つを解放するたびに、後続の割り当てにスロットが使用できることを確認できます。


1
パターン2は、「char buffer [size];」を使用してmalloc()およびfree()のサイクルを追加できるため、回避する必要があります。(C ++)。アンチパターン「Never from a ISR」も追加したいと思います。
ミカエルパテル

9

malloc/ freeまたはnew/ を介して)動的割り当てを使用することdeleteは、本質的に悪いことではありません。実際、文字列処理のようなもの(たとえば、Stringオブジェクトを介した)の場合、非常に役立ちます。これは、多くのスケッチが文字列のいくつかの小さな断片を使用しているためであり、それらは最終的には大きな断片に結合されます。動的割り当てを使用すると、それぞれに必要なだけのメモリを使用できます。対照的に、それぞれに固定サイズの静的バッファーを使用すると、コンテキストに完全に依存しますが、多くのスペースを浪費することになります(メモリーをはるかに速く使い果たすことになります)。

これらすべてが述べられているので、メモリ使用量が予測可能であることを確認することは非常に重要です。実行時の状況(入力など)に応じてスケッチが任意の量のメモリを使用できるようにすると、遅かれ早かれ問題が発生しやすくなります。場合によっては、完全に安全な場合があります。たとえば、使用量が多くならないことがわかっている場合です。ただし、プログラミングプロセス中にスケッチが変更される場合があります。何かが後で変更されたときに、早期に行われた仮定が忘れられ、予期しない問題が発生する可能性があります。

堅牢性を確保するため、通常は可能な限り固定サイズのバッファーを使用し、最初からこれらの制限を明示的に使用するようにスケッチを設計することをお勧めします。これは、スケッチに対する将来の変更、または予期しない実行時の状況が、メモリの問題を引き起こさないことを意味します。


6

私はあなたがそれを使うべきではないと思う人や、一般的には不要だと思う人に同意しません。あなたはそれの内と外を知らない場合、それは危険であると信じていますが、それは便利です。構造体またはバッファのサイズが(コンパイル時または実行時に)わからない(そして気にする必要がない)場合があります。特に、私が世界に送り出すライブラリの場合です。アプリケーションが既知の単一の構造のみを処理している場合は、コンパイル時にそのサイズでベークするだけでよいことに同意します。

例:任意の長さのデータペイロード(構造体、uint16_tの配列など)を受け取ることができるシリアルパケットクラス(ライブラリ)があります。そのクラスの送信側で、Packet.send()メソッドに、送信したいもののアドレスと、それを送信したいHardwareSerialポートを単に伝えるだけです。ただし、受信側では、受信ペイロードを保持するために動的に割り当てられた受信バッファーが必要です。そのペイロードは、たとえば、アプリケーションの状態に応じて、任意の時点で異なる構造になる可能性があるためです。単一の構造体を前後に送信するだけの場合は、コンパイル時に必要なサイズのバッファーを作成します。ただし、パケットの長さが時間とともに異なる場合、malloc()とfree()はそれほど悪くありません。

次のコードを使用してテストを数日間実行し、継続的にループさせましたが、メモリの断片化の証拠は見つかりませんでした。動的に割り当てられたメモリを解放すると、空き量は以前の値に戻ります。

// found at learn.adafruit.com/memories-of-an-arduino/measuring-free-memory
int freeRam () {
    extern int __heap_start, *__brkval;
    int v;
    return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
}

uint8_t *_tester;

while(1) {
    uint8_t len = random(1, 1000);
    Serial.println("-------------------------------------");
    Serial.println("len is " + String(len, DEC));
    Serial.println("RAM: " + String(freeRam(), DEC));
    Serial.println("_tester = " + String((uint16_t)_tester, DEC));
    Serial.println("alloating _tester memory");
    _tester = (uint8_t *)malloc(len);
    Serial.println("RAM: " + String(freeRam(), DEC));
    Serial.println("_tester = " + String((uint16_t)_tester, DEC));
    Serial.println("Filling _tester");
    for (uint8_t i = 0; i < len; i++) {
        _tester[i] = 255;
    }
    Serial.println("RAM: " + String(freeRam(), DEC));
    Serial.println("freeing _tester memory");
    free(_tester); _tester = NULL;
    Serial.println("RAM: " + String(freeRam(), DEC));
    Serial.println("_tester = " + String((uint16_t)_tester, DEC));
    delay(1000); // quick look
}

RAMの劣化や、この方法を使用して動的に割り当てる能力の低下は見られなかったため、実行可能なツールだと思います。FWIW。


2
テストコードは使用パターン2に準拠しています。前の回答で説明した短命のバッファーのみを割り当てます。これは、安全であることが知られている数少ない使用パターンの1つです。
エドガーボネット

言い換えると、プロセッサを他の未知のコードと共有し始めると問題が発生します。これはまさにあなたが避けようとしている問題です。一般に、常に機能するか、リンク中に失敗する何かが必要な場合は、最大サイズの固定割り当てを行い、たとえば初期化でユーザーに渡すことで何度も使用します。通常、すべてが2048バイトに収まらなければならないチップで実行していることを覚えておいてください-一部のボードではもっと多いかもしれませんが、他のボードではずっと少ないかもしれません。
クリスストラットン

@EdgarBonetはい、正確に。ただ共有したかった。
-StuffAndyMakes

1
必要なサイズだけのバッファを動的に割り当てることは危険です。解放する前に何か他のものが割り当てられた場合、フラグメンテーション(再利用できないメモリ)を残すことができます。また、動的割り当てには追跡オーバーヘッドがあります。固定割り当てとは、メモリを複数回使用できないことを意味するのではなく、プログラムの設計に共有を加えなければならないことを意味します。純粋にローカルなスコープを持つバッファーの場合、スタックの使用を比較検討することもできます。malloc()が失敗する可能性についてもチェックしていません。
クリスストラットン

1
「あなたはそれの内と外を知らなければ危険になる可能性がありますが、それは便利です。」C / C ++でのすべての開発をほぼまとめています。:-)
ThatAintWorking

4

Arduinoでmalloc()とfree()を使用するのは本当に悪い考えですか?

簡単な答えはイエスです。理由は次のとおりです。

MPUとは何か、利用可能なリソースの制約内でプログラムする方法を理解することがすべてです。Arduino Unoは、32KB ISPフラッシュメモリ、1024B EEPROM、および2KB SRAMを備えたATmega328p MPUを使用します。それは多くのメモリリソースではありません。

2KB SRAMは、すべてのグローバル変数、文字列リテラル、スタック、およびヒープの使用可能性に使用されることに注意してください。スタックには、ISR用のヘッドルームも必要です。

メモリレイアウトは次のとおりです。

SRAMマップ

今日のPC /ラップトップのメモリ量は1.000.000倍以上です。スレッドごとの1Mバイトのデフォルトスタックスペースは珍しくありませんが、MPUでは完全に非現実的です。

組み込みソフトウェアプロジェクトでは、リソースの予算が必要です。これは、ISRレイテンシ、必要なメモリスペース、計算能力、命令サイクルなどを推定しています。残念ながら、無料のランチはなく、ハードリアルタイムの組み込みプログラミングは、プログラミングスキルの中で最も習得が困難です。


アーメン:「[H] ardリアルタイム組み込みプログラミングは、習得するのが最も難しいプログラミングスキルです。」
StuffAndyMakes

mallocの実行時間は常に同じですか?使用可能なRAM内で適合するスロットをさらに検索するため、mallocにより時間がかかると想像できますか?これは、外出先でメモリを割り当てないというもう1つの引数です(RAMを使い果たすことは別として)。
ポール

@Paulヒープアルゴリズム(mallocおよびfree)は通常、一定の実行時間ではなく、リエントラントではありません。このアルゴリズムには、スレッドを使用するときにロックを必要とする検索およびデータ構造が含まれます(同時実行)。
ミカエルパテル

0

わかりました、これは古い質問であることは知っていますが、答えをよく読めば読むほど、顕著に見える観察に戻ってきます。

停止の問題は現実です

ここにチューリングの停止問題とのリンクがあるようです。動的割り当てを許可すると、「停止」の確率が高くなるため、質問はリスク許容度の1つになります。malloc()失敗などの可能性を振り切るのは便利ですが、それでも有効な結果です。OPが尋ねる質問は、テクニックに関するものであるように見えます。はい、使用されるライブラリの詳細または特定のMPUが重要です。会話は、プログラムが停止したり、その他の異常終了したりするリスクを減らすことになります。私たちは、リスクを大きく異なって許容する環境の存在を認識する必要があります。LEDストリップにきれいな色を表示する私の趣味のプロジェクトは、何か異常なことが起こっても誰かを殺すことはありませんが、心肺マシン内のMCUはそうするでしょう。

こんにちはMr. Turing私の名前はHubrisです

LEDストリップでは、ロックするかどうかは気にしません。リセットするだけです。私はそれの結果がロックアップまたは動作に失敗MCUによって制御される人工心肺装置にあった場合は、文字通り生死あるのでに関する質問malloc()free()氏を発揮する可能性がどのように意図したプログラムのお得な情報間の分割をする必要がありますチューリングの有名な問題。それが数学的証明であることを忘れがちであり、十分に賢い場合にのみ計算の限界の犠牲者を避けることができると確信することは簡単です。

この質問には2つの回答が必要です。1つは顔の停止問題を凝視する際に瞬きを強いられる人向け、もう1つは他のすべての人向けです。arduinoのほとんどの用途は、ミッションクリティカルなアプリケーションや生死にかかわるアプリケーションではない可能性がありますが、コーディングするMPUに関係なく、その区別は依然として存在します。


ヒープの使用が必ずしもarbitrary意的ではないという事実を考慮すると、この特定の状況ではホールティングの問題は当てはまらないと思います。明確に定義された方法で使用される場合、ヒープの使用は予想どおり「安全」になります。Halting問題のポイントは、必然的に任意の、それほど明確に定義されていないアルゴリズムに何が起こるかを決定できるかどうかを把握することでした。より広い意味でのプログラミングに本当に当てはまりますので、特にここではあまり意味がないと思います。完全に正直であるとはまったく関係ないと思います。
ジョナサングレイ

私はいくつかの修辞的な誇張を認めますが、ポイントは実際に動作を保証したい場合です。ヒープを使用すると、スタックのみを使用するよりもはるかに高いレベルのリスクが含まれます。
ケリーS.フランス

-3

いいえ。ただし、割り当てられたメモリのfree()に関しては、慎重に使用する必要があります。一般にソフトウェア開発と互換性のないレベルの無能さを意味するため、人々が直接メモリ管理を避けるべきだと言う理由を私は理解したことがありません。

arduinoを使用してドローンを制御するとしましょう。コードのあらゆる部分にエラーがあると、コードが空から落ちて誰かまたは何かを傷つける可能性があります。言い換えれば、誰かがmallocを使用する能力を欠いている場合、小さなバグが深刻な問題を引き起こす可能性のある他の領域が非常に多いため、コーディングするべきではありません。

mallocによって引き起こされるバグを追跡して修正するのは難しいですか?はい、しかしそれはリスクというよりもコーダー側のフラストレーションの問題です。リスクに関する限り、コードが正しく実行されていることを確認するための手順を実行しないと、コードのどの部分もmallocと同等またはそれ以上にリスクが高くなります。


4
例としてドローンを使用したのは興味深いことです。この記事(mil-embedded.com/articles/…)によると、「危険性のため、DO-178B標準では、安全に不可欠な組み込みアビオニクスコードでは、動的メモリ割り当ては禁止されています。」
ガブリエルステープルズ

DARPAには、請負業者が独自のプラットフォームに合った仕様を開発できるようにする長い歴史があります。請求書を支払うのは納税者であるのに、なぜそうすべきではないのですか。これが、他の人が10,000ドルでできることを開発するのに100億ドルかかる理由です。まるで軍事産業団地を参考にしているかのように聞こえます。
JSON

動的割り当ては、プログラムが停止問題で説明されている計算の限界を実証するための招待のようです。このような停止のわずかなリスクを処理できる環境がいくつかあり、制御可能なリスクを一切許容しない環境(宇宙、防衛、医療など)が存在するため、「してはならない」操作を禁止します。失敗するのは、ロケットを発射したり、心臓/肺の機械を制御しているときは、「機能するはずです」だけでは不十分だからです。
ケリーS.フランス語
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.