在Arduino上使用malloc()和free()真的不是一个好主意吗?


49

采用malloc()free()似乎在Arduino的世界非常罕见的。纯AVR C中使用它的频率更高,但仍要谨慎。

它是一个非常糟糕的主意,使用malloc()free()与Arduino的?


2
否则,您的内存将很快用完,否则,如果您知道要使用多少内存,则还是可以静态分配它
棘手的怪胎

1
我不知道它是否不好,但是我认为它没有被使用,因为对于大多数草图,您几乎永远不会用完RAM,这只是浪费闪存和宝贵的时钟周期。另外,不要忘记范围(尽管我不知道该空间是否仍分配给所有变量)。
匿名企鹅2014年

4
通常,正确的答案是“取决于情况”。您尚未提供足够的信息来确定动态分配是否适合您。
WineSoaked 2014年

Answers:


40

对于嵌入式系统,我的一般规则是malloc()在程序开始时(例如,在中)仅对大型缓冲区进行一次缓冲setup()。当您分配和取消分配内存时,问题就来了。在长期运行的会话中,内存会变得碎片化,最终由于缺少足够大的可用区域而导致分配失败,即使总的可用内存足以满足请求。

(从历史的角度来看,如果不感兴趣,请跳过):根据加载程序的实现,运行时分配与编译时分配(初始化的全局变量)的唯一优势是十六进制文件的大小。当使用具有所有易失性存储器的现成计算机构建嵌入式系统时,该程序通常是从网络或仪器计算机上载到嵌入式系统的,有时上载时间是个问题。从图像中遗漏零缓冲区可能会大大缩短时间。)

如果我需要在嵌入式系统中进行动态内存分配,则通常malloc()(或最好是静态地)分配一个大池,然后将其划分为固定大小的缓冲区(或分别为小缓冲区和大缓冲区的一个池),然后自行分配/从该池中取消分配。然后,对那些不超过固定缓冲区大小的任何内存量的请求都将使用这些缓冲区之一进行响应。调用函数不需要知道它是否大于请求,并且通过避免拆分和重新组合块,我们可以解决碎片问题。当然,如果程序具有分配/取消分配错误,则仍然可能发生内存泄漏。


另一个历史记录表明,这很快导致了BSS段,该段允许程序将其自己的内存清零以进行初始化,而无需在程序加载期间缓慢复制零。
rsaxvc '16

16

通常,在编写Arduino草图时,您将避免动态分配(对于C ++实例,mallocnew用于C ++实例),人们宁可使用全局-或static-变量或局部(堆栈)变量。

使用动态分配会导致几个问题:

  • 内存泄漏(如果丢失了指向先前分配的内存的指针,或者更可能的是如果您不再需要释放已分配的内存,则很有可能)
  • 堆碎片(几次malloc/ free调用之后),其中堆增长大于当前分配的实际内存量

在大多数情况下,我不需要动态分配,或者可以使用宏避免动态分配,如以下代码示例所示:

MySketch.ino

#define BUFFER_SIZE 32
#include "Dummy.h"

虚拟的

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

没有#define BUFFER_SIZE,如果我们想让Dummy类具有不固定的buffer大小,则必须使用动态分配,如下所示:

class Dummy
{
    const byte* buffer;

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

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

在这种情况下,我们有比第一个示例更多的选项(例如,使用每个Dummy对象具有不同buffer大小的不同对象),但是我们可能会遇到堆碎片问题。

请注意,使用析构函数可确保bufferDummy删除实例时释放为其动态分配的内存。


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);
}

如果内部没有malloc do_whatever_with(),或者该函数释放了它分配的所有内容,那么您可以避免碎片。

3.总是释放最后分配的缓冲区

这是前两种情况的概括。如果您像堆一样使用堆(后进先出),那么它将表现得像堆,而不是碎片。应该注意的是,在这种情况下,可以安全地调整最后分配的缓冲区的大小realloc()

4.始终分配相同的大小

这不会阻止碎片,但是从某种意义上讲,堆不会增长到大于最大已大小是安全的。如果所有缓冲区的大小都相同,则可以确定,只要释放其中一个缓冲区,该插槽就可用于后续分配。


1
应避免使用模式2,因为它可以通过“ char buffer [size];”来完成,从而增加了malloc()和free()的周期。(在C ++中)。我还想添加反模式“从ISR永不”。
Mikael Patel '02

9

因此,使用动态分配(通过malloc/ freenew/ delete)并不是天生的坏事。实际上,对于诸如字符串处理之类的事情(例如通过String对象),它通常非常有用。这是因为许多草图使用了几个小的字符串片段,最终将它们组合成一个更大的字符串。使用动态分配可使您仅使用每个内存所需的内存。相反,为每个缓冲区使用固定大小的静态缓冲区最终可能会浪费大量空间(导致其更快地耗尽内存),尽管这完全取决于上下文。

综上所述,确保内存使用量可预测非常重要。根据运行时环境(例如输入)允许草图使用任意数量的内存,迟早会引起问题。在某些情况下,这可能是完全安全的,例如,如果您知道使用量绝不会相加。草图可以在编程过程中更改。稍后更改某些内容时,可能会忘记早期做出的假设,从而导致无法预料的问题。

为了提高鲁棒性,通常最好在可能的情况下使用固定大小的缓冲区,并设计草图以从一开始就明确地处理那些限制。这意味着以后对草图的任何更改,或任何意外的运行时环境,都希望不会引起任何内存问题。


6

我不同意那些认为您不应该使用它或通常不必要的人的观点。我相信,如果您不了解它的来龙去脉,可能会很危险,但这很有用。在某些情况下,我不知道(也不应该知道)结构或缓冲区的大小(在编译时或运行时),尤其是涉及到我向世界发送的库时。我同意,如果您的应用程序仅处理单个已知结构,则应在编译时烘烤该大小。

示例:我有一个串行数据包类(一个库),可以接收任意长度的数据有效载荷(可以是struct,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。仅分配我在上一个答案中描述的短期缓冲区。这是少数几种安全的使用模式之一。
埃德加·博内

换句话说,当您开始与其他未知代码共享处理器时,就会出现问题-这正是您认为要避免的问题。通常,如果您希望某些东西在链接过程中始终可以工作或失败,则可以对最大大小进行固定分配,并一遍又一遍地使用它,例如,让用户在初始化时将其传递给您。请记住,您通常在芯片上运行,所有内容都必须容纳2048字节-在某些板上可能更多,而在其他板上可能更少。
克里斯·斯特拉顿

@EdgarBonet是的,完全是。只是想分享。
StuffAndy

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倍。每个线程1 MB的默认堆栈空间并不罕见,但在MPU上完全不现实。

嵌入式软件项目必须进行资源预算。这是在估计ISR延迟,必要的内存空间,计算能力,指令周期等。不幸的是,没有自由编程,而硬实时嵌入式编程是最难掌握的编程技能。


对此表示支持:“ [H] ard实时嵌入式编程是最难掌握的编程技能。”
StuffAndyMakes

malloc的执行时间是否始终相同?我可以想象malloc会花费更多的时间,因为它会在可用的ram中进一步搜索适合的插槽?这是另一个说法(除了用完了ram之外),不要随时随地分配内存吗?
保罗

@Paul堆算法(malloc和free)通常不是恒定的执行时间,也不是可重入的。该算法包含使用线程(并发)时需要锁定的搜索和数据结构。
Mikael Patel

0

好的,我知道这是一个古老的问题,但是我越仔细地阅读答案,就越能回头再看一次似乎很重要的观察结果。

暂停问题是真实的

这里似乎与图灵的暂停问题有关。允许动态分配会增加上述“暂停”的可能性,因此该问题成为风险承受能力之一。尽管消除malloc()失败的可能性等很方便,但这仍然是有效的结果。OP提出的问题仅与技术有关,是的,所使用的库的详细信息或特定的MPU确实很重要。对话将转向降低程序暂停或任何其他异常结束的风险。我们需要认识到容忍风险的环境大不相同。我的爱好项目是在LED灯条上显示漂亮的颜色,即使发生异常情况也不会杀死某人,但是心肺机内部的MCU可能会杀死某人。

你好图灵先生

对于我的LED灯带,我不在乎它是否锁定,我将其重置。如果我是通过MCU它的后果锁住或不操作控制的心脏心肺机上字面上生死,所以这个问题有关malloc(),并free()应该如何预定程序交易之间划分了展示先生的可能性图灵的著名问题。容易忘记这是一个数学证明,并且使自己相信,只要我们足够聪明,我们就可以避免成为计算极限的牺牲品。

这个问题应该有两个可以接受的答案,一个是针对那些盯着脸上的“停止问题”而被迫眨眼的人,另一个是针对所有其他人的。尽管arduino的大多数用途可能不是关键任务应用或生死攸关的应用程序,但无论您要编码哪个MPU,都仍然存在区别。


考虑到堆使用不一定是任意的这一事实,我认为停止问题不适用于这种特定情况。如果以明确定义的方式使用,则堆使用情况可以预料为“安全”。指出了停止问题的要点,即是否可以确定某个必然的,定义不太明确的算法会发生什么。从更广泛的意义上讲,它确实更适用于编程,因此我发现它在这里并不是特别相关。老实说,我什至认为这根本不重要。
乔纳森·格雷

我承认有些夸张的说法,但要点实际上是如果您要保证行为,使用堆意味着比只坚持使用堆栈要高得多的风险级别。
凯利·S·法文

-3

不,但是在释放分配的内存方面,必须非常小心地使用它们。我从来不明白为什么人们会说应该避免直接内存管理,因为这暗示了通常与软件开发不兼容的无能水平。

假设您使用arduino控制无人机。代码任何部分中的任何错误都可能导致其掉出天空并伤害某人或某物。换句话说,如果某人缺乏使用malloc的能力,那么他们很可能根本不应该编写代码,因为在很多其他领域中,小错误可能会引起严重的问题。

由malloc引起的错误难于跟踪和修复吗?是的,但这更多的是让编码人员感到沮丧而不是冒险。就风险而言,如果您不采取措施确保正确执行,则代码的任何部分都可能比malloc具有同等或更多的风险。


4
您以无人驾驶飞机为例很有趣。根据这篇文章(mil-embedded.com/articles/…),“由于存在风险,因此在DO-178B标准下,在安全关键型嵌入式航空电子代码中禁止动态内存分配。”
加布里埃尔·斯台普斯2015年

DARPA有悠久的历史,允许承包商开发适合自己平台的规范-为什么当纳税人付款时,他们为什么不这样做呢?这就是为什么他们需要花100亿美元来开发别人可以用10,000美元完成的事情。听起来好像您是在使用军事工业园区作为诚实的参考。
JSON

动态分配似乎邀请您的程序演示暂停问题中描述的计算限制。在某些环境中,可以处理这种停止的风险很小,并且确实存在一些环境(太空,国防,医疗等)不能容忍任何可控制的风险,因此它们禁止“不应”进行操作失败,因为当您发射火箭或控制心脏/肺部机器时,“它应该起作用”还不够好。
凯利·法国
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.