在嵌入式系统中是否应广泛使用std::vectors?

16
在编写有限CPU和内存资源的嵌入式系统的C++代码时,通常的经验法则是在堆栈中实例化对象,并避免使用堆,除非真正必要。这样做当然有许多已知的好处,但随着STL的出现和人们推荐std::vectors作为一种高效的数据结构,它是否违反了我提到的经验法则,因为vector将使用堆?
例如:过去,我们会声明具有满足使用要求的已知大小的静态数组。现在,我们只需使用向量。
我对这种转变并不感到舒适,因为向量有可能无法分配所需的内存(提醒:这是针对内存有限的嵌入式系统的)。使用已知大小的堆栈数组可以保证在编译时有空间进行分配。
调用reserve()有些帮助,但这是在运行时完成的。
那么,这是一个值得关注的问题,还是我只是过于谨慎?对于嵌入式环境来说,使用这些向量肯定更容易,但可能不是一个好主意?
注意:这不是关于动态与固定数组的问题,而更多的是关于如何在内存中分配数据的问题,这对我的环境来说非常重要。例如,有些人会这样做:假设数组的大小在1到10个元素之间变化。有些人会在堆栈中创建一个覆盖这个大小的数组,并根据当前大小进行空置终止。这样可以避免碎片,而且我们可以保证在编译时进行分配。然而,转换为vector使代码更清晰了,但代价是使用堆,并有可能在分配失败时处理异常。这就是我担心的地方。

如果您知道向量中将有多少项(或至少有一个粗略估计),则可以为数据“reserve”空间。 - Some programmer dude
2
如果在编译时已知大小并确保堆栈足够大,则静态或自动数组没有问题。当您需要动态数组时,请使用“vector”。(现在,如果您想要使固定大小的数组看起来更像STL,则可以使用“std :: array”)。 - Mike Seymour
这个问题没有通用的答案。在嵌入式系统上,您可能会遇到由标准向量引起的瓶颈,也可能不会。只有在对特定系统上运行的特定代码进行适当基准测试之后,才能决定是否使用它。 - user529758
4
堆栈和堆一样,都可能出现问题。 "Stacks can blow just like heaps can be full." 的意思是,如果使用不当,堆栈会产生类似于堆满的问题。 - Yakk - Adam Nevraumont
2
“有限的CPU和内存资源”今天的含义与创建此类经验法则时不同。这取决于您的硬件资源有多么有限。如果您的内存只有几十或几百字节,CPU速度只有几千赫兹,那么这些规则仍然适用,但是如果您拥有数兆字节的RAM和数千兆赫的处理器速度,则通常可以避免担心优化问题。 - Kristopher Johnson
3
Kristopher Johnson,我不同意您的观点。我正在使用的硬件有32MB的内存,并遵循经验法则使我们能够保持这个规格而不必创建新的硬件。随着新程序员的加入,每个功能所使用的内存突然激增。 - Ryuu
5个回答

10

我认为您忘记了STL容器的一个非常重要的属性: 分配器

STL容器(无论是vector还是其他容器)都从其分配器中获取所有内存(除了您可以使用sizeof检查的非常基本的堆栈占用)。因此,在嵌入式开发中,提供专用的分配器完全适用,该分配器:

  • 将从预留的内存区域分配
  • 将限制最大资源消耗以防止OOM
  • ...

随着C++11的出现,您甚至可以使用有状态的分配器,以便单个分配器类型可以指向不同的内存池。

因此,使用std::vector,甚至包括std::setstd::map,与预分配策略并不矛盾;但请记住,除了std::vector之外,其他STL容器通常具有一些每项开销,必须在计算应绘制的内存区域大小时将其考虑在内。


谢谢,马特。我会研究分配器的。 - Ryuu
1
似乎无论您使用什么动态分配方法,无论是堆、'allocators'还是栈,如果超出了编译时定义的内存,您将需要管理内存不足的情况。捕获异常并不困难。困难的是设计故障安全或鲁棒的响应来处理内存不足错误。对于带有相关风险/危害的嵌入式系统,我建议坚持使用固定大小的数组。 - Dana
@Dana:在这里,固定大小的数组和向量之间没有区别;无论哪种情况,您都需要计算(并验证)所需的最大内存量,并确保它存在。 - Matthieu M.

4
“依赖”这个词可能有点轻描淡写了。使用向量和reserve来分配数据空间,可以高效地分配空间并避免不必要的拷贝。如果编译器足够好,向量本身将演化为一个带有大小的堆分配数组。
此外,请注意我提到了“堆”。如果你更愿意将数据分配到堆栈上,你只能使用固定大小的数组(包括std::array)。
而且,vector的实现也很大程度上取决于编译器。老版本的编译器可能会(强调可能)效率较低。因此,在嵌入式系统中,你真的需要了解架构和编译器,才能在对使用什么东西做出概括性判断时避免被误导。”

3
在堆栈中使用已知大小的数组可以保证在编译时有足够的空间进行分配。但这种做法是错误的假设。
当你有很多调用(即使不是递归,但是有很多层级的堆栈),你不能确定堆栈是否有足够的空间来容纳需要创建的对象。
虽然有基本检查(如果您的对象大小至少超过size_t),但我甚至不认为它们被标准强制执行。
下面是一个 -- 有些过度 -- 的例子:
#include <iostream>

template <unsigned long size>
struct BigObject
{
    unsigned long array[size * size];
    BigObject<size - 1> object;
};


template <>
struct BigObject<0>
{
    unsigned long array[1]; 
};

BigObject<900>& recurse(BigObject<900>& object1, unsigned long n)
{
    if (n == 0)
    {
        return object1;
    }

    BigObject<900> object;

    return recurse(object, n);
}

int main(int argc, char const *argv[])
{
    BigObject<900> object;
    recurse(object, 20);

    std::cout << sizeof(object) << std::endl;
    return 0;
}

http://ideone.com/pkHo43

它会崩溃。 而这并不是什么特殊情况。我在一个32位的桌面应用程序中遇到了这个问题(所以没有内存限制),在堆栈上分配了一个太大的数组。


如果对象/数组是全局的,那么它不是有保证的吗? - Ryuu
是的,这是由链接器保证的。你需要检查一下,但我认为如果程序只使用存储在.data段中的对象(除非无法加载库本身),它就不会耗尽内存而运行失败。 - Johan
虽然你可以获得衡量可能调用链的工具,并计算所需的最大堆栈深度(或者,至少,你可以 - 它们可能在支持递归的语言中不太有用)。 - Martin Bonner supports Monica

2

std::vector用于动态调整大小的数组。如果您在编译时知道大小,可以使用std::array代替。

如果您只知道数组的大致大小,自C++14以来,您可以使用std::dynarray。此数组具有固定大小,在对象生命周期内无法更改。当使用没有分配器的std::dynarray时,可能会进行额外的优化,不会调用operator new,而是使用基于堆栈的分配。


有些人会这样做:假设数组的大小可以在1到10个元素之间增长或缩小。有些人会在堆栈中创建一个覆盖此大小的数组,并根据当前大小进行NULL终止。转换为向量更加简洁,但代价是使用堆。这就是我所担心的。 - Ryuu
好的,我会研究一下std::dynarray。谢谢! - Ryuu
@Ryuu 注意,std::dynarray 仅在 C++14 及以上版本中可用。 - fasked

1
当使用足够的优化标志进行编译时,std::vector只是一个指针和两个size_t。现在,如果您事先知道向量的大小并且它永远不会改变,则size_t的内存将浪费。
我建议:
  • 固定(小)大小 => 简单数组或 std::array
  • 动态大小或大量元素 => std::vector
  • 正如评论中提到的那样,您可以通过为begin/end迭代器使用指针,在固定大小的数组上使用例如<algorithms>的大多数功能。
    例如:
    int array[8] = { 1,2,3,4,5,6,7,8 };
    std::random_shuffle(array, array+8);
    

    为什么存在着分段的风险?vector将其元素存储在连续的内存中。 - Danvil
    在嵌入式系统中,您可能希望使用向量来降低堆栈使用率,特别是在递归函数调用时。 - IdeaHat
    @Ryuu 在你重构所有代码之前,你知道你也可以在连续内存的指针上使用std::sort等函数,对吧? - IdeaHat
    尽管向量元素是连续的,但每次 push_back 可能会导致重新分配内存以适应新的大小。如果下一个内存块被占用,它需要找到其他地方。这难道不会导致碎片化吗? - Ryuu
    MadScienceDreams,是的,就像我之前提到的,我知道它们也适用于指针。我会尝试看看其他像find()这样的东西是否也可以这样工作。 - Ryuu
    显示剩余2条评论

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接