有什么方法可以"factor out"公共字段以节省空间?
Any way to "factor out" common fields to save space?
我有一个大数组(>百万)的Item
,其中每个Item
的形式为:
struct Item { void *a; size_t b; };
有几个不同的a
字段,这意味着有许多项目具有相同的a
字段。
我想"分解"这些信息以节省大约 50% 的内存使用量。
但是,麻烦的是这些Item
具有重要的顺序,并且可能会随着时间的推移而改变。因此,我不能继续为每个不同的a
单独Item[]
,因为这将失去项目相对于彼此的相对顺序。
另一方面,如果我将所有项目的顺序存储在size_t index;
字段中,则删除void *a;
字段会丢失任何节省的内存。
那么有没有办法让我在这里真正节省内存,或者没有?
(注意:我已经可以想到例如使用unsigned char
让a
索引到一个小数组中,但我想知道是否有更好的方法。这将要求我要么使用未对齐的内存,要么将每个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
位整数,你可以根据你对每个a
的b
s 计数的了解,在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
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
- 将结构字段的类型展开为可变模板参数
- 将位字段导出到数组
- 为了方便起见,我应该避免公开私有字段变量吗
- 当字段可以为null时,如何使用C++接口在Avro中写入数据
- 在java中读取c++字节的位字段
- 链接器找不到在虚拟类 c++ 中访问的静态字段的符号
- 私有字段对象与私有继承?
- 声明没有默认构造函数的字段
- C++内存模型和位字段的最大序列
- 声明为无效的变量或字段'...' Ardunio 编译器上的错误
- 如何在QByteArray中放置和检索位字段而不会感到痛苦?
- C++ win32 如何使密码字段可选并启用复制和粘贴?
- 如何通过UDP接收QByteArray并将其解析为位字段结构?
- 仅匹配集合中的某些字段
- 结构字段名称与 GDB 中的 STL 数组冲突
- 如何使用位字段将数据从二进制文件复制到结构中?
- 结构体和类的不同大小(),彼此具有相同的字段类型
- 如何避免在数据结构中包含存储为字段的类?
- 聚合初始化和删除的复制构造函数,也称为不可复制的 obejcts 作为字段
- 有什么方法可以"factor out"公共字段以节省空间?