如何消除结构中未使用的元素?

now*_*wox 5 c embedded gcc arm

背景

我正致力于嵌入式设备的模块化架构,其中不同的抽象层必须彼此通信.当前的方法是拥有大量的函数,变量和定义它们所属的模块名称.

这种方法有点痛苦,我想为API定义某种通用接口.关键的想法是更好地了解哪些模块共享相同的HAL接口.

提案

我想使用OOP启发的架构,我将结构用作接口.所有这些结构都是静态填充的.

这个解决方案看起来不错但我可能会浪费大量内存,因为编译器不知道如何切断结构并只保留它真正需要的内容.

以下示例可以使用或不使用构建,-DDIRECT并且行为应该完全相同.

来源(test.c)

#include <stdlib.h>

int do_foo(int a) {
    return 42 * a;
}

#ifdef DIRECT
int (*foo)(int) = do_foo;
int (*bar)(int);
int storage;
#else
struct foo_ts {
    int (*do_foo)(int);
    int (*do_bar)(int);
    int storage;
} foo = {.do_foo = do_foo};
#endif

int main(char argc) {
    #ifdef DIRECT
    return foo(argc);
    #else
    return foo.do_foo(argc);
    #endif
}
Run Code Online (Sandbox Code Playgroud)

Makefile文件

CFLAGS=-O2 -mthumb -mcpu=cortex-m3 -g --specs=nosys.specs
CC=arm-none-eabi-gcc

upper=$(shell echo $(1) | tr a-z A-Z)

EXEC=direct.out with_struct.out

all: $(EXEC)

%.out:test.c
    $(CC) $(CFLAGS) -D$(call upper,$(basename $@)) -o $@ test.c
    size $@
Run Code Online (Sandbox Code Playgroud)

产量

可以注意到struct变体使用的内存占用量更大,因为编译器不允许自己删除未使用的成员.

arm-none-eabi-gcc -O2 -mthumb -mcpu=cortex-m3 -g \
                  --specs=nosys.specs -DDIRECT -o direct.out test.c
size direct.out
   text    data     bss     dec     hex filename
    992    1092      36    2120     848 direct.out
arm-none-eabi-gcc -O2 -mthumb -mcpu=cortex-m3 -g \
                  --specs=nosys.specs -DWITH_STRUCT -o with_struct.out test.c
size with_struct.out
   text    data     bss     dec     hex filename
    992    1100      28    2120     848 with_struct.out
Run Code Online (Sandbox Code Playgroud)

在这个例子中,我演示了使用结构有利于可读性和模块化,但它可能会降低效率并增加内存使用量.

有没有办法获得两种解决方案的优势?换句话说,有没有办法告诉编译器更聪明?

OOP?

关于这个问题的评论,一个建议是使用C++代替.不幸的是,会出现同样的问题,因为编译器永远不会简化具有未使用成员的类.所以我和两种语言陷入了同样的陷阱.

另一个提出的观点是结构中未使用成员的原因.为了解决这个问题,我们可以想象一个通用的3轴加速度计,用于仅使用1轴的应用中.该加速度计的HAL可以有方法read_x_acc,read_y_accread_z_acc而只有read_x_acc在应用程序使用.

如果我用C++或C语言中的结构声明一个类,那么未使用的方法/函数的函数指针仍会消耗内存.

小智 1

让我首先向您展示一种可能的编辑方法,您当前的界面具有三个功能,但有时您只需要其中一个。您可以定义两个接口:

typedef struct I1DAccel
{
    double (*read)(void);
} I1DAccel;

typedef struct I3DAccel
{
    union
    {
        I1DAccel x;
        struct
        {
            double (*read_x)(void);
        };
    };

    union
    {
        I1DAccel y;
        struct
        {
            double (*read_y)(void);
        };
    };

    union
    {
        I1DAccel z;
        struct
        {
            double (*read_z)(void);
        };
    };
} I3DAccel;
Run Code Online (Sandbox Code Playgroud)

然后您可以使加速度计的实现执行以下操作:

I1DAccel accel_x = { read_x_acc };
I1DAccel accel_y = { read_y_acc };
I1DAccel accel_z = { read_z_acc };
I3DAccel accel =
{
    .read_x = read_x_acc,
    .read_y = read_y_acc,
    .read_z = read_z_acc,
};
Run Code Online (Sandbox Code Playgroud)

通过适当的链接时优化,编译器可以丢弃应用程序代码未使用的任何全局结构。

当然,如果代码的一部分仅需要accel_x另一部分需要整个accel. 您必须手动查找这些案例。


当然,使用“接口” struct,您总是会比没有它们消耗更多的内存,指针必须存储在某个地方。因此,典型的方法确实是将模块名称添加到函数前面并直接调用它们,例如在这样的加速度计的情况下:

typedef int acchndl; // if you need more than one accelerometer

double Accel_read_x(acchndl accel);
double Accel_read_y(acchndl accel);
double Accel_read_z(acchndl accel);
Run Code Online (Sandbox Code Playgroud)

这在概念上类似于您在 C++ 中所做的事情:

class Accel
{
    double read_x();
    double read_y();
    double read_z();
};

double Accel::read_x()
{
    // implementation here
}
Run Code Online (Sandbox Code Playgroud)

使用上面的普通 C,您可以使用任何其他类型的“对象句柄”,而不是实例指针typedef,如to所示int,这通常是嵌入式代码的优势。