std::array 模板实例会占用更多的代码内存吗?

Will std::array template instances occupy more code memory?

我有一个没有 MMU 的微控制器,但我们使用的是 C 和 C++。

我们正在避免使用所有动态内存(即没有 new SomeClass()malloc())和大多数标准库。

半题 0:

据我了解,std::array 不使用任何动态内存,因此它的用法应该没问题(它仅在堆栈上)。查看 std::array 源代码,它看起来不错,因为它创建了一个 c 风格的数组,然后围绕该数组包装功能。

我们使用的芯片有1MB的闪存用于存储代码。

问题 1:

我担心在std::array中使用模板会导致二进制文件变大,这可能会导致二进制文件超过 1MB 代码内存限制。

我想如果你创建一个 std::array< int, 5 > 的实例,那么所有对 std::array 上的函数的调用都会占用一定数量的代码内存,比如说 X 字节的内存。

如果您创建另一个 std::array< SomeObject, 5 > 实例,然后调用该 std::array 的函数,这些函数中的每一个现在是否会在二进制文件中重复,从而占用更多代码内存? X 字节内存 + Y 字节内存。

如果是这样,您认为在代码内存容量有限的情况下生成的代码量是否会成为一个问题?

问题 2:

在上面的示例中,如果您创建了第二个 std::array< int, 10 > 实例,对函数的调用是否也会重复生成代码中的函数调用?即使两个实例属于同一类型,int?

std::array 被认为是零成本抽象,这意味着它应该可以由编译器相当优化。

对于任何零成本抽象,它可能会导致较小的编译时间惩罚,如果不支持真正零成本的优化,那么它可能会导致较小的尺寸或运行时惩罚。

但是,请注意编译器可以自由地在结构的末尾添加填充。由于 std::array 是一个结构,您应该检查您的平台如何处理 std::array,但我非常怀疑您的情况是否如此。

取这个数组和std::array案例:

#include <numeric>
#include <iterator>

template<std::size_t n>
int stuff(const int(&arr)[n]) {
    return std::accumulate(std::begin(arr), std::end(arr), 0);
}

int main() {
    int arr[] = {1, 2, 3, 4, 5, 6};
    return stuff(arr);
}

#include <numeric>
#include <iterator>
#include <array>

template<std::size_t n>
int stuff(const std::array<int, n>& arr) {
    return std::accumulate(std::begin(arr), std::end(arr), 0);
}

int main() {
    std::array arr = {1, 2, 3, 4, 5, 6};
    return stuff(arr);
}

Clang 很好地支持这种情况。所有带有 std::array 或原始数组的情况都以相同的方式处理:

-O2 / -O3 数组和 std::array with clang:

main: # @main
  mov eax, 21
  ret

然而,GCC 似乎在优化它时遇到了问题,对于 std::array 和原始数组的情况:

-O3 与 GCC 数组和 std::array:

main:
  movdqa xmm0, XMMWORD PTR .LC0[rip]
  movaps XMMWORD PTR [rsp-40], xmm0
  mov edx, DWORD PTR [rsp-32]
  mov eax, DWORD PTR [rsp-28]
  lea eax, [rdx+14+rax]
  ret
.LC0:
  .long 1
  .long 2
  .long 3
  .long 4

然后,在原始数组的情况下,-O2 似乎可以更好地优化,而 std::array:

则失败

-O2 海湾合作委员会 std::array:

main:
  movabs rax, 8589934593
  lea rdx, [rsp-40]
  mov ecx, 1
  mov QWORD PTR [rsp-40], rax
  movabs rax, 17179869187
  mov QWORD PTR [rsp-32], rax
  movabs rax, 25769803781
  lea rsi, [rdx+24]
  mov QWORD PTR [rsp-24], rax
  xor eax, eax
  jmp .L3
.L5:
  mov ecx, DWORD PTR [rdx]
.L3:
  add rdx, 4
  add eax, ecx
  cmp rdx, rsi
  jne .L5
  rep ret 

-O2 GCC 原始数组:

main:
  mov eax, 21
  ret

似乎 GCC 错误未能优化 -O3 但成功 -O2 已在最新版本中修复。

这是一个编译器资源管理器,其中包含所有 O2 and the O3


在陈述了所有这些情况后,您可以看到一个共同的模式:二进制文件中没有输出有关 std::array 的信息。没有构造函数,没有 operator[],甚至没有迭代器,也没有算法。一切都是内联的。编译器擅长内联简单的函数。 std::array 成员函数通常非常简单。

If you create another instance of std::array< SomeObject, 5 >, then call functions to that std::array, will each of those functions now be duplicated in the binary, thus taking up more flash memory? X bytes of memory + Y bytes of memory.

嗯,您更改了数组包含的数据类型。如果您手动添加所有函数的重载来处理这种额外的情况,那么是的,所有这些新函数可能会占用一些 space。如果你的函数很小,它们很有可能被内联并且占用更少space。正如您在上面的示例中看到的,内联和常量折叠可能会大大减少二进制文件的大小。

In the above example, if you created a second std::array instance, would the calls to functions also duplicate the function calls in flash memory? Even though both instances are of the same type, int?

这又取决于。如果您在数组大小中有许多函数模板,std::array 和原始数组都可能 "create" 不同的函数。但同样,如果它们是内联的,则无需担心重复。

两者都是原始数组,std::array,您可以传递指向数组开头的指针并传递大小。如果您发现这更适合您的情况,请使用它,但原始数组和 std::array 仍然可以做到这一点。对于原始数组,它隐含地衰减为一个指针,并且使用 std::array,您必须使用 arr.data() 来获取指针。