这是一种有些hack的方法,但我过去曾经用过并取得了一定的成功。对象访问的额外开销被显著的内存减少所补偿。
典型的使用场景是在一个环境中,(a) 值实际上是带有限个不同类型的类型指示器的歧义联合体,并且 (b) 值大多数情况下保留在大的连续向量中。
在这种环境下,某些值的有效负载部分很可能使用了为其分配的所有位。可能还需要将数据类型存储在对齐的内存中。
实际上,现在大多数主流CPU不再需要对齐访问,我会使用一个紧密结构代替以下hack。如果您不需要支付非对齐访问的费用,则将{一个字节类型 + 八字节值}作为九个连续字节存储可能是最优的;唯一的成本是您需要将索引访问乘以 9 而不是8,因为9是编译时常量,这是微不足道的。
如果您必须支付非对齐访问的费用,则可以尝试以下方法。 "增强"值的向量具有以下类型:
typedef struct Chunk8 { uint8_t kind[8]; Payload value[8]; }
typedef struct Chunk4 { uint8_t kind[4]; Payload value[4]; }
向量是块的向量。为了使hack起作用,它们必须在8字节(或4字节)对齐的内存地址上分配,但我们已经假定对于Payload类型需要对齐。
这个hack的关键是如何表示指向单个值的指针,因为该值在内存中不是连续的。我们使用指向其“kind”成员的指针作为代理:
typedef uint8_t ValuePointer;
然后使用以下低开销但不是零开销的函数:
#define P_SIZE 8U
#define P_MASK P_SIZE - 1U
static inline size_t vpMask(ValuePointer vp) {
return (uintptr_t)vp & P_MASK;
}
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));
}
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;
}
static inline ValuePointer eltk(Chunk* ch, size_t k) {
return &ch[k / P_SIZE].kind[k % P_SIZE];
}
static inline ValuePointer inck(ValuePointer vp, size_t k) {
size_t off = vpMask(vp);
return eltk((Chunk*)(vp - off), k + off);
}
我省略了很多其他的技巧,但我相信你可以自行理解。
通过交错存储值的各个部分,一个很酷的事情是它具有中等良好的局部性。对于8字节版本,几乎一半的时间,对某种类型和值的随机访问仅会命中一个64字节缓存线;其余时间会命中两个连续的缓存线,结果是向前(或向后)遍历向量与遍历普通向量一样友好,只不过因为对象大小减半所以使用的缓存线更少。4字节版本甚至更加缓存友好。
CHAR_BIT
取代 8。 - Angew is no longer proud of SOCHAR_BIT
是指char
变量的位数,因此它取决于平台。是的,在大多数现代硬件上,它几乎可以保证是8,但这并不是一般情况下都成立的。而且,CHAR_BIT
比8
更具有自我描述性。 - Angew is no longer proud of SO