【问题标题】:Unexpectedly high memory usage, using a std::stack and std::map使用 std::stack 和 std::map 出乎意料的高内存使用率
【发布时间】:2014-02-18 07:21:24
【问题描述】:

我正在尝试遍历一棵树,以访问 4x4 滑动拼图的所有可能状态。我写的算法最初是递归的,但由于(显然)非常深的树,这被证明是不可能的。它崩溃并报告了一个段错误。然后我决定重写算法以迭代地完成它的工作,据我所见,它工作得很好。但是,一段时间后,由于交换,它开始显着减慢。我做了一些计算,但无法弄清楚所有这些内存使用的来源......

代码贴在下面,但这里有一些重要的功能:

  • std::stack<char, std::vector<char>> stack
  • std::map<unsigned long long, int> distanceTable

假设stack 的内存占用与其拥有的元素数量成正比,并假设map(其中元素是pair<unsigned long long, int>)相同,我打印出预期的内存占用:

cout << (stack.size() * sizeof(char) +
         distanceTable.size() * sizeof(pair<unsigned long long, int>))/(1<<20) << "MB\n";

并将输出与top 的输出进行比较。当我自己的程序报告大约 500MB 时,top 报告说它使用了我所有内存(4GB)的一半以上。这是我的推理无法解释的因素 4。我在这里错过了什么?

代码:

#include <iostream>
#include <map>
#include <stack>
#include <vector>
#include <sstream>
#include "slider.h"
using namespace std;

typedef Slider<4> Slider4;
typedef Slider4::Move Move;
typedef map<unsigned long long, int> Map;
typedef stack<char, std::vector<char>> Stack;

Move const moves[] = {Slider4::N, Slider4::S, Slider4::E, Slider4::W};
Move const opposite[] = {Slider4::S, Slider4::N, Slider4::W, Slider4::E};

int const moveIdx[] = {0, 1, 2, 3};
int const oppositeIdx[] = {1, 0, 3, 2};


Map generateDistanceTable()
{
    // non-recursive tree-walker to generate the distance-table
    Map distanceTable;
    Stack stack;
    Slider4 slider;
    unsigned long long depth = 1;

    stack.push(-1);
    distanceTable[slider.hash()]= depth;
    while (depth != 0)
    {
        cout << (stack.size() * sizeof(char) + 
                 distanceTable.size() * sizeof(pair<unsigned long long, int>))/(1ULL<<20) << "MB\n";

        int currentMove = stack.top() + 1;

        // find next move
        while (currentMove != 4)
        {
            // Try the move
            if (!slider.move(moves[currentMove]))
            {
                ++currentMove;
                continue;
            }

            // Check the current state of the puzzle
            auto &d = distanceTable[slider.hash()];
            if (d != 0)
            { // already encountered this state -> move back
                int undoMove = oppositeIdx[currentMove];
                slider.moveUnsafe(moves[undoMove]);
                ++currentMove; // try next move
                continue;
            } 

            stack.push(currentMove);
            d = ++depth;
            currentMove = 0;
        }

        if (currentMove == 4)
        {
            int undoMove = oppositeIdx[stack.top()];
            slider.moveUnsafe(moves[undoMove]);
            --depth;
            stack.pop();
        }
    }
}

int main()
{
    Map table = generateDistanceTable();
}

【问题讨论】:

  • 您在输出中交换了大小 - 这只是复制和粘贴错误,还是您的代码中也有。
  • distanceTable.size() * sizeof(char) => 这是如何准确估计地图大小的?它与实际大小有一个恒定的因子差异,这正是您所报告的。
  • @Angew 我的错误,复制粘贴错误。已在帖子中修复它,所有报告的结果仍然有效。
  • 您可以使用function 来防止此类错误;-)
  • @Angew 在您的估计中log(N)来自哪里?二叉树具有线性大小。用 boost::flat_map 或 std::unordered_map 替换 std::map 通常是一种胜利。

标签: c++ memory map stack swap


【解决方案1】:

映射中的每个条目必须至少有一个unsigned long long 和一个int。很可能,它也有两个指针(将条目保存在一起)。

distanceTable.size() * sizeof(char)

应该是这样的:

distanceTable.size() *
    (2 * sizeof (void *) + sizeof (unsigned long long ) + sizeof (int))

【讨论】:

  • @StefanoSanfilippo 我已经考虑到了这一点。但是如果预分配方案是聪明的,这种影响会随着大小的增加而变小。仅供参考,我在引擎盖下使用vector
  • 地图中的一个节点几乎肯定有两个以上的指针额外信息。我不认为没有父指针的线性迭代也是可能的,除了左和右,典型的实现使用红黑树,这也需要有关颜色的信息。 (这可以侵入指针的低位,但这样做的运行时成本很高。)当然,节点是动态分配的,这也增加了成本。
【解决方案2】:

首先,std::map 在以下方面特别低效 内存使用。您插入的每个值将被放置在一个单独的 节点,除了值之外,通常包含三个 指针和一些附加信息(MS 中的 2 char 执行)。此外,每个节点通常被分配 分开,因此分配器所需的额外开销必须 被添加。在 32 位系统上,总开销为 至少 20 个字节;在 64 位系统上,40。

至于std::vector(它是您的std::stack 的基础),它是 好多了,但是如果你不使用reserve 预分配,它会不时重新分配,通常 将容量乘以 1.5 或 2。这意味着它可能 最终占用比必要更多的内存。 (还, 根据分配模式,系统可能不会 能够有效地重用期间释放的内存 重新分配。)

不过,通常最好使用 std::vector,保存在 使用std::lower_bound 订购,而不是std::map

最后,如果你事先知道有多少条目 向量将具有或可以建立一些合理的上限, 您可以使用reserve 进行预分配。这样可以避免任何风险 大小翻倍。

【讨论】:

  • 所以它不会从上面丢失,boost::flat_mapstd::unordered_map 也被建议作为内存效率更高的容器的可能候选者。
【解决方案3】:

这里的其他答案很好,所以我将尝试采取不同的策略:减小游戏树的大小。

game trees 上的 wiki 文章显示了一个使用井字游戏的示例:

请注意,由于棋盘可以镜像/反射和旋转,实际上只有 3 个独特的位置可以开始游戏。根据您尝试解决的问题类型,您或许可以利用类似的可能性。

【讨论】:

  • 哈哈不错。我当然已经考虑过这一点。我知道我的实现是一个非常幼稚的实现,显然我的下一步是考虑旋转和镜像对称。无论如何感谢您的建议!
  • 15-puzzle 中所有镜像状态都不可达。尽管如此,我还是从旋转中得到了 4 个因子。只是让你知道;-)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-19
  • 2017-09-07
  • 2014-03-01
  • 2021-09-25
  • 1970-01-01
  • 2012-08-16
相关资源
最近更新 更多