有什么方法可以"factor out"公共字段以节省空间?

Any way to "factor out" common fields to save space?

本文关键字:字段 节省 空间 factor 什么 方法 out      更新时间:2023-10-16

我有一个大数组(>百万)的Item,其中每个Item的形式为:

struct Item { void *a; size_t b; };

有几个不同的a字段,这意味着有许多项目具有相同的a字段。

我想"分解"这些信息以节省大约 50% 的内存使用量。

但是,麻烦的是这些Item具有重要的顺序,并且可能会随着时间的推移而改变。因此,我不能继续为每个不同的a单独Item[],因为这将失去项目相对于彼此的相对顺序。

另一方面,如果我将所有项目的顺序存储在size_t index;字段中,则删除void *a;字段会丢失任何节省的内存。

那么有没有办法让我在这里真正节省内存,或者没有?

(注意:我已经可以想到例如使用unsigned chara索引到一个小数组中,但我想知道是否有更好的方法。这将要求我要么使用未对齐的内存,要么将每个Item[]分成两部分,这对于内存局部性来说不是很好,所以我更喜欢其他东西。

(注意:我已经可以想到例如使用无符号字符作为 a 索引到一个小数组中,但我想知道是否有更好的方法。

这种想法是正确的,但事情并没有那么简单,因为您会遇到一些令人讨厌的对齐/填充问题,这些问题会抵消您的记忆增益。

此时,当您开始尝试抓取此类结构的最后几个字节时,您可能希望使用位字段。

#define A_INDEX_BITS 3
struct Item { 
size_t a_index : A_INDEX_BITS; 
size_t b       : (sizeof(size_t) * CHAR_BIT) - A_INDEX_BITS; 
};

请注意,这将限制可用于b的位数,但在sizeof(size_t)为 8 的现代平台上,从中剥离 3-4 位很少是一个问题。

使用轻量级压缩方案的组合(有关示例和一些参考,请参阅此处)来表示a*值。 例如,@Frank的答案采用 DICT 后跟 NS。如果你有相同指针的长时间运行,你可以考虑在此之上的RLE(运行长度编码)。

这有点黑客,但我过去曾使用它并取得了一些成功。对象访问的额外开销通过显著的内存减少得到补偿。

一个典型的用例是这样的环境:(a) 值实际上是具有有限数量的不同类型的可区分联合(即,它们包括类型指示符),并且 (b) 值主要保存在大型连续向量中。

在这种环境下,(某些类型)值的有效负载部分很可能会用完为其分配的所有位。数据类型也有可能要求(或受益于)存储在对齐的内存中。

在实践中,既然大多数主流 CPU 不需要对齐访问,我只会使用打包结构而不是以下技巧。如果您不为未对齐的访问付费,那么将 { 一个字节类型 + 八个字节值 } 存储为九个连续字节可能是最佳的;唯一的代价是您需要乘以 9 而不是 8 才能进行索引访问,这很简单,因为 9 是编译时常量。

如果您确实必须为未对齐的访问权限付费,则可以执行以下操作。"增强"值的向量具有以下类型:

// Assume that Payload has already been typedef'd. In my application,
// it would be a union of, eg., uint64_t, int64_t, double, pointer, etc.
// In your application, it would be b.
// Eight-byte payload version:
typedef struct Chunk8 { uint8_t kind[8]; Payload value[8]; }
// Four-byte payload version:
typedef struct Chunk4 { uint8_t kind[4]; Payload value[4]; }

然后向量是块的向量。要使黑客起作用,必须在 8(或 4)字节对齐的内存地址上分配它们,但我们已经假设有效负载类型需要对齐。

黑客的关键在于我们如何表示指向单个值的指针,因为该值在内存中不是连续的。我们使用指向其kind成员的指针作为代理:

typedef uint8_t ValuePointer;

然后使用以下低但不为零的开销函数:

#define P_SIZE 8U
#define P_MASK P_SIZE - 1U
// Internal function used to get the low-order bits of a ValuePointer.
static inline size_t vpMask(ValuePointer vp) {
return (uintptr_t)vp & P_MASK;
}
// Getters / setters. This version returns the address so it can be
// used both as a getter and a setter
static inline uint8_t* kindOf(ValuePointer vp) { return vp; }
static inline Payload* valueOf(ValuePointer vp) {
return (Payload*)(vp + 1 + (vpMask(vp) + 1) * (P_SIZE - 1));
}
// Increment / Decrement
static inline ValuePointer inc(ValuePointer vp) {
return vpMask(++vp) ? vp : vp + P_SIZE * P_SIZE;
}
static inline ValuePointer dec(ValuePointer vp) {
return vpMask(vp--) ? vp - P_SIZE * P_SIZE : vp;
}
// Simple indexed access from a Chunk pointer
static inline ValuePointer eltk(Chunk* ch, size_t k) {
return &ch[k / P_SIZE].kind[k % P_SIZE];
}
// Increment a value pointer by an arbitrary (non-negative) amount
static inline ValuePointer inck(ValuePointer vp, size_t k) {
size_t off = vpMask(vp);
return eltk((Chunk*)(vp - off), k + off);
}

我省略了一堆其他黑客,但我相信你可以弄清楚它们。

交错值的各个部分的一个很酷的事情是它具有适度的参考位置。对于 8 字节版本,几乎一半的时间随机访问一个类型和一个值只会命中一个 64 字节的缓存行;其余时间是命中两个连续的缓存行,结果是向前(或向后)遍历一个向量与遍历普通向量一样对缓存友好,只是它使用的缓存行更少,因为对象的大小只有一半。四字节版本甚至对缓存更友好。

我想我自己找到了信息理论上的最佳方法来做到这一点......就我而言,这并不值得,但我会在这里解释它,以防它帮助其他人。
但是,它需要未对齐的记忆(在某种意义上)。
也许更重要的是,你失去了轻松动态添加新值的能力a

这里真正重要的是不同Item的数量,即不同(a,b)对的数量。毕竟,对于一个a来说,可能有十亿个不同的b,但对于其他只有少数,所以你想利用这一点。

如果我们假设有N不同的项目可供选择,那么我们需要n = ceil(log2(N))位来表示每个Item.因此,我们真正想要的是一个n位整数数组,n运行时计算。然后,一旦你得到n位整数,你可以根据你对每个abs 计数的了解,在log(n)时间内进行二叉搜索,以确定它对应于哪个a。(这可能会对性能造成一些影响,但这取决于不同a的数量。

你不能以一种很好的记忆对齐方式做到这一点,但这还不错。你要做的是创建一个uint_vector的数据结构,每个元素的位数是一个动态可指定的数量。然后,要随机访问它,您需要执行一些除法或模组操作以及位移来提取所需的整数。

这里需要注意的是,除以变量可能会严重损害您的随机访问性能(尽管它仍然是 O(1))。缓解这种情况的方法可能是为n的共同值编写几个不同的过程(C++模板在这里有所帮助!),然后使用各种if (n == 33) { handle_case<33>(i); }switch (n) { case 33: handle_case<33>(i); }等分支到它们中,以便编译器将除数视为常量并根据需要生成移位/加/乘,而不是除法。

只要每个元素需要恒定的位数,这在信息理论上是最优的,这是您想要的随机访问。但是,如果您放宽该约束,则可以做得更好:您可以将多个整数打包为k * n位,然后用更多的数学运算提取它们。这可能也会降低性能。

(或者,长话短说:C 和 C++ 真的需要一个高性能的uint_vector数据结构......

结构数组方法可能会有所帮助。也就是说,有三个向量...

vector<A> vec_a;
vector<B> vec_b;
SomeType  b_to_a_map;

您以以下身份访问您的数据...

Item Get(int index)
{
Item retval;
retval.a = vec_a[b_to_a_map[index]];
retval.b = vec_b[index];
return retval;
}

现在您需要做的就是为SomeType选择一些明智的东西。例如,如果 vec_a.size() 为 2,则可以使用 vector或 boost::d ynamic_bitset。对于更复杂的情况,您可以尝试位打包,例如支持 A 的 4 值,我们简单地用...

int a_index = b_to_a_map[index*2]*2 + b_to_a_map[index*2+1];
retval.a = vec_a[a_index];

您始终可以通过使用范围打包来击败位打包,使用div/mod 存储每个项目的分数位长度,但复杂性会迅速增加。

一个很好的指南可以在这里找到 http://number-none.com/product/Packing%20Integers/index.html