【问题标题】:Heap implementation using absurd amounts of memory - C使用荒谬的内存量的堆实现 - C
【发布时间】:2012-04-11 15:43:22
【问题描述】:

我正在用 C 语言编写 min-heap 的实现,作为 Dijkstra 算法的一部分。我已经把所有细节都写下来了,我的测试程序通过了 valgrind 测试,但是它在这个过程中分配了荒谬的内存量。最后的测试是在INT_MAX by 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);
    }
}

任何帮助将不胜感激。

【问题讨论】:

  • 我感觉elt_assign 方法在这里也会派上用场。
  • 我同意 Makoto,至于您发布的代码没有明显的泄漏。
  • 您说“它在进程中分配了荒谬的内存量”。它分配了多少?打印出 malloc 和 realloc 返回的值可能会有一些见解。 (我会制作包装函数 myMalloc 和 myRealloc)。 可能是内存在浮动开始时被 Elt x = malloc... 碎片化,所以我想知道 heap_insert 中的每个 realloc 是否都以某种低效的方式分配了一块全新的内存。还要对每个 malloc 和 realloc 计时,并打印时间,可能会显示指数减速。将Elt x = malloc 替换为typedef struct elt e = a[pos];
  • 我省略了 elt_assign 函数,因为我已经在问题中有大量代码,但它只是 e->p.x = temp->p.x 等。谢谢 gbulmer,我会尝试这两个。我相当有信心在 10≤x≤16 的情况下运行 2^x 次插入,我的速度会呈指数级下降。 16岁,速度慢得无法忍受。我并不是说每个 alloc 分配的内存比有意义的多,但是如果我将 8000 个 Elts 插入堆中,valgrind 将报告我分配了 8 MB 超过 50 万个分配(全部释放,但仍然太多) .另外,在您最后的建议中,为什么 typedef 应该在那里?
  • @jclancy - 抱歉,我复制错误+粘贴了; typedef struct elt e = a[pos]; 应该是 struct elt e = a[pos];。将 floatDown 中的 Elt x = malloc... 更改为 struct elt e = a[pos]; 可能会显着减少 malloc'ed & free'd 的空间量;如果 floatdown 被调用 (N.log N) 次,那是相当多的 malloc+free 调用。

标签: c memory-management data-structures


【解决方案1】:

这是我的工作理论。我愿意发现自己错了,但是如果没有其余代码,我就无法检测、运行和测试它。

... struct heap { ... Elt *elts; } ... 的间接寻址在 typedef struct elt {...} *Elt; 时节省了复制 4 个整数并用复制 1 个指针替换它的成本,但是复制速度很快,并且只发生 log2(N) 次。

相反,每个struct elt 都是单独分配的。无需四处寻找 malloc'd 块的实际大小,我们可以估计,平均而言这将浪费 N/2 sizeof(struct elt) (实际上,我认为它在我的机器上更糟)。

它还可能创建不连续的内存块(通过将小块放在大块之间),因此 realloc 必须总是分配更大的块,因此重用以前的块会更加困难。在这种特定情况下,我认为这并不像由于内部碎片或大量 malloc 调用造成的浪费那么重要。

它还可能创建一个“缓存破坏者”。实际值分布在整个内存中,并且由于 malloc 结构 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'd 的内存量应该大约为 2 * N * sizeof(struct elt)。函数/宏 elt_assign 可能会被更改以隐藏其他更改。

然后通过改变进一步减少malloc'ing的数量:

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'ed 和 free'd 的内存量。

本质上,realloc 应该只有(大约)log2(N) 次调用。 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

【讨论】:

  • @wildplasser - 当代码需要处理指针时,我不喜欢隐藏指针的 typedef。我不喜欢Elt x = malloc(sizeof(struct elt)); 之类的东西。我更喜欢明确的指针; Elt* x = malloc(sizeof(Elt)); 立即有意义(对我而言)。同样,我会将typedef struct heap { ... } *Heap; 更改为typedef struct heap { ... } Heap;,这样void heap_insert(Heap h, ...) { if(h-&gt;capacity &lt; (h-&gt;size + 2)) { h-&gt;elts = ... 之类的代码就会变成void heap_insert(Heap* h, ...) { if(h-&gt;capacity &lt; (h-&gt;size + 2)) { h-&gt;elts = ...,我觉得这更明显。
  • 好的,谢谢,我做了修改,明天测试。你能做这样的结构赋值,只用一个等号,而不是单独做所有事情吗?
  • @jclancy - 除非您使用的是非常旧的(1990 年之前)C 编译器,否则结构赋值是有效的。即使您不喜欢我建议的更改,我认为重要的部分是删除大量 malloc 调用;我认为他们造成了很多你描述的问题。所以请尝试更改。我认为这会产生的不同。如果没有改善,请报告您的发现。
猜你喜欢
  • 2023-03-08
  • 2016-06-04
  • 2018-03-05
  • 2011-11-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-09-07
相关资源
最近更新 更多