嵌入式项目中打造自己的utils库-不同芯片的基础时空开销计算

最近又重新翻看了《编程珠玑》这本书,里面着重对工程化处理进行了讲解,写得很好。印象最深的一个例子,作者拿布鲁克林大桥举例,20世纪40年代前,有很多悬索桥会因为风暴断裂。而要对这种气动上升现象做正确的数学建模和工程计算,要等到10年后。而同一时期的布鲁克林大桥却没有断裂,工程师John Roebling说,他知道气动上升现象的存在,但是又无法从本质上去正确处理它(建模),于是他按照所需强度的6倍设计了大桥。最后作者建议我们,以2、4或6的系数来降低对性能的估计。其实就是要预留足够的性能。作者最后说:“我们是和John Roebling一样的工程师吗?我很怀疑”。
言归正传,书中提到了时空开销模型,用于对代码中时空开销做计算,可以方便的用于代码性能估计和代码优化。本文对此代码做了整理,可以加入到utils库中,并且可在ESP32和STM32F103C8上运行。

基本类型的空间占用

#define SIZE_MEASURE(T) \
do { \
    printf("sizeof(%s)=%d\n", #T, sizeof(T)); \
} while (0)

void platform_base_type_size_measure(void)
{
    printf("\nplatform_base_type_size_measure\n");
    SIZE_MEASURE(char);
    SIZE_MEASURE(short);
    SIZE_MEASURE(int);
    SIZE_MEASURE(long);
    SIZE_MEASURE(long long);
    SIZE_MEASURE(float);
    SIZE_MEASURE(double);
    SIZE_MEASURE(int*);
}

这里用到了前面的知识点,宏定义参数前加#,可以转换为字符串。
结果:

platform_base_type_size_measure
sizeof(char)=1
sizeof(short)=2
sizeof(int)=4
sizeof(long)=4
sizeof(long long)=8
sizeof(float)=4
sizeof(double)=8
sizeof(int*)=4

因为esp32和stm32都是32位平台,所以int是4字节,指针也是4字节。

结构体对齐和malloc占用空间

typedef struct{char c;} structc;
typedef struct {int i; char c;} structic;
typedef struct {int i; struct structip *p;} structip;
typedef struct {double d; char c;} structdc;
typedef struct {char c; double d;} structcd;
typedef struct {char c; double d; char cc;} structcdc;
typedef struct {int i; int ii; int iii;} structiii;
typedef struct {int i; int ii; char c;} structiic;
typedef struct {char c[12];} struct12;
typedef struct {char c[13];} struct13;
typedef struct {char c[28];} struct28;
typedef struct {char c[29];} struct29;

#define SIZE_MALLOC_MEASURE(T) \
do { \
    printf("%s%c\t", #T, strlen(#T)<8?'\t':' '); \
    printf("%d\t", sizeof(T)); \
    int lastp = 0; \
    for(int i=0; i<11; i++) { \
        T* p = (T*)malloc(sizeof(T)); \
        uint32_t thisp = (uint32_t)p; \
        if(lastp != 0) \
            printf(" %d", thisp - lastp); \
        lastp = thisp; \
    } \
    printf("\n"); \
} while (0)

void platform_struct_size_measure(void)
{
    printf("\nstruct\t\tsizeof\t malloc size\n");
    SIZE_MALLOC_MEASURE(int);
    SIZE_MALLOC_MEASURE(structc);
    SIZE_MALLOC_MEASURE(structic);
    SIZE_MALLOC_MEASURE(structip);
    SIZE_MALLOC_MEASURE(structdc);
    SIZE_MALLOC_MEASURE(structcd);
    SIZE_MALLOC_MEASURE(structcdc);
    SIZE_MALLOC_MEASURE(structiii);
    SIZE_MALLOC_MEASURE(structiic);
    SIZE_MALLOC_MEASURE(struct12);
    SIZE_MALLOC_MEASURE(struct13);
    SIZE_MALLOC_MEASURE(struct28);
    SIZE_MALLOC_MEASURE(struct29);
}

命名规则为:c:chart i:int d:double,并且互相拼接。
结果:

struct          sizeof   malloc size
int             4        16 267900 16 16 16 16 16 16 16 16
structc         1        16 16 16 16 16 16 16 16 16 16
structic        8        16 16 16 16 16 16 16 16 16 16
structip        8        16 16 16 16 16 16 16 16 16 16
structdc        16       20 20 20 20 20 524 20 20 20 20
structcd        16       20 20 20 20 20 20 20 20 20 20
structcdc       24       28 28 28 28 28 28 28 28 28 28
structiii       12       16 16 16 16 16 16 16 16 16 16
structiic       12       16 16 16 16 16 16 16 16 16 16
struct12        12       16 16 16 16 16 16 16 16 16 16
struct13        13       20 20 20 20 20 20 20 20 20 20
struct28        28       32 32 32 32 32 32 32 32 32 32
struct29        29       36 36 36 36 36 36 36 36 36 36

可以看到,结构体会进行对齐补全,有double会补齐8字节。所以想在通信代码中通过结构体转换协议,记得使用#pragma pack(1)按1字节对齐。
malloc在申请内存时,也会有多余的空间占用,并且不一定是线性增长,这与malloc的实现方式有关。

整形运算符时间消耗

#define INT_TIME_MEASURE(OP, n) \
do { \
    printf("%s%c\t", #OP, strlen(#OP)<8?'\t':' '); \
    int64_t timesum = 0; \
    for(int ttt=0; ttt<5; ttt++) { \
        int64_t start = esp_timer_get_time(); \
        for(int i=1; i<=n; i++) { \
            for(int j=1; j<=n; j++) { \
                OP; \
            } \
        } \
        int64_t t = esp_timer_get_time() - start; \
        printf("%lld ", t); \
        timesum += t; \
    } \
    printf("\t%lld\n", 1000*timesum / (n*n*5)); \
} while (0)

void platform_int_time_measure(void)
{
    int loop_n = 500;
    printf("\nplatform_int_time_measure (n=%d)\n", loop_n);
    printf("oprate\t\ttime(us)\t\ttime avg(ns)\n");
    volatile int k = 0;
    INT_TIME_MEASURE({}, loop_n);
    INT_TIME_MEASURE(k++, loop_n);
    INT_TIME_MEASURE(k=i+j, loop_n);
    INT_TIME_MEASURE(k=i-j, loop_n);
    INT_TIME_MEASURE(k=i*j, loop_n);
    INT_TIME_MEASURE(k=i/j, loop_n);
    INT_TIME_MEASURE(k=i%j, loop_n);
    INT_TIME_MEASURE(k=i&j, loop_n);
    INT_TIME_MEASURE(k=i|j, loop_n);
}

因为涉及到时间计算,得使用计时函数,这里是ESP32的实现方式。由于运行速度很快,定时器分辨率不高,所以采用多次运行取平均值的方式。
结果:

//ESP32
platform_int_time_measure (n=500)
oprate          time(us)                        time avg(ns)
{}              1 1 1 1 1       0
k++             11473 11475 11471 11471 11471   45
k=i+j           7306 7305 7301 7305 7305        29
k=i-j           7302 7305 7304 7305 7301        29
k=i*j           9390 9390 9390 9390 9390        37
k=i/j           9621 9620 9620 9621 9621        38
k=i%j           10664 10663 10662 10662 10662   42
k=i&j           8347 8346 8346 8346 8343        33
k=i|j           8347 8346 8347 8346 8346        33

//STM32F103C8
platform_int_time_measure (n=500)
oprate        time(us)                         time avg(ns)
{}            27290 27290 27280 27280 27290     109
k++           39150 39150 39150 39150 39150     156
k=i+j        39060 39060 39070 39070 39070     156
k=i-j        35110 35110 35110 35110 35100     140
k=i*j        35180 35180 35180 35180 35180     140
k=i/j        44410 44410 44410 44410 44410     177
k=i%j        63790 63790 63790 63790 63790     255
k=i&j        39060 39070 39070 39060 39060     156
k=i|j        46820 46820 46820 46820 46830     187

ESP32的自增运算比较耗时,有点奇怪。STM32上自增和加法是一样的。
ESP32的除法没有更耗时,STM32的除法是更耗时的。
平时没注意到的是,求余运算非常耗时。与运算也不能说省时间。

浮点运算符时间消耗

#define FLOAT_TIME_MEASURE(OP, n) \
do { \
    printf("%s%c\t", #OP, strlen(#OP)<8?'\t':' '); \
    int64_t timesum = 0; \
    volatile float fi,fj; \
    for(int ttt=0; ttt<5; ttt++) { \
        int64_t start = esp_timer_get_time(); \
        for(int i=1; i<=n; i++) { \
            fi = i; \
            for(int j=1; j<=n; j++) { \
                fj = j; \
                OP; \
            } \
        } \
        int64_t t = esp_timer_get_time() - start; \
        printf("%lld ", t); \
        timesum += t; \
    } \
    printf("\t%lld\n", 1000*timesum / (n*n*5)); \
} while (0)

void platform_float_time_measure(void)
{
    int loop_n = 100;
    printf("\nplatform_float_time_measure (n=%d)\n", loop_n);
    printf("oprate\t\ttime(us)\t\ttime avg(ns)\n");
    volatile float fk = 0;
    FLOAT_TIME_MEASURE({}, loop_n);
    FLOAT_TIME_MEASURE(fk=fi+fj, loop_n);
    FLOAT_TIME_MEASURE(fk=fi-fj, loop_n);
    FLOAT_TIME_MEASURE(fk=fi*fj, loop_n);
    FLOAT_TIME_MEASURE(fk=fi/fj, loop_n);
}

结果:

//ESP32
platform_float_time_measure (n=100)
oprate          time(us)                        time avg(ns)
{}              1685 1683 1683 1683 1683        168
fk=fi+fj        3941 3935 3939 3936 3935        393
fk=fi-fj        4516 4511 4514 4510 4514        451
fk=fi*fj        4293 4291 4295 4291 4295        429
fk=fi/fj        14036 14037 14034 14033 14037   1403

//STM32
platform_float_time_measure (n=100)
oprate        time(us)                         time avg(ns)
{}            6000 6010 6000 6000 6010     600
fk=fi+fj     15670 15680 15680 15680 15680     1567
fk=fi-fj     14760 14760 14760 14760 14760     1476
fk=fi*fj     13920 13920 13920 13920 13920     1392
fk=fi/fj     20790 20790 20790 20790 20790     2079

过程中需要先将整数赋值给浮点,消耗了不少时间。所以需要减去空运行的时间。即使是带FPU的ESP32,依然消耗不少时间。

数组运算符时间消耗

void platform_array_time_measure(void)
{
    int loop_n = 500;
    printf("\nplatform_array_time_measure (n=%d)\n", loop_n);
    printf("oprate\t\ttime(us)\t\ttime avg(ns)\n");
    volatile int k = 0;
    int x[loop_n+1];
    INT_TIME_MEASURE(k=i+j, loop_n);
    INT_TIME_MEASURE(k=x[i]+j, loop_n);
    INT_TIME_MEASURE(k=i+x[j], loop_n);
    INT_TIME_MEASURE(k=x[i]+x[j], loop_n);
}

直接使用整数的宏,结果和整数操作差异不大

platform_array_time_measure (n=500)
oprate          time(us)                        time avg(ns)
k=i+j           7302 7304 7304 7301 7304        29
k=x[i]+j        7313 7313 7309 7312 7313        29
k=i+x[j]        10431 10429 10430 10430 10430   41
k=x[i]+x[j]     10432 10432 10432 10432 10432   41

操作符和数学库时间消耗

void platform_function_time_measure(void)
{
    int loop_n = 100;
    printf("\nplatform_function_time_measure (n=%d)\n", loop_n);
    printf("oprate\t\ttime(us)\t\ttime avg(ns)\n");
    volatile int k = 0;
    volatile float fk = 0;
    INT_TIME_MEASURE(k=(i>j)?i:j, loop_n);
    INT_TIME_MEASURE(rand(), loop_n);
    FLOAT_TIME_MEASURE(fk=sqrt(j+fi), loop_n);
    FLOAT_TIME_MEASURE(fk=sin(j+fi), loop_n);
    // FLOAT_TIME_MEASURE(fk=sinh(j+fi), loop_n);
    // FLOAT_TIME_MEASURE(fk=asin(j+fi), loop_n);
    // FLOAT_TIME_MEASURE(fk=cos(j+fi), loop_n);
    // FLOAT_TIME_MEASURE(fk=tan(j+fi), loop_n);
}

结果:

platform_function_time_measure (n=100)
oprate          time(us)                        time avg(ns)
k=(i>j)?i:j     337 336 336 336 336             33
rand()          3763 3753 3756 3752 3756        375
fk=sqrt(j+fi)   58442 58416 58417 58419 58416   5842
fk=sin(j+fi)    111011 110967 110970 110967 110971      11097

开方没有想象中的耗时,三角函数耗时真的大。如果对精度要求不高,如呼吸灯什么的,强烈建议使用快速三角函数。

malloc时间消耗

#define ALLOC_TIME_MEASURE(OP, n) \
do { \
    printf("%s%c\t", #OP, strlen(#OP)<8?'\t':' '); \
    int64_t timesum = 0; \
    int64_t start = esp_timer_get_time(); \
    for(int i=1; i<=n; i++) { \
        OP; \
    } \
    int64_t t = esp_timer_get_time() - start; \
    printf("%lld ", t); \
    timesum += t; \
    printf("\t\t%lld\n", 1000*timesum / (n)); \
} while (0)

#define ALLOC_FREE_MEASURE(OP, n) \
do { \
    printf("%s%c\t\t", #OP, strlen(#OP)<8?'\t':' '); \
    int64_t timesum = 0; \
    int64_t start = esp_timer_get_time(); \
    for(int i=1; i<=n; i++) { \
        OP; \
    } \
    int64_t t = esp_timer_get_time() - start; \
    printf("%lld ", t); \
    timesum += t; \
    printf("\t\t%lld\n", 1000*timesum / (n)); \
} while (0)

void platform_alloc_time_measure(void)
{
    int loop_n = 100;
    printf("\nplatform_alloc_time_measure (n=%d)\n", loop_n);
    printf("oprate\t\t\ttime(us)\ttime avg(ns)\n");
    volatile int* k[loop_n+1];
    ALLOC_TIME_MEASURE(k[i]=malloc(16), loop_n);
    ALLOC_FREE_MEASURE(free(k[i]), loop_n);
    ALLOC_TIME_MEASURE(k[i]=malloc(100), loop_n);
    ALLOC_FREE_MEASURE(free(k[i]), loop_n);
    ALLOC_TIME_MEASURE(k[i]=malloc(2000), loop_n);
    ALLOC_FREE_MEASURE(free(k[i]), loop_n);
}

结果:

platform_alloc_time_measure (n=100)
oprate                  time(us)        time avg(ns)
k[i]=malloc(16)         317             3170
free(k[i])              195             1950
k[i]=malloc(100)        317             3170
free(k[i])              194             1940
k[i]=malloc(2000)       613             6130
free(k[i])              189             1890

free耗时比较稳定。malloc耗时会随着空间增大而增大,但也不是线性关系。并且这两个的耗时也不能忽略不计。

End

通过这些对比,以前忽视的malloc和free,其实也是时间空间消耗大户。求余运算与运算也值得注意。
有了这些空间和时间消耗的对比,相信大家就可以对自己使用的平台有了更进一步的了解。同时在以后需要优化代码的时候,也能有一个明确的方向。

18 声望
3 粉丝
0 条评论
推荐阅读
float类型中NaN和Inf是什么
做嵌入式的同学应该知道,c语言中int的大小,是和平台有关的,有的占4字节,有的占2字节。所以我们对有期望长度的变量,很少直接用int定义,而更多用uint8_t, uint16_t, uint32_t。但到了float,我们为什么没有fl...

银翼Neal阅读 639

C 程序眼中的 Unicode
去年写了一篇文章「在 C 程序中处理 UTF-8 字符串」,介绍了如何使用 GLib 提供的 UTF-8 字符串处理函数来实现基本的 UTF-8 文本处理。不过,GLib 是一个功能比较全面的 C 程序库,C 字符串处理仅仅是它的一个很...

garfileo3阅读 5.7k评论 5

计算机如何表示整数
在计算机中,任何的数据都是用二进制: 0 和 1 来表示。整数也不例外。生活中的 10,在 8 个字节的整数中表示为 00001010。但是这样子只能表示正数和零。怎么表示负数呢?于是有了符号位的概念。在 8 个字节的整...

kang2阅读 3k评论 7

麒麟操作系统 (kylinos) 从入门到精通 - 研发环境 - 第二十一篇 C++/C语言开发环境搭建
类别:笔记本型号:中国长城 NF14C硬件平台:飞腾处理器(ArmV8 指令集)系统:银河麒麟操作系统 V10 SP1(2203) 关键词:信创,麒麟系统,linux,c++,c,内核飞腾,arm

码上世界1阅读 2.4k评论 1

封面图
浙大版《C语言程序设计》第四版(何钦铭颜晖) 第5章 函数 课后习题答案
你也可以上程序咖([链接]),打开大学幕题板块,不但有答案,讲解,还可以在线答题。一、选择题1.在 C 语言程序中,若对函数类型未加显式说明,则函数的隐含类型为( )。A. voidB. double C. charD. int答:D解析...

茹茹1阅读 1k

(持续更新,已更新至2022年11月26日)C语言经典题集合
(持续更新,最新时间2022年11月26日)1. 三个数由小到大排序输入任意3个整数,编程实现对这3个整数进行由小到大排序井将排序后的结果显示在屏幕上 {代码...} 2. a²+b²要求输入整数a和 b, 若a²+b²的结果大与100, ...

瞿小凯阅读 1.4k评论 2

封面图
看门狗
看门狗的本质是一个定时器,在启动后,需要在一定时间内再给它一个信号,俗称“喂狗”,如果没有按时“喂狗”,说明MCU可能处于非正常状态,这时看门狗就向MCU发送个复位信号,使整个系统重启,重新进入正常的工作状...

十二楼主阅读 973

18 声望
3 粉丝
宣传栏