《嵌入式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】设计模式之简单工厂模式、工厂模式、抽象工厂模式的对比

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



    羽林君 某嵌入式程序猿分享技术、生活、人生云云文字。如有诗云:去年今日此门中,人面桃花相映红。人面不知何处去,桃花依旧笑春风。
    评论 (0)
    •   无人机结构仿真与部件拆解分析系统平台解析   北京华盛恒辉无人机结构仿真与部件拆解分析系统无人机技术快速发展的当下,结构仿真与部件拆解分析系统平台成为无人机研发测试的核心工具,在优化设计、提升性能、降低成本等方面发挥关键作用。以下从功能、架构、应用、优势及趋势展开解析。   应用案例   目前,已有多个无人机结构仿真与部件拆解分析系统在实际应用中取得了显著成效。例如,北京华盛恒辉和北京五木恒润无人机结构仿真与部件拆解分析系统。这些成功案例为无人机结构仿真与部件拆解分析系统的推广和应用提
      华盛恒辉l58ll334744 2025-04-23 15:00 218浏览
    • 2025-4-25全球信息报告出版商Global Info Research(环洋市场咨询)发布了【2025年全球市场高介电常数材料总体规模、主要生产商、主要地区、产品和应用细分研究报告】,报告主要调研全球高介电常数材料总体规模、主要地区规模、主要生产商规模和份额、产品分类规模、下游主要应用规模以及未来发展前景预测。统计维度包括销量、价格、收入,和市场份额。同时也重点分析全球市场主要厂商(品牌)产品特点、产品规格、价格、销量、销售收入及发展动态。历史数据为2020至2024年,预测数据为2025
      用户1745398400862 2025-04-25 08:48 46浏览
    •   后勤实验仿真系统平台深度解析   北京华盛恒辉后勤实验仿真系统平台依托计算机仿真技术,是对后勤保障全流程进行模拟、分析与优化的综合性工具。通过搭建虚拟场景,模拟资源调配、物资运输等环节,为后勤决策提供数据支撑,广泛应用于军事、应急管理等领域。   应用案例   目前,已有多个后勤实验仿真系统平台在实际应用中取得了显著成效。例如,北京华盛恒辉和北京五木恒润后勤实验仿真系统平台。这些成功案例为后勤实验仿真系统平台的推广和应用提供了有力支持。   一、核心功能   (一)后勤资源模拟
      华盛恒辉l58ll334744 2025-04-23 15:39 188浏览
    •   有效样本分析决策系统平台全面解析   一、引言   北京华盛恒辉有效样本分析决策系统在当今数据驱动的时代,企业、科研机构等面临着海量数据的处理与分析挑战。有效样本分析决策系统平台应运而生,它通过对样本数据的精准分析,为决策提供有力支持,成为提升决策质量和效率的关键工具。   应用案例   目前,已有多个有效样本分析决策系统在实际应用中取得了显著成效。例如,北京华盛恒辉和北京五木恒润有效样本分析决策系统。这些成功案例为有效样本分析决策系统的推广和应用提供了有力支持。   二、平台概述
      华盛恒辉l58ll334744 2025-04-24 11:13 117浏览
    • 为通过金融手段积极推进全球绿色发展,国际金融论坛(IFF)于2020年创立了“IFF全球绿色金融奖”,旨在对全球绿色金融领域取得突出成绩的机构及创新性的解决方案进行表彰和奖励。该奖项依托IFF“高层次、高水平、国际化”一流智库资源优势,积极促进绿色金融领域的国际交流合作和创新实践,助力联合国可持续发展目标的实现。“IFF全球绿色金融奖”重点关注和鼓励那些促进经济增长模式转型、防治环境污染、应对气候变化,以及致力于提高能效水平、强化节能减排实效的绿色金融创新解决方案。该奖项面向全球,是对政策创新、
      华尔街科技眼 2025-04-24 20:43 20浏览
    •   海上训练与保障调度指挥平台系统解析   北京华盛恒辉海上训练与保障调度指挥平台系统是现代海上作战训练的核心枢纽,融合信息技术、GIS、大数据及 AI 等前沿技术,旨在实现海上训练高效组织、作战保障科学决策。以下从架构功能、应用场景、系统优势及发展挑战展开解读。   应用案例   目前,已有多个海上训练与保障调度指挥平台在实际应用中取得了显著成效。例如,北京华盛恒辉和北京五木恒润海上训练与保障调度指挥平台。这些成功案例为海上训练与保障调度指挥平台的推广和应用提供了有力支持。   一
      华盛恒辉l58ll334744 2025-04-24 15:26 130浏览
    •   高海拔区域勤务与装备保障调度系统平台解析   北京华盛恒辉高海拔区域勤务与装备保障调度系统平台专为高海拔特殊地理环境打造,致力于攻克装备适应、人员健康保障、物资运输及应急响应等难题。以下从核心功能、技术特点、应用场景及发展趋势展开全面解读。   应用案例   目前,已有多个高海拔区域勤务与装备保障调度系统在实际应用中取得了显著成效。例如,北京华盛恒辉和北京五木恒润高海拔区域勤务与装备保障调度系统。这些成功案例为高海拔区域勤务与装备保障调度系统的推广和应用提供了有力支持。   一、核心
      华盛恒辉l58ll334744 2025-04-24 10:13 120浏览
    • 最近,途虎养车发布的2024年财报数据,可谓相当吸睛。全年营收达到147.59亿元,同比增长8.5%,这个数字直观地展现了途虎在市场上的强大吸金能力,在行业里稳稳占据前列。利润方面同样出色,毛利37.46亿元,毛利率提升0.7个百分点至25.4%;经调整净利润6.24亿元,同比增长 29.7%,经营利润同比更是增长104%至3.31亿元,盈利能力显著增强,这样的利润增长幅度,在同行业中十分亮眼。在用户规模上,途虎养车同样成绩斐然。累计注册用户近1.4亿,同比增长20.4%,交易用户数达2410万
      用户1742991715177 2025-04-24 19:12 31浏览
    •   陆地装备体系论证与评估综合平台系统解析   北京华盛恒辉陆地装备体系论证与评估综合平台系统是契合现代军事需求而生的专业系统,借助科学化、智能化手段,实现对陆地装备体系的全方位论证与评估,为军事决策和装备发展提供关键支撑。以下从功能、技术、应用及展望展开分析。   应用案例   目前,已有多个陆地装备体系论证与评估综合平台在实际应用中取得了显著成效。例如,北京华盛恒辉和北京五木恒润陆地装备体系论证与评估综合平台。这些成功案例为陆地装备体系论证与评估综合平台的推广和应用提供了有力支持。
      华盛恒辉l58ll334744 2025-04-24 10:53 125浏览
    • 随着轻薄笔记本的普及,再加上电竞玩家对于高画质音视频体验的需求日益高涨,如何让轻薄笔记本在兼顾轻便携带性的同时,还能提供足以支持3A(AAA/Triple-A game)大作的良好运算性能,便成为各家品牌急欲突破的共同难题。然而,对于主打轻巧便携的轻薄笔记本而言,若要内置独立显卡,势必要先突破空间受限的瓶颈,同时还需解决散热问题,确实难以兼顾两全!对此,“Thunderbolt”与“OCuLink”这两项技术应运而生。用户可以通过这两种传输接口,再搭配外接显卡盒(eGPU)及高性能显卡(如NVI
      百佳泰测试实验室 2025-04-24 17:56 35浏览
    •   通用装备论证与评估系统平台解析   北京华盛恒辉通用装备论证与评估系统平台是服务军事装备全生命周期管理的综合性信息化平台,通过科学化、系统化手段,实现装备需求论证、效能分析等核心功能,提升装备建设效益。   应用案例   目前,已有多个通用装备论证与评估系统在实际应用中取得了显著成效。例如,北京华盛恒辉和北京五木恒润通用装备论证与评估系统。这些成功案例为通用装备论证与评估系统的推广和应用提供了有力支持。   一、系统分层架构   (一)数据层   整合装备性能、作战、试验等多源异
      华盛恒辉l58ll334744 2025-04-24 16:14 141浏览
    • 引言:语音交互的智能化跃迁在全球化与智能化深度融合的今天,语音交互设备的应用场景已从单一提示功能向多语言支持、情感化表达及AI深度交互演进。传统离线语音方案受限于语种单一、存储容量不足等问题,而纯在线方案又依赖网络稳定性,难以满足复杂场景需求。WT3000A离在线TTS方案,通过“本地+云端”双引擎驱动,集成16国语种、7种方言切换、AI大模型对话扩展等创新功能,重新定义语音提示器的边界,为智能硬件开发者提供更灵活、更具竞争力的语音交互解决方案。一、方案核心亮点离在线双模融合,场景全覆盖离线模式
      广州唯创电子 2025-04-25 09:14 41浏览
    • 引言在智能语音技术飞速发展的今天,语音交互已成为消费电子、智能家居、工业控制等领域的标配功能。传统的ISD系列录音芯片虽应用广泛,但其高成本与功能局限性逐渐难以满足市场对高性价比、高灵活性的需求。推出的WT2000P录音语音芯片,凭借其卓越性能、低功耗设计及高度可定制化特性,成为ISD系列芯片的理想替代方案,助力开发者突破产品创新瓶颈。一、WT2000P产品概述WT2000P是一款专为嵌入式语音场景设计的多功能录音芯片,采用ESOP8封装,体积小巧(尺寸仅4.9mm×3.9mm),集成度高,支持
      广州唯创电子 2025-04-25 08:44 30浏览
    •   航空兵训练与战术对抗仿真平台系统解析   北京华盛恒辉航空兵训练与战术对抗仿真平台系统是现代军事训练的关键工具,借助计算机技术构建虚拟战场,支持多兵种协同作战模拟,为军事决策、训练及装备研发提供科学依据。   应用案例   目前,已有多个航空兵训练与战术对抗仿真平台在实际应用中取得了显著成效。例如,北京华盛恒辉和北京五木恒润航空兵训练与战术对抗仿真平台。这些成功案例为航空兵训练与战术对抗仿真平台的推广和应用提供了有力支持。   一、系统架构与核心功能   系统由模拟器、计算机兵力生
      华盛恒辉l58ll334744 2025-04-24 16:34 149浏览
    •   电磁频谱数据综合管理平台系统解析   一、系统定义与目标   北京华盛恒辉电磁频谱数据综合管理平台融合无线传感器、软件定义电台等前沿技术,是实现无线电频谱资源全流程管理的复杂系统。其核心目标包括:优化频谱资源配置,满足多元通信需求;运用动态管理与频谱共享技术,提升资源利用效率;强化频谱安全监管,杜绝非法占用与干扰;为电子战提供频谱监测分析支持,辅助作战决策。   应用案例   目前,已有多个电磁频谱数据综合管理平台在实际应用中取得了显著成效。例如,北京华盛恒辉和北京五木恒润电磁频谱数
      华盛恒辉l58ll334744 2025-04-23 16:27 212浏览
    我要评论
    0
    0
    点击右上角,分享到朋友圈 我知道啦
    请使用浏览器分享功能 我知道啦