【问题标题】:How to minimize the memory usage of a struct-type?如何最小化结构类型的内存使用?
【发布时间】:2011-06-06 18:12:57
【问题描述】:

用于连接四的转置表(通常是哈希表) 游戏,我想有效地使用内存(尽可能存储 元素的数量)。一个表格元素必须存储以下信息:

  • 锁定:无符号 64 位
  • 移动:[0..6] --> 无符号 3 位
  • 分数:[-2000..2000] --> 12 位签名
  • 标志:有效,UBOUND,LBOUND:--> 无符号 2 位
  • 高度:[-1..42]:--> 带符号的 7 位

首先我尝试了以下数据结构,它需要 24 个字节:

struct TableEntry1
{
    unsigned __int64 lock;
    unsigned char move;
    short score;
    enum { VALID, UBOUND, LBOUND } flag;
    char height;
};

重新排列元素后,它需要 16 个字节(我为此找到了 answer 行为):

struct TableEntry2
{
    unsigned __int64 lock;
    enum { VALID, UBOUND, LBOUND } flag;
    short score;
    char height;
    unsigned char move;
};

我最后一次尝试是:

struct TableEntry3
{
    unsigned __int64 lock;
    unsigned int move:3;
    int score:12;
    enum { VALID, UBOUND, LBOUND } flag:2;
    int height:7;
};

这也需要 16 个字节。是否可以改变结构,使其 只使用 12 字节(在 32 位架构上)?为什么编译器不让我最后一次尝试 12 字节长?

谢谢!

编辑属性lock是用于检测哈希冲突的唯一元素ID。

【问题讨论】:

  • 对于转置表,重视速度而不是内存效率不是更有意义。为什么不正确对齐结构,而不是保存最后 4 个字节?我估计这会给您带来更好的整体性能。
  • @TommyA:转置表中可用的 Connect-Four-Game-Representations 越多,需要评估的 Game-Representations 就越少。评估成本更高。
  • 无需优化即可获得最佳包装和最佳性能。只需按大小顺序列出成员。从最大到最小。然后编译器将添加最小峰值。 (经验法则)。
  • 如果你强制优化太多,这可能对这个架构没问题,但是当你明年升级到 64 位 Windows 机器时,你可能会发现所有工作都浪费了,因为最小可寻址块是 64 位因此任何小于 8 字节的内容都会被填充。

标签: c++ c memory-management alignment


【解决方案1】:

是的,因为您只有 88 位信息,所以可以将其打包成 96 位(12 字节);但是,你真的需要吗?在极端情况下,请记住这种打包会降低运行时性能。

如果您将数以百万计的这些数据存储到磁盘上,那么早先考虑微小的效率会更有意义,但这里是这种情况吗?你见过内存使用的问题吗?您需要多少内存,目前有 16 个字节的对象,这与您计划的限制有多接近?在不回答最后两个问题的情况下尝试优化运行时内存使用是premature

除此之外,我怀疑您的编译器在结构的末尾进行了填充,因此 __int64 始终在 8 字节边界上对齐。考虑一个长度为 2 的数组:大小为 12 字节,最多有一个 __int64 子对象可以是 8 字节对齐的。

【讨论】:

  • 当然,磁盘通常也比 RAM 大,所以它确实必须是数以亿计的数——它们之间的差异太大了,这里就不赘述了。更大的问题可能是磁盘/存储结构更加永久,而 RAM/工作结构可能更加灵活。
  • 磁盘未使用。我看到了最小化大小的两个优点:将大小从 24 字节减小到 12 字节,可以在转置表中存储两倍多的元素(我还使用连接四游戏的对称属性来减少表元素)。如果无法通过转置表获得 Connect-Four-Board-Representation,则必须对其进行昂贵的评估。其次,我认为程序变得更快是因为更多的元素可以保存在快速访问的 cpu 缓存中。
  • @Christian:减少内存使用总是 听起来是个好主意,但是您是否发现内存使用存在问题?您当前的使用(16 字节大小)与您的计划限制有多接近?如果答案是“否”和“远低于”,那么假设计划的限制不需要修改,您要解决什么问题?
  • @Christian:将数据放入缓存并不总是能提高速度:打包和解包这些位域可能会很慢,,而且会导致代码大小增加,这可能会潜在地导致更多的缓存未命中。
【解决方案2】:

您可以使用非标准构造(例如 Visual Studio #pragma pack)达到 12 个字节:

#pragma pack(1)
struct TableEntry3
{
    unsigned __int64 lock;
    unsigned int move:3;
    int score:12;
    enum { VALID, UBOUND, LBOUND } flag:2;
    int height:7;
};

这里,sizeof(TableEntry3) 为我生成 12。

【讨论】:

  • 此编译指示也可用于 GCC,以防您使用它。
  • 当然以使lock 不对齐为代价。为避免这种情况,lock 可以存储在与其余数据不同的表中。
  • 一般来说,锁必须对齐才能正常工作 - 当您尝试使用它跨越缓存行时,原子比较和交换往往会中断。 :)
  • Borland (Embarcardero C++ Builder) 也支持#pragma pack 和英特尔一样(显然):)
  • 在GCC中,推荐使用__attribute__((aligned(1)))属性指定全打包,见gcc.gnu.org/onlinedocs/gcc-3.2.3/gcc/Type-Attributes.html
【解决方案3】:

根据您实现锁的方式,可能会减小锁字段的大小(我假设这是典型的 SMP 锁 SMP 机器)

一种选择是减少锁的数量。也就是说,拥有一个单独较小的锁数组,并在此处锁定从您的数组元素派生的元素。也就是说,如果您正在访问转置表元素t,请使用锁定t % locktablesize。锁定表不必几乎与转置表一样大。

使用这种方法和您的 TableEntry2 字段顺序,并假设您的锁定表是转置表大小的一半(这可能比必要的大),您可以减少到 12 个字节 而不会因以下原因而损失性能位移操作 - 这种性能损失可能非常显着,因此能够避免它总是有帮助的。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多