《嵌入式Linux内存与性能详解》笔记2——进程内存优化

羽林君 2023-12-23 00:29

一、前言

我们上文《linux应用程序——内存测量》说了如何测量分析系统内存和进程内存的使用情况。当我们大概知道进程的使用情况后,我们可以针对性地做一些优化,那么本文将简单地说几种内存优化的方法。

二、堆栈优化

在讲解内存优化前,这里简单地说明一下一个程序的组成

  • 栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量的值等。

  • 堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由 OS 回收。

  • 数据段:初始化的全局变量和静态变量在一块区域,程序结束后由系统释放

  • BSS段:未初始化的全局变量 和未初始化的静态变量所在区域。程序结束后由系统释放。

  • 代码段:存放程序的二进制代码和文字常量

其中我们讲解 的优化方法,其余的更加深入我们找机会再做讲述

2.1 堆优化

提起堆,熟悉的读者应该就会想起 malloc 或者 new 。开辟内存是我们常见的使用手法,但使用得不好容易造成内存泄露和内存空洞,导致系统无法正常回收内存,造成的结果就是系统内存不足,严重的导致程序崩溃退出。

2.1.1 malloc

  • 当程序 malloc 申请内存时:

  1. 如果堆的地址空间还在 1G 以下,进程会通过系统调用 brk,来让 Linux 内核扩展堆顶的内存空间(堆是向上增长)。

  2. 如果堆的地址空间还在 1G 以上,那么会使用 mmap 映射空闲的物理页内存来获取内存

  • 当使用 free 释放内存时,进程又会通过系统调用 brkummap,来让内核缩减堆的内存空间或去映射

  • 用户态 使用 malloc 申请的内存是以 字节 为单位,而在内核中内存的管理是以 页(4K) 为单位

  • malloc 开辟的内存都是 8字节对齐

  • 太多的 brk系统调用,会使进程的速度变得很慢。

  • malloc 最小的分配长度为 16字节,如果分配 1、2个字节会造成浪费

  • 使用 malloc 时并不会直接向内核请求内存,而是先通过 glibc 的堆管理,再获得内存。而 glibc 的堆管理使用使用一种结构体。该结构体来定义 malloc 分配或释放的内存块,如下所示:

    struct malloc_chunk {    INTERNAL_SIZE_T      prev_size;  /* Size of previous chunk (if free).  */    INTERNAL_SIZE_T      size;       /* Size in bytes, including overhead. */    struct malloc_chunk* fd;         /* double links -- used only if free. */    struct malloc_chunk* bk;    /* Only used for large blocks: pointer to next larger size.  */    struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */    struct malloc_chunk* bk_nextsize;}

    我们以一段小程序为例子来讲解:

    #include #include  int main() {      char* p;    p = malloc(20);    strcpy(p, "Hello,world!");    printf("%s\n", p);     printf("%#x\n", *(p-4));     free(p);     return 0; }


    运行结果

    (p-4) 这个地址,其实记录着 malloc空间 的大小,我们看以下这段内存的分布图,如下:

    内存分布图:

    会发现在数据前面还有 2 个 4字节 的空间,在将其与我们前面的结构体对比就会发现前 2 个成员其实是一样的,而后面的 4 个指针成员被复用为内存空间了,为什么呢?
    因为如果当前 chunk 是使用中的,那么 fdbkfd_nextsizebk_nextsize都是无效的,它们都是关于空闲链表的指针,那么这些指针的空间全部被认为是空闲空间,所以直接复用这些成员的内存。

    注意:这里面有两个标志位:

    • P=1:表示上一块正在被使用,这时 prev_size 通常为 0

    • P=0:表示上一块空闲,这时 prev_size 通常为上一块的大小。

    • M=1:表示该内存块通过 mmap 来分配。在分配 大块内存 时,会采用 mmap 的 方式,那么在释放时会由 munmap_chunk 去释放;否则,释放时由 chunk_free 完成。

    • M=0:则表示该内存块不采用 mmap 方式分配。

    我们再看看程序的计算结果。0x19 去掉 3 个比标志位就是 0x18 等于十进制的 24,我们分配了 20 字节,其 8 字节对齐的结果就是 24

    2.1.2 mallopt

    glibc 为我们提供了了 堆管理 ,同时也保留了一些策略设置的接口,让我们自行定义策略。我们可以通过使用 mallopt 来定义完成策略的设置,其原型如下:

    /*    * param:想要设置的属性项    * value:设置值,单位为字节*/int mallopt(int param, int value);

    param 参数意义如下:

    • M_MXFAST:定义 fastbins小块内存 阀值,小于该阀值的小块空闲内存在释放后将不会去尝试合并,并且会在进程中继续使用(可以提高内存分配的速度)。其 缺省值 为 64,设置为 0 则禁止掉 fastbinsfastbins 生效时,对于一些小块的内存在释放后不去尝试合并以节省 CPU 和内存,因为 glibc 合并内存块需要去跟踪每个内存块。跟踪内存是需要代价的,如果关闭 fastbins 可以则会为每个内存块付出代价,无论内存块的大小。所以设置 M_MXFAST ,需要适度,太大太下都有可能产生不良影响


    • M_MMAP_THRESHOLDlibc大块内存的阀值。申请大于该阀值的内存,内存管理器将使用 mmap 系统调用申请内存;如果申请小于该阀值的内存,内存管理使用 brk系统调 用来扩展堆顶指针。该阀值缺省值为 128kB

    int main(){    char *p=malloc(1024*521);     pid_t pid=getpid();       printf("pid = %d\n", pid);      pause();     return 0; }


    如上所示的代码,会发现进程的 maps 多了一个段,该段的地址不在 ,这就是使用 mmap 开辟    的内存空间,如下所示:

    • M_MMAP_MAX:该进程中多使用 mmap 分配地址段的数量。

    • M_TRIM_THRESHOLD:堆顶内存回收阀值,当堆顶连续空闲内存数量大于该阀值时,libc 的内存管理其将调用系统调用 brk,来调整堆顶地址,释放内存。该值缺省为 128k

    int main(){  char *p[11];       int i;       /* 开辟 11 片内存 */  for(i = 0; i < 11; i++)       {               p[i]=(char*)malloc(1024*2);               strcpy(p[i], "123");       }        /* 释放 11 片内存 */  for(i = 0; i < 11; i++)   {               free(p[i]);       }   
    pid_t pid=getpid(); printf("pid = %d\n", pid); pause();
    return 0; }


    如上所示的代码,在运行后我们查看 smaps 属性,如下:

    smaps属性

    可见,我们的内存并没有被系统回收,而是一直还存在内存中,如果我们在前面加上下面的代码:

     /* 当堆顶空闲内存大于 1Kb 时回收 */
    mallopt(M_TRIM_THRESHOLD, 1024); /* 一定要加入打印才能生效,不然无法生效,笔者也不清楚为何,求大神告知 */ printf("finished M_TRIM_THRESHOLD\n"); /* 设置堆顶无空闲内存块 */ mallopt(M_TOP_PAD, 0);

    运行后查看 smaps 属性如下所示,可见内存已经被回收了

    • M_TOP_PAD:该参数决定了当 libc 内存管理器调用 brk 释放内存时,堆顶还需要保留的空闲内存数量。该值缺省为 0

    2.1.2 内存空洞

    内存泄露 是程序常见的一种漏洞,但这种漏洞我们往往能够通过跟踪或者其他手段找出来,但 内存空洞 比较不容易发现,我们看看下面这段代码:

    int main(){    mallopt(M_TRIM_THRESHOLD, 1024);    printf("finished M_TRIM_THRESHOLD\n");         mallopt(M_TOP_PAD, 0);
    char *p[11]; int i; /* 开辟 11 片内存 */ for(i = 0; i < 11; i++) { p[i]=(char*)malloc(1024*2); strcpy(p[i], "123"); } /* 只释放10片内存 */ for(i = 0; i < 10; i++) { free(p[i]); }
    pid_t pid=getpid(); printf("pid = %d\n", pid); pause();
    return 0; }

    我们通过设置 堆顶空闲内存块大小1KB堆定空闲内存0 的方法去掉因为策略造成影响,与上面不同的是我们开辟 11 块内存,但只释放了 10 块,我们看看运行后的 smaps 的情况:

    smaps

    发现我们释放堆顶下面的 10 块内存并没有被系统回收,与前面的相比,仅仅只是少释放了一块内存。这样的现象就是 内存空洞,也就是 只要堆顶部还有内存在使用,堆顶下方不管释放了多少内存都不会被释放。

    假设我们把开辟的内存放大一点,如下所示:


    int main(){    mallopt(M_TRIM_THRESHOLD, 1024);    printf("finished M_TRIM_THRESHOLD\n");         mallopt(M_TOP_PAD, 0);
    char *p[11]; int i; for(i = 0; i < 5; i++) { p[i]=(char*)malloc(1024*512); strcpy(p[i], "123"); }
    for(i = 0; i < 4; i++) { free(p[i]); }
    pid_t pid=getpid(); printf("pid = %d\n", pid); pause();
    return 0; }

    上面的代码使用了 512KB 大小的内存块,那么我们看看运行后的 smaps

    smaps


    因为我们使用了大内存块,所以系统是通过 mmap 来开辟内存的,所以 堆段 的使用情况没有变,而下面新增了一个内存块,可见该段实际使用的物理内存只有 4K,以为我们为其赋值了字符串,所以内核为该段开辟了一个内存页,而其他内存已经被释放掉,这样就没有造成 内存空洞但因为使用了更多的系统调用,其性能会有所下降。

    《嵌入式Linux内存与性能详解》建议:在申请分配内存时,本着就近原则 就可以了,需要的时候才分配内存,不需要了立刻释放。不必去严格的追求申请和释放的顺序。

    2.1.3 内存跟踪

    这里简单的介绍一种排查 内存泄露 的方法,使用 mtrace 来进行内存跟踪。使用步骤如下:

    1. 引入头文件 #include

    2. 在需要跟踪的程序中需要包含头文件,而且在 main函数开始 调用函数 mtrace。这样进程后面一切分配和释放内存的操作都可以由 mtrace 来跟踪和分析。

    3. 定义一个 环境变量,用来指示一个文件。该文件用来输出 log 信息。如下的例子:
      export MALLOC_TRACE=mtrace.log

    4. 正常运行程序,此时程序中的关于内存分配和释放的操作都可以记录下来。


    代码如下:

    #include #include #include  #include  #include #include  int main(){    mtrace(); 
    mallopt(M_TRIM_THRESHOLD, 1024); printf("finished M_TRIM_THRESHOLD\n"); mallopt(M_TOP_PAD, 0);
    char *p[11]; int i; for(i = 0; i < 5; i++) { p[i]=(char*)malloc(1024*512); strcpy(p[i], "123"); }
    for(i = 0; i < 4; i++) { free(p[i]); }
    pid_t pid=getpid(); printf("pid = %d\n", pid); muntrace(); pause();
    return 0; }

    mtrace.log


    我们开辟了 5 块内存,却释放了 4 块,所以图中一共有 5 个 +号 和 4 个 -号

    2.1.4 堆优化总结

    • 堆内存的小单位为 16Byte,所以尽量减少小块内存的申请,避免内存浪费。

    • 调整 M_MMAP_THRESHOLD,降低 mmap 的门槛,会降低内存空洞的风险,但也会增加系统调用,降低性能。

    • 调整 M_TRIM_THRESHOLD,减少堆顶连续内存门槛,释放更多的堆顶内存。

    以上是从书中获取到的经验,但无论如何还是需要结合实际的工程需求来做优化,希望可以帮到各位读者

    2.2 栈优化

    进程的 栈 是由程序自动来维护的,不需要手动申请和释放。一般情况下,栈是一段线性分布的内存,不会出现碎片问题。它是给函数存放跳转时的环境的。

    2.2.1 栈分配内存

    虽然栈的使用我们一般不需要去操心,但在某些情况下我们可以使用函数 alloc 来获取栈上的内存。同理,这块内存是不需要我们释放的。

    • 使用 alloc 分配的内存跟通过定义变量获取的内存有什么不同呢?

    我们看看下面 2 段程序及其结果:


    int main(){    int n = 0;     char* p = NULL;     for(int i = 0; i < 1024; i++)    {        p = (char*)alloca(1024*5);     }    pid_t pid = getpid();    printf("pid:%d\n",pid);    pause();    return 0;}


    不赋值

    #include #include #include  #include  #include #include  #include 
    int main(){ int n = 0; char* p = NULL; for(int i = 0; i < 1024; i++) { p = (char*)alloca(1024*5); memcpy(p, "123", 4); } pid_t pid = getpid(); printf("pid:%d\n",pid); pause(); return 0;}


    复制

    可以看到,复制前后我们使用的物理内存完全不一样,也就是说使用 alloc 开辟的内存未必就是物理内存,只有在复制后产生 缺页异常 后才能获取内存

    再看看 通过变量获取内存,同理还是看看 2 段程序:

    int main(){    int n = 0;     char p[1204*1024*5];     pid_t pid = getpid();    printf("pid:%d\n",pid);    pause();    return 0;}


    非初始化赋值

    #include #include #include  #include  #include #include  #include 
    int main(){ int n = 0; char p[1204*1024*5] = {0}; pid_t pid = getpid(); printf("pid:%d\n",pid); pause(); return 0;}


    初始化赋值

    可见,通过定义变量获取的内存也是需要对其进行赋值才会有物理内存产生。

    可见也就是说栈的申请不会通过系统调用来获取物理内存。而是随着压栈的操作,栈顶指针访问不存在的物理内存后发生 缺页异常 从而获取内存。因为没有通过系统调用,所以这样的内存获取也比较快捷和方便

    综上所述,所以按照笔者的理解 两者应该是没有区别。

    2.2.2 栈释放内存

    那么我们在栈上面申请的物理内存是如何释放的,我们通过一个代码片段看一看:

    int num=10000;   int func() {     num--;     if(num == 0)     {             return 0;     }     func(); }int main(){    func();    pid_t pid = getpid();    printf("pid:%d\n",pid);    pause();    return 0;}


    递归栈使用情况

    我们发现:在函数退出后,栈依旧没被释放掉,还是被进程抢占着。这个发现似乎有点惊讶,我们以为的是在函数退出后,物理内存会被释放掉返回给系统。这意味着 栈内存的使用只会增加不会减少。

    毕竟在栈的内存并不通过系统调用,有触发申请跟回收的事件。但栈只是通过 缺页异常 这种硬件异常来获取内存,且我们并没有合适的时间来让栈进行释放。但是这样做的好处就是函数使用栈的时候不用频繁使用内存,对于使用频繁的函数来说这样的效率会更加高。

    2.2.3 栈内存总结

    • 尽量避免在使用频率低的栈空间申请大量内存

    • 尽量避免使用递归函数

    最后附上一张 函数栈帧结构图

    函数栈帧结构图


    三、ELF文件瘦身

    3.1 ELF文件介绍

    ELF文件 是 linux 下的 可执行文件格式,包括 可定位文件(.o)、静态库(.a)、共享库(.so) 和 核心转储文件(core dump)等。
    那么在查看 ELF文件 时我们有 2 种角度来查看:

    • 链接视图:将 ELF文件 中所需要保存的信息按照信息的类型、格式的不同,分别保存在文件中不同的 节区(section) 。为了访问这些 section,在 ELF文件 中又包含了一个 section 位置的索引,我们称这个索引为 节区头部表(section headers)

    • 执行视图:按照运行时的需要,执行视图把 section 按照规则分类,并且划分为不同的 段(segment)。为了说明 segment 和 section 的关系,在 ELF 文件中又引入了 程序头部表(program headers),又称为 段表。

    我们可以看看 section 和 segment 之间的关系,如下图所示:

    链接视图与执行视图比较


    3.1.1 ELF文件头分析

    我们使用 readelf -h 来查看 ELF文件头。
    ELF文件头 各个字段的含义可以查看文章 《linux应用程序——ELF查看工具》。我们主要简单看一下需要注意的地方

    • Magic:该字段的值为 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00

    1. 7f  45  4c  46 表明该文件是 ELF文件,

    2. 01 表明该文件运行在32位的操作系统上

    3. 01表明数据采用小端排序,高位在前

  • Size of this header:ELF文件头 大小,值为 52

  • Start of program headers:注意这个字段的值为 52,即 程序头部表(program header) 在 ELF文件 中的位置位 52,而 ELF文件头 大小的值也是为 52,说明 程序头部表 在文件中紧挨着 ELF文件头。

  • 3.1.2 ELF文件节区信息

    使用 readelf -S 查看文件的节区信息,如下图所示

    节区信息


    其输出细节可以查看《linux应用程序——ELF查看工具》,这里我们看常用的节区:

    • .hash:符号哈希表

    • .dynsym:动态链接符号表

    • .dynstr:动态链接字符串表

    • .rel.dyn:节区中包含了重定位信息

    • .rel.plt:节区中包含了重定位信息

    • .init:此节区包含了可执行指令,是进程初始化代码的一部分。当程序开始执行时,系统要在开始 调用主程序入口之前(通常指 C 语言的 main 函数)执行这些代码

    • .plt:此节区包含过程链接表(procedure linkage table)

    • .text:此节区包含程序的可执行指令。

    • .fini:此节区包含了可执行的指令,是进程终止代码的一部分。程序正常退出时,系统将安排执行 这里的代码。

    • .rodata:这些节区包含只读数据,这些数据通常参与进程映像的只读代码段。

    • .init_array:进程初始化的所运行的函数指针数组

    • .fini_array:进程退出时的所运行的函数指针数组

    • .dynamic:此节区包含动态链接信息。

    • .got:此节区包含全局偏移表,其与 plt 一起协作完成符号的动态查找

    • .data:节区包含初始化了的数据,将出现在程序的内存映像中。

    • .bss:包含将出现在程序的内存映像中的为初始化数据。根据定义,当程序开始执行,系统将把这 些数据初始化为 0。此节区不占用文件空间。

    • .comment:符号调试信息

    • .debug_aranges:符号调试信息

    • .debug_pubnames:符号调试信息

    • .debug_info:符号调试信息

    • .debug_abbrev:符号调试信息

    • .debug_line:符号调试信息

    • .debug_frame:符号调试信息

    • .debug_str:符号调试信息

    • .note.gnu.arm.ide:符号调试信息

    • .debug_ranges:符号调试信息

    • .shstrtab:节区包含 节区名称

    • .symtab:节区包含一个 符号表

    • .strtab:节区包含一个 字符串表

    Flags属性 为 A 和 AX 的节,在 ELF文件 的分布是连续的,中间没有穿插 AW 或 不需要内存 的节。同样地,所有属性为 AW 的节都顺序的排列在一起。这是为进程运行时划分为 只读代码段 和 可读写数据段 奠定基础。

    3.1.3 ELF文件段信息

    使用 readelf -l 查看文件的段信息,如下图所示

    段信息


    其输出细节可以查看《linux应用程序——ELF查看工具》,这里我们来分析其中常用的段:

    • PHDR:其偏移值为 0x34,十进制为 52,正好是 程序头部表 在 ELF文件 中的位置

    • INTERP:该段给出了运行该文件所需要的解释器,如图所示,该程序使用的解释器为 ld-linux-armhf.so.3。对 ELF文件 来讲,loader的作用是加载 ELF文件 到内存后做 符号解析,直到把程序指针返还给进程

    • LOAD:段的内容将会被加载到内存中,可以根据权限来判断是代码段还是数据段。权限为 只读、可执行 ,该段对应于 代码段,权限为 可读、可写 的,对应于我们常说的 数据段。

    • DYNAMIC:该段给出了此 ELF文件 所需要的 动态链接信息。

    再看看下面的 节与段的映射关系,可以看到我们一共有 9 个段,而下面的映射也将一些节区分别映射到了这 9 个段中,有些节区没有映射进来是因为不需要参与到程序的运行。同时可以看到,在 代码段 中的各个 节区 在文件中是 顺序排列 的;数据段 也是一样。这是因为在程序运行前期,loader 会将 ELF文件 的 代码段 和 *数据段 使用 mmap 将其映射到内存中,这就要求各段所包含的 节区 在文件中必须是连续的。

    3.1.4 ELF文件动态链接信息

    使用 readelf -d 查看文件的动态信息,如下图所示:

    动态链接信息


    其中 Type 属性为 NEEDEN 的代表程序依赖该 动态库,这样 loader 就知道加载哪些动态库到内存中来支持我们的程序。

    3.1.5 ELF文件瘦身

    在知晓了 ELF文件 的一些构成后,我们可以使用 strip 工具来删除 ELF文件 一些没有用的节区。
    比如先使用 strip 工具直接对程序进行删减:

    strip


    我们还可以加入选项 --remove-section 来指定要删除的节区,比如 comment节区 我们不需要,我们可以使用 strip --remove-section=.comment,如下所示:

    删除指定段

    注意:图中使用 交叉编译链 是因为 strips 无法识别 ARM 平台的程序。

    四、数据段及代码段优化

    4.1 数据段说明

    在我们的程序中,与 数据段 相关的节区有:

    • .init_array

    • .fini_array

    • .dynamic

    • .got

    • .data

    • .bss

    各个节区的作用我们之前已经有简单说过了,下面我们重点关注 .data 和 .bss 段。他们 2 者的作用如下:

    • .bss:主要用来保存 未初始化 或 初始化为 0 的全局变量和静态变量。

    • .data: 主要用来保存初 始化不为 0 的全局变量或静态变量。

    他们之间显著的区别就是变量是否 初始化不为 0:

    • 因为初始化不为 0 ,所以程序需要记录他们的初始值。这样就需要在程序中开辟空间来记录他们的值

    • 初始化为 0 ,则将这些变量所在的段映射到一个全 0 的页面即可,所以 .bss段 不占空间

    我们先看看 初始化为 0 的实例代码,如下所示的:

    int bss_array[1024 * 1024] = {0}; int main(int argc, char* argv[]) {    pause();     return 0; }


    bss段大小

    maps

    readelf

    初始化不为 0 的实例代码:

    int data_array[1024 * 1024] = {1}; int main(int argc, char* argv[]) {    pause();     return 0; }


    data段大小

    maps

    readelf

    两段代码都在程序中开辟了 4M大小 的数组,他们的区别如下:

    • 初始化为0:

    1. 其 maps 属性中,堆区 的范围是经过拓展的,是大于 4M的

    2. readelf 读取出来的都是数据段大小不符合 4M大小 的要求

  • 初始化不为0:

    1. 其 maps 属性中,堆区 的范围并没有经过拓展。

    2. maps 属性中的 数据段 大小符合 4M 的要求

    3. readelf 读取出来的都是数据段大小符合 4M大小 的要求

    Loader 在处理数据段时,其首先根据 FileSiz 的大小来创建 数据段。初始化为 0 进程的数据段只有 4k,而 初始化不为 0进程的数据段就有 4M 多。初始化为 0 进程为了容纳 bss段 中 4M大小 的数据,loader 在进程的 堆段 中申请出足够的内存来容纳它。所以我们发现在 初始化为 0 进程中,虽然我们没有申请内存,但却有了一个 4M 的堆段。初始化不为 0进程则是数据段就直接开辟为 4M大小 以供程序使用

    某种程度上来说,使用 bss段 自动使用了 malloc 来帮我们自动开辟内存了,所以在我们访问此段时是不会产生 缺页异常 的。而使用 data段 则会触发并分配物理内存

    以上是只有 bss段 和 data段 的情况,如果同时有 bss段 和 data段,代码如下:

    #include #include #include  #include  #include #include  #include  int data_array[1024*4 - 2] = {1}; int bss_array[4] = {0};int main(int argc, char* argv[]) {    pid_t pid = getpid();    printf("pid:%d\n",pid);       printf("data_array = %p, bss_array = %p\n", data_array, bss_array);    pause();     return 0; }


    运行结果

    maps

    我们发现 2 个不同段的数组,其地址居然都在数据段。在这种情况下, loader 将 数据段 中的 .data 节进行填充后,使用 .bss 节数据对 剩余的字节进行填充,并将这些剩余的字节全部填充为 0,这同时会造成对后一个页面的 写操作,从而产生 dirty page。

    4.2 数据段优化

    关于 数据段 的优化,并不是针对进程本身,而是针对 动态库。如果我们在编写动态库时能尽量优化 数据段 ,那么可以节省比较多的内存空间。这里有一些方法步骤可供参考:

    • 尽可能的减少全局变量和静态变量。我们可以使用“nm”来列出所有在.data 和.bss 节的变量

    • 将 只读的全局变量,加上 const,从而使其转移到 代码段。因为代码段的共享特性,可以节省内存。

    • 对频繁引用的字符串可以将其定义在 代码段
      *不要在 头文件 定义变量(但可以声明,比如使用 extern 声明模块内部的变量),除了能够有效减少编译错误外还能减少因为多个重复引用而浪费内存。

    五、代码段

    代码段在内存优化方面作用并不大,因为代码段是整个进程共享的,而且在内存不足的时候会回收。这里简要地说几点重要的。

    5.1 删除冗余代码

    我们尽可能的删除不必要的代码及变量,因为冗余代码有可能会导致物理内存的使用增加。比如我们定义一变量,却不使用它。在程序运行的时候,因为这种变量的存在,可能会导致 缺页异常 发生的概率增加,因此进程的运行效率会下降。下面 2 个编译选项有助于显示此类冗余代码:

    • -Wunused:检查无用代码

    • --Wunreachable-code :检查从未使用的代码

    5.2 使用 Thumb指令

    我们知道 Thumb指令 是一种指令高级密集的指令集,它与 ARM指令集 之间的关系大致如下:

    • 在功能相同的情况下,Thumb指令集 比 ARM指令集 占用的内存空间小,因为它是一种 16位 的指令集。

    • 所有的 ARM指令 多有对应的 Thumb指令,两者在一定的情况下可以互相调用。

    • 大多数 Thumb指令 是无条件执行的,而大多数 ARM指令 是有条件执行的

    高密度就以为在同等功能的情况下,Thumb指令集 要使用更多的指令来完成功能,从而有可能导致运行的时间比较长,有说法 Thumb指令集 和 ARM指令集 之间的效率关系如下:

    • Thumb 代码所需的存储空间约为 ARM 代码的 60%-70%

    • Thumb 代码使用的指令数比 ARM 代码多约 30%-40%

    • 若使用 32位 的存储器,ARM 代码比 Thumb 代码 快约 40%

    • 若使用 16位 的存储器,Thumb 代码比 ARM 代码快约 40%-50%

    • 与 ARM 代码相比较,使用 Thumb 代码,存储器的 功耗 会降低约 30%

    综上所述,如果系统的 性能 有较高要求,应应该使用 32位存储系统 和 ARM指令集。如果系统的 成本及功耗有较高要求,则应使用 16位存储系统 和 Thumb指令集。

    我们可以添加编译选项 -mthumb ,让编译器使用 Thumb指令 来编译程序。

    当然了,在一些极端场合,可能我们不得不同时使用 ARM指令集 和 Thumb指令集,编译器是支持同时使用 2 种指令集的。但是有些情况是只有在 ARM状态下才能执行

    • 使用或者禁止异常中断 只能在 ARM状态 下完成

    • ARM处理器 总是从 ARM状态 开始执行。按照笔者理解 main函数 所在的文件需要用 ARM指令集编译

    下面是笔者的例子,与 《嵌入式Linux内存与性能详解》 描述不符,但作为笔记还是记下来。

    /* thumb.c */#include void func_thumb(){    printf("I'm thunmb\n");}
    /* arm.c */#include extern void func_thumb();void func_arm(){ func_thumb(); printf("I'm arm\n");}
    int main(){ func_arm(); }


    第一次分别使用下面的指令进行编译:

    arm-linux-gnueabihf-gcc  -mthumb -o thumb.o -c thumb.carm-linux-gnueabihf-gcc  -o arm.o -c arm.carm-linux-gnueabihf-gcc  -o arm_and_thumb arm.o thumb.o

    然后在使用 objdump 反编译 arm_and_thumb,返现他们之间的调用并没有 Thumb状态 和 **ARM状态之间切换,其汇编如下图所示:

    反汇编


    可以看到在各个函数之间的跳转并没使用 bx 或者 blx 这样的会更改状态的指令
    注:关于 bx 或者 blx 请各位读者自行查阅资料

    在笔者经过一番查找后发现有一个编译选项 -mthumb-interwork,其意义是 生成的目标文件,允许在ARM和Thumb之间交叉调用。
    第一次分别使用下面的指令进行编译:

    arm-linux-gnueabihf-gcc -mthumb-interwork -mthumb -o thumb.o -c thumb.c
    arm-linux-gnueabihf-gcc -mthumb-interwork -o arm.o -c arm.c
    arm-linux-gnueabihf-gcc -mthumb-interwork -o arm_and_thumb arm.o thumb.o

    反编译之后情况依旧相同,与书中描述不符。这里笔者猜想可能是编译器不支持该选项,如果有读者可以解答该问题,还请不吝赐教。

    六、参考链接

    《嵌入式Linux内存与性能详解》
    malloc_chunk边界标记法和空间复用https://blog.csdn.net/sim120/article/details/39373229
    对于GNU编译器中-mthumb-interwork和-mthumb的理解https://blog.csdn.net/moqingxinai2008/article/details/53909051

    来源:https://www.jianshu.com/p/bc61df40d85d

       ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧  END  ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧

    推荐阅读

    【1】jetson nano开发使用的基础详细分享

    【2】Linux开发coredump文件分析实战分享

    【3】CPU中的程序是怎么运行起来的 必读

    【4】cartographer环境建立以及建图测试

    【5】设计模式之简单工厂模式、工厂模式、抽象工厂模式的对比

    本公众号全部原创干货已整理成一个目录,回复[ 资源 ]即可获得。



    羽林君 某嵌入式程序猿分享技术、生活、人生云云文字。如有诗云:去年今日此门中,人面桃花相映红。人面不知何处去,桃花依旧笑春风。
    评论
    • 在物联网领域中,无线射频技术作为设备间通信的核心手段,已深度渗透工业自动化、智慧城市及智能家居等多元场景。然而,随着物联网设备接入规模的不断扩大,如何降低运维成本,提升通信数据的传输速度和响应时间,实现更广泛、更稳定的覆盖已成为当前亟待解决的系统性难题。SoC无线收发模块-RFM25A12在此背景下,华普微创新推出了一款高性能、远距离与高性价比的Sub-GHz无线SoC收发模块RFM25A12,旨在提升射频性能以满足行业中日益增长与复杂的设备互联需求。值得一提的是,RFM25A12还支持Wi-S
      华普微HOPERF 2025-02-28 09:06 143浏览
    • 美国加州CEC能效跟DOE能效有什么区别?CEC/DOE是什么关系?美国加州CEC能效跟DOE能效有什么区别?CEC/DOE是什么关系?‌美国加州CEC能效认证与美国DOE能效认证在多个方面存在显著差异‌。认证范围和适用地区‌CEC能效认证‌:仅适用于在加利福尼亚州销售的电器产品。CEC认证的范围包括制冷设备、房间空调、中央空调、便携式空调、加热器、热水器、游泳池加热器、卫浴配件、光源、应急灯具、交通信号模块、灯具、洗碗机、洗衣机、干衣机、烹饪器具、电机和压缩机、变压器、外置电源、消费类电子设备
      张工nx808593 2025-02-27 18:04 120浏览
    • 更多生命体征指标风靡的背后都只有一个原因:更多人将健康排在人生第一顺位!“AGEs,也就是晚期糖基化终末产物,英文名Advanced Glycation End-products,是存在于我们体内的一种代谢产物” 艾迈斯欧司朗亚太区健康监测高级市场经理王亚琴说道,“相信业内的朋友都会有关注,最近该指标的热度很高,它可以用来评估人的生活方式是否健康。”据悉,AGEs是可穿戴健康监测领域的一个“萌新”指标,近来备受关注。如果站在学术角度来理解它,那么AGEs是在非酶促条件下,蛋白质、氨基酸
      艾迈斯欧司朗 2025-02-27 14:50 400浏览
    • RGB灯光无法同步?细致的动态光效设定反而成为产品客诉来源!随着科技的进步和消费者需求变化,电脑接口设备单一功能性已无法满足市场需求,因此在产品上增加「动态光效」的形式便应运而生,藉此吸引消费者目光。这种RGB灯光效果,不仅能增强电脑周边产品的视觉吸引力,还能为用户提供个性化的体验,展现独特自我风格。如今,笔记本电脑、键盘、鼠标、鼠标垫、耳机、显示器等多种电脑接口设备多数已配备动态光效。这些设备的灯光效果会随着音乐节奏、游戏情节或使用者的设置而变化。想象一个画面,当一名游戏玩家,按下电源开关,整
      百佳泰测试实验室 2025-02-27 14:15 137浏览
    • 在2024年的科技征程中,具身智能的发展已成为全球关注的焦点。从实验室到现实应用,这一领域正以前所未有的速度推进,改写着人类与机器的互动边界。这一年,我们见证了具身智能技术的突破与变革,它不仅落地各行各业,带来新的机遇,更在深刻影响着我们的生活方式和思维方式。随着相关技术的飞速发展,具身智能不再仅仅是一个技术概念,更像是一把神奇的钥匙。身后的众多行业,无论愿意与否,都像是被卷入一场伟大变革浪潮中的船只,注定要被这股汹涌的力量重塑航向。01为什么是具身智能?为什么在中国?最近,中国具身智能行业的进
      艾迈斯欧司朗 2025-02-28 15:45 221浏览
    • Matter 协议,原名 CHIP(Connected Home over IP),是由苹果、谷歌、亚马逊和三星等科技巨头联合ZigBee联盟(现连接标准联盟CSA)共同推出的一套基于IP协议的智能家居连接标准,旨在打破智能家居设备之间的 “语言障碍”,实现真正的互联互通。然而,目标与现实之间总有落差,前期阶段的Matter 协议由于设备支持类型有限、设备生态协同滞后以及设备通信协议割裂等原因,并未能彻底消除智能家居中的“设备孤岛”现象,但随着2025年的到来,这些现象都将得到完美的解决。近期,
      华普微HOPERF 2025-02-27 10:32 214浏览
    • 振动样品磁强计是一种用于测量材料磁性的精密仪器,广泛应用于科研、工业检测等领域。然而,其测量准确度会受到多种因素的影响,下面我们将逐一分析这些因素。一、温度因素温度是影响振动样品磁强计测量准确度的重要因素之一。随着温度的变化,材料的磁性也会发生变化,从而影响测量结果的准确性。因此,在进行磁性测量时,应确保恒温环境,以减少温度波动对测量结果的影响。二、样品制备样品的制备过程同样会影响振动样品磁强计的测量准确度。样品的形状、尺寸和表面处理等因素都会对测量结果产生影响。为了确保测量准确度,应严格按照规
      锦正茂科技 2025-02-28 14:05 134浏览
    • 构建巨量的驾驶场景时,测试ADAS和AD系统面临着巨大挑战,如传统的实验设计(Design of Experiments, DoE)方法难以有效覆盖识别驾驶边缘场景案例,但这些边缘案例恰恰是进一步提升自动驾驶系统性能的关键。一、传统解决方案:静态DoE标准的DoE方案旨在系统性地探索场景的参数空间,从而确保能够实现完全的测试覆盖范围。但在边缘案例,比如暴露在潜在安全风险的场景或是ADAS系统性能极限场景时,DoE方案通常会失效,让我们看一些常见的DoE方案:1、网格搜索法(Grid)实现原理:将
      康谋 2025-02-27 10:00 252浏览
    • 应用趋势与客户需求,AI PC的未来展望随着人工智能(AI)技术的日益成熟,AI PC(人工智能个人电脑)逐渐成为消费者和企业工作中的重要工具。这类产品集成了最新的AI处理器,如NPU、CPU和GPU,并具备许多智能化功能,为用户带来更高效且直观的操作体验。AI PC的目标是提升工作和日常生活的效率,通过深度学习与自然语言处理等技术,实现更流畅的多任务处理、实时翻译、语音助手、图像生成等功能,满足现代用户对生产力和娱乐的双重需求。随着各行各业对数字转型需求的增长,AI PC也开始在各个领域中显示
      百佳泰测试实验室 2025-02-27 14:08 255浏览
    • 请移步 gitee 仓库 https://gitee.com/Newcapec_cn/LiteOS-M_V5.0.2-Release_STM32F103_CubeMX/blob/main/Docs/%E5%9F%BA%E4%BA%8ESTM32F103RCT6%E7%A7%BB%E6%A4%8DLiteOS-M-V5.0.2-Release.md基于STM32F103RCT6移植LiteOS-M-V5.0.2-Release下载源码kernel_liteos_m: OpenHarmon
      逮到一只程序猿 2025-02-27 08:56 195浏览
    •           近日受某专业机构邀请,参加了官方举办的《广东省科技创新条例》宣讲会。在与会之前,作为一名技术工作者一直认为技术的法例都是保密和侵权方面的,而潜意识中感觉法律有束缚创新工作的进行可能。通过一个上午学习新法,对广东省的科技创新有了新的认识。广东是改革的前沿阵地,是科技创新的沃土,企业是创新的主要个体。《广东省科技创新条例》是广东省为促进科技创新、推动高质量发展而制定的地方性法规,主要内容包括: 总则:明确立法目
      广州铁金刚 2025-02-28 10:14 103浏览
    • 2025年2月26日,广州】全球领先的AIoT服务商机智云正式发布“Gokit5 AI智能体开发板”,该产品作为行业首个全栈式AIoT开发中枢,深度融合火山引擎云原生架构、豆包多模态大模型、扣子智能体平台和机智云Aiot开发平台,首次实现智能体开发全流程工业化生产模式。通过「扣子+机智云」双引擎协同架构与API开放生态,开发者仅需半天即可完成智能体开发、测试、发布到硬件应用的全流程,标志着智能体开发进入分钟级响应时代。一、开发框架零代码部署,构建高效开发生态Gokit5 AI智能体开发板采用 “
      机智云物联网 2025-02-26 19:01 162浏览
    • 一、VSM的基本原理震动样品磁强计(Vibrating Sample Magnetometer,简称VSM)是一种灵敏且高效的磁性测量仪器。其基本工作原理是利用震动样品在探测线圈中引起的变化磁场来产生感应电压,这个感应电压与样品的磁矩成正比。因此,通过测量这个感应电压,我们就能够精确地确定样品的磁矩。在VSM中,被测量的样品通常被固定在一个震动头上,并以一定的频率和振幅震动。这种震动在探测线圈中引起了变化的磁通量,从而产生了一个交流电信号。这个信号的幅度和样品的磁矩有着直接的关系。因此,通过仔细
      锦正茂科技 2025-02-28 13:30 100浏览
    • 1,微软下载免费Visual Studio Code2,安装C/C++插件,如果无法直接点击下载, 可以选择手动install from VSIX:ms-vscode.cpptools-1.23.6@win32-x64.vsix3,安装C/C++编译器MniGW (MinGW在 Windows 环境下提供类似于 Unix/Linux 环境下的开发工具,使开发者能够轻松地在 Windows 上编写和编译 C、C++ 等程序.)4,C/C++插件扩展设置中添加Include Path 5,
      黎查 2025-02-28 14:39 140浏览
    •         近日,广电计量在聚焦离子束(FIB)领域编写的专业著作《聚焦离子束:失效分析》正式出版,填补了国内聚焦离子束领域实践性专业书籍的空白,为该领域的技术发展与知识传播提供了重要助力。         随着芯片技术不断发展,芯片的集成度越来越高,结构也日益复杂。这使得传统的失效分析方法面临巨大挑战。FIB技术的出现,为芯片失效分析带来了新的解决方案。它能够在纳米尺度上对芯片进行精确加工和分析。当芯
      广电计量 2025-02-28 09:15 116浏览
    我要评论
    0
    点击右上角,分享到朋友圈 我知道啦
    请使用浏览器分享功能 我知道啦