我在C中编写min-heap
的实现,作为Dijkstra算法的一部分。我已经完成了所有细节,我的测试程序通过了valgrind测试,但它在这个过程中分配了大量的内存。最终测试是在INT_MAX
到INT_MAX
的网格上(坐标只是整数),我测试时出现SIGXCPU
错误。即使我只是将16k
位置插入队列然后删除所有内容,它仍然需要很长时间并且分配超过8 MB
。当我在巨大的网格测试用例上运行它时,在我手动退出之前它可以达到500 MB
。会发生什么事?这是我的代码的一部分:
struct position {
int x;
int y
};
typedef struct elt {
int priority;
int distance;
struct position p;
} *Elt;
typedef struct heap {
int size;
int capacity;
Elt *elts;
} *Heap;
void heap_insert(Heap h, Elt e, int *counter) {
if(h->capacity < (h->size + 2)) {
h->elts = realloc(h->elts, h->capacity * sizeof(Elt) * 2);
h->capacity *= 2;
}
h->elts[h->size] = malloc(sizeof(*Elt));
elt_assign(h->elts[h->size], e);
h->size++;
heapify(h->size, h->elts);
*counter = *counter + 1;
}
我的所有其他功能都是一次性,功能性或根本不进行内存管理。在这种情况下,初始大小为64
,但从1024
开始,我的效果相同。我也试过限制队列的大小,但没有用。我很确定这不是我的堆码代码,但这只是以防万一
static void floatDown(int n, Elt *a, int pos) {
Elt x = malloc(sizeof(struct elt));
elt_assign(x, a[pos]);
for(;;) {
if(Child(pos, 1) < n && a[Child(pos, 1)]->priority < a[Child(pos, 0)]->priority) {
if(a[Child(pos, 1)]->priority < x->priority) {
elt_assign(a[pos], a[Child(pos, 1)]);
pos = Child(pos, 1);
} else {
break;
}
} else if(Child(pos, 0) < n && a[Child(pos, 0)]->priority < x->priority) {
elt_assign(a[pos], a[Child(pos, 0)]);
pos = Child(pos, 0);
} else {
break;
}
}
elt_assign(a[pos], x);
free(x);
}
static void heapify(int n, Elt *a) {
for(int i = n - 1; i >= 0; i--) {
floatDown(n, a, i);
}
}
非常感谢任何帮助。
答案 0 :(得分:2)
这是我的工作理论。我愿意发现我错了,但没有剩下的代码,我就无法检测,运行和测试它。
当... struct heap { ... Elt *elts; } ...
保存复制4个int的成本并用复制1指针替换它时typedef struct elt {...} *Elt;
的间接,但复制速度很快,而且只发生log2(N)次。
相反,每个struct elt
都是单独的malloc。如果没有挖掘来找到malloc'd块的实际大小,我们可以估计平均会浪费N / 2 sizeof(struct elt)(实际上,我认为它在我的机器上更糟)。
它也可能创建不连续的内存块(通过在较大的块之间放置小块),因此realloc必须始终分配更大的块,因此重用前一个块将更加困难。在这个特定情况下,我认为这与内部碎片造成的浪费或者malloc的大量调用一样重要。
它也可能会创建一个'缓存破坏者'。实际值正在整个内存中传播,并且由于malloc'd struct elt块的内部碎片,缓存行相对稀疏。
所以替换:
typedef struct elt {
int priority;
int distance;
struct position p;
} *Elt;
typedef struct heap {
int size;
int capacity;
Elt *elts;
} *Heap;
与
typedef struct elt {
int priority;
int distance;
struct position p;
} Elt; // no longer a pointer
typedef struct heap {
int size;
int capacity;
Elt *elts;
} *Heap;
并改变:
void heap_insert(Heap h, Elt e, int *counter) {
if(h->capacity < (h->size + 2)) {
h->elts = realloc(h->elts, h->capacity * sizeof(Elt) * 2);
h->capacity *= 2;
}
h->elts[h->size] = malloc(sizeof(*Elt));
elt_assign(h->elts[h->size], e);
h->size++;
heapify(h->size, h->elts);
*counter = *counter + 1;
}
到
void heap_insert(Heap h, Elt e, int *counter) {
if(h->capacity < (h->size + 2)) {
h->elts = realloc(h->elts, h->capacity * sizeof(Elt) * 2);
h->capacity *= 2;
}
h->elts[h->size] = e; // no longer need to malloc
h->size++;
heapify(h->size, h->elts);
*counter = *counter + 1;
}
因此,用于保存堆的malloc'd / realloc的内存量应大约为2 * N * sizeof(struct elt)。函数/宏elt_assign可能会被更改为隐藏其他更改。
然后通过更改:
进一步减少malloc的数量static void floatDown(int n, Elt *a, int pos) {
Elt x = malloc(sizeof(struct elt));
elt_assign(x, a[pos]);
...
elt_assign(a[pos], x);
free(x);
}
到
static void floatDown(int n, Elt *a, int pos) {
Elt x = a[pos];
...
a[pos] = x;
}
这应该进一步减少malloc和内存的内存量。
基本上,应该只有(大约)log2(N)调用realloc。 realloc可能还有更好的机会扩展现有的块而不是副本。
编辑:
heap_insert
中存在比内存分配更大的问题:
void heap_insert(Heap h, Elt e, int *counter) {
...
heapify(h->size, h->elts);
...
}
heapify
被称为插入堆中的每个元素,即heapify被调用N次。 heapify
是:
static void heapify(int n, Elt *a) {
for(int i = n - 1; i >= 0; i--) {
floatDown(n, a, i);
}
}
到目前为止,在每个元素 元素>>上调用floatdown
。因此heap_insert
的运行时间约为(N ^ 2)/ 2 (即O(N ^ 2))运行时间。
我相信heap_insert
应该为它添加到堆中的每个元素使用floatDown
,而不是heapify
。