首页 > 解决方案 > 使用结构最有效的方法是什么?

问题描述

我有以下结构和结构数组,如下所示。

typedef struct {
    uint8_t configuration,
    uint8_t count_1, count_2, count_3, count_4;
} *zonePtr, zoneT;

typedef enum {
    CONFIG_1,
    CONFIG_2,
    CONFIG_3,
    CONFIG_4
} config_t;

const zoneT zones[5] = {
    { CONFIG_1, 1, 2, 0, 0 },
    { CONFIG_1, 3, 5, 0, 0 },
    { CONFIG_2, 2, 0, 0, 0 },
    { CONFIG_4, 2, 6, 7, 3 },
    { CONFIG_3, 1, 2, 3, 4 },
};

在 zone 数组中,我们可以看到一些count_x成员已经初始化为 0,这意味着它们没有被使用,这只是浪费了这些额外的字节。仅使用那些count_x已初始化为 0 以外的任何值的成员。如果我们有一个大小为 100 的大数组,那么这种保存配置数据的方式将导致大量内存被浪费。

我的问题是,在这种情况下,执行上述操作并防止浪费空间的最有效但最灵活的方法是什么?

标签: c

解决方案


通常,旨在加快代码速度的优化是主要关注点,而不是在这里和那里减少几个字节的 RAM 使用。您对浪费大约 100 个字节的担忧在 PC 等中高端计算机上根本没有意义。主要是非常低端的微控制器应用程序需要到处寻找杂散的 RAM 字节。

再仔细一看,const zoneT zones[5] = ...居然看起来像一个只读常量。因此,这甚至不会出现在资源适度受限的堆栈中,而是出现在一些只读链接器部分.rodata中。现在,即使是最糟糕的 8 位 MCU 也不再需要担心,因为这最终会出现在闪存中,而不是 RAM。

所以不管你怎么说,这听起来像是大时代的“过早优化”。但是让我们忽略这一点,看看如果我们要优化它,我们如何才能真正改进代码......:


好的,假设我们出于某种原因决定此代码是内存和/或速度的瓶颈。我们不关心可读性或可维护性,我们只想要原始程序的效率。

这里的主要问题是您创建了一个 5 字节的结构。这很糟糕,计算机讨厌所有不是 2 的倍数的东西。它们特别倾向于喜欢 4 字节的块,尤其是 32 位 CPU。出于这个原因,一些编译器可能会决定在结构中加入填充字节,使其变大 8 个字节。当所有成员都存在时不太可能,char但在以下情况下极有可能typedef struct { uint8_t configuration; int foo; }

这意味着任何声音优化都应该专注于将结构的大小减少到 4 个字节。例如,您可能会问自己是否真的需要每个“计数”来涵盖 0-255 的值。或者我们可以使用 0-127 吗?

然后,我们可以通过让每个 7 位计数值位于它当前所在的位置,但滥用每个字节的 MSB 来存储配置,来创建一个相当棘手但非常快速的位域。

首先提出一些位掩码:

#define CONFIG_1 0x80000000u
#define CONFIG_2 0x00800000u
#define CONFIG_3 0x00008000u
#define CONFIG_4 0x00000080u
#define CONFIG_MASK (CONFIG_1 | CONFIG_2 | CONFIG_3 | CONFIG_4)

现在,假设小端,我们可以像这样初始化整个东西:

const uint32_t zone [5] = 
{ 
  CONFIG_1 | (1u << 0 | 2u << 8 | 0 << 16 | 0 << 24),
  CONFIG_1 | (3u << 0 | 5u << 8 | 0 << 16 | 0 << 24),
  CONFIG_2 | (2u << 0 | 0u << 8 | 0 << 16 | 0 << 24),
  CONFIG_4 | (2u << 0 | 6u << 8 | 7 << 16 | 3 << 24),
  CONFIG_3 | (1u << 0 | 2u << 8 | 3 << 16 | 4 << 24)
};

这在任何通用计算机上都针对速度和内存进行了优化。漂亮吗?不,这太可怕了。但有效率。完整代码示例:

#include <stdint.h>
#include <stdio.h>

#define CONFIG_1 0x80000000u
#define CONFIG_2 0x00800000u
#define CONFIG_3 0x00008000u
#define CONFIG_4 0x00000080u
#define CONFIG_MASK (CONFIG_1 | CONFIG_2 | CONFIG_3 | CONFIG_4)

#define ZONE_VALUE_MASK 0x7Fu

const uint32_t zone [5] = 
{ 
  CONFIG_1 | (1u << 0 | 2u << 8 | 0 << 16 | 0 << 24),
  CONFIG_1 | (3u << 0 | 5u << 8 | 0 << 16 | 0 << 24),
  CONFIG_2 | (2u << 0 | 0u << 8 | 0 << 16 | 0 << 24),
  CONFIG_4 | (2u << 0 | 6u << 8 | 7 << 16 | 3 << 24),
  CONFIG_3 | (1u << 0 | 2u << 8 | 3 << 16 | 4 << 24)
};

const char* get_config (uint32_t u32)
{
  switch(u32 & CONFIG_MASK)
  {
    case CONFIG_1: return "CONFIG_1"; 
    case CONFIG_2: return "CONFIG_2"; 
    case CONFIG_3: return "CONFIG_3"; 
    case CONFIG_4: return "CONFIG_4"; 
  }
}

int main (void)
{
  for(size_t i=0; i<5; i++)
  {
    printf("%s ", get_config(zone[i]));
    
    const uint8_t* byte = (uint8_t*)&zone[i];
    for(size_t j=0; j<sizeof(zone[i]); j++)
    {
      printf("%d ", byte[j] & ZONE_VALUE_MASK);
    }
    printf("\n");
  }
}

输出

CONFIG_1 1 2 0 0 
CONFIG_1 3 5 0 0 
CONFIG_2 2 0 0 0 
CONFIG_4 2 6 7 3 
CONFIG_3 1 2 3 4 

现在这个例子中的字符串处理函数只是一些“又快又脏”的函数,而且printf显然非常慢。但实际的数据迭代速度非常快,内存效率高且缓存友好。

(因为它比结构更便携,除了初始化期间提到的字节顺序问题。我们也可以通过交换uint32_t联合来解决这个问题,但这是另一个故事,与性能无关。)


推荐阅读