有什么方法可以“分解”常用字段以节省空间?

时间:2018-05-13 18:33:27

标签: c++ c algorithm memory data-structures

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

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

有一些不同的a字段 - 意味着许多项目具有相同的a字段。

我想将这些信息“分解”出来以节省大约50%的内存使用量。

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

另一方面,如果我存储 size_t index;字段中所有项目的排序,那么删除void *a;字段会减少任何内存节省

那么有没有办法在这里实际节省内存,或者没有?

(注意:我已经可以想到例如使用unsigned chara索引到一个小数组,但我想知道是否有更好的方法。那将要求我使用未对齐的内存或将每个Item[]拆分为两个,这对于内存位置并不好,所以我更喜欢别的东西。)

4 个答案:

答案 0 :(得分:5)

结合使用轻量级压缩方案(请参阅this获取示例和一些参考资料)来表示a*值。例如,@ Frank的回答雇用了DICT,然后是NS。如果你有相同指针的长时间运行,你可以考虑RLE(运行长度编码)。

答案 1 :(得分:3)

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

典型的用例是这样一种环境,其中(a)值是实际区分的联合(即,它们包括类型指示符),具有有限数量的不同类型,(b)值大多数保存在大的连续向量中。 / p>

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

实际上,现在大多数主流CPU都不需要对齐访问,我只会使用压缩结构而不是下面的hack。如果你不支付未对齐访问,那么将{one-byte type + 8-byte value}存储为九个连续字节可能是最佳的;唯一的代价是你需要乘以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]; }

向量是Chunks的向量。为了使hack工作,它们必须分配在8(或4)字节对齐的内存地址上,但我们已经假设有效负载类型需要对齐。

hack的关键是我们如何表示指向单个值的指针,因为该值在内存中不是连续的。我们使用指向它的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字节的高速缓存行;其余的时间是两个连续的高速缓存行被击中,结果向前(或向后)通过向量向前走动就像通过普通向量一样缓存友好,除了它使用更少的高速缓存行,因为对象是一半大小。四字节版本甚至更加缓存。

答案 2 :(得分:2)

我想我自己想出了理论上最佳的信息方式......在我的案例中它并不值得获得,但我会在这里解释它,以防它帮助某人别的。
但是,它需要未对齐的内存(在某种意义上) 也许更重要的是,您失去了动态轻松添加a新值的能力。

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

如果我们假设有N个不同的项可供选择,那么我们需要n = ceil(log2(N))位来表示每个Item。所以我们真正想要的是一个n位整数数组,其中n计算在运行时。然后,一旦获得n位整数,您就可以在log(n)时间内进行二进制搜索,根据您对{的计数的了解,找出它对应的a每个b {1}}。 (这可能会受到性能影响,但这取决于不同a的数量。)

你不能以良好的记忆协调的方式做到这一点,但这并不是太糟糕。你要做的是创建一个a数据结构,其中每个元素的位数是一个动态可指定的数量。然后,要随机访问它,您需要执行一些除法或mod操作以及位移以提取所需的整数。

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

这在信息理论上是最优的,只要您需要每个元素的常数位数,这是您想要随机访问的。但是,如果放宽约束,你可以做得更好:你可以将多个整数打包成switch (n) { case 33: handle_case<33>(i); }位,然后用更多的数学提取它们。这也可能会扼杀性能。

(或者,长话短说:C和C ++确实需要高性能的k * n数据结构......)

答案 3 :(得分:2)

Array-of-Structures方法可能会有所帮助。也就是说,有三个向量......

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&lt; bool&gt;或者boost :: dynamic_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