【问题标题】:std::unique_ptr twice as big as underlying objectstd::unique_ptr 是底层对象的两倍
【发布时间】:2024-01-19 00:14:01
【问题描述】:

我遇到了 std::unique_ptrs(特别是 MSFT VS 10.0 的实现)的问题。当我创建它们的 std::list 时,我使用的内存是创建仅包含底层对象的 std::list 时的两倍(注意:这是一个大对象——~200 字节,所以它不仅仅是一个周围有额外的参考计数器)。

换句话说,如果我运行:

std::list<MyObj> X;
X.resize( 1000, MyObj());

我的应用程序需要的内存是我运行时的一半:

std::list<std::unique_ptr<MyObj>> X;
for ( int i=0; i<1000; i++ ) X.push_back(std::unique_ptr<MyObj>(new MyObj()));

我已经检查了 MSFT 的实现,但没有发现任何明显的东西——有人遇到过这种情况并有什么想法吗?

编辑:好的,更清楚/具体一点。这显然是 Windows 内存使用问题,我显然遗漏了一些东西。我现在尝试了以下方法:

  1. 创建一个std::list of 100000 MyObj
  2. 创建一个std::list of 100000 MyObj*
  3. 创建一个 100000 int* 的 std::list
  4. 创建一个 50000 整数的 std::list*

在每种情况下,列表的每个 add'l 成员,无论是指针还是其他,都会使我的应用程序膨胀 4400(!) 字节。这是一个 64 位版本的版本,不包含任何调试信息(链接器 > 调试 > 生成调试信息设置为否)。

我显然需要对此进行更多研究,以将其缩小到更小的测试用例。

对于那些感兴趣的人,我正在使用Process Explorer 确定应用程序大小。

原来这完全是堆碎片。多么可笑。每个 8 字节对象 4400 字节!我切换到预分配,问题完全消失了——我习惯了依赖每个对象分配的效率低下,但这太荒谬了。

MyObj 实现如下:

class   MyObj
{
public:
    MyObj() { memset(this,0,sizeof(MyObj)); }

    double              m_1;
    double              m_2;
    double              m_3;
    double              m_4;
    double              m_5;
    double              m_6;
    double              m_7;
    double              m_8;
    double              m_9;
    double              m_10;
    double              m_11;           
    double              m_12;           
    double              m_13;
    double              m_14;
    double              m_15;
    double              m_16;
    double              m_17;
    double              m_18;
    double              m_19;
    double              m_20;
    double              m_21;
    double              m_22;
    double              m_23;
    CUnit*              m_UnitPtr;
    CUnitPos*           m_UnitPosPtr;
};

【问题讨论】:

  • 您是否看到使用原始MyObj* 的内存使用情况相同?
  • 您的意思是,如果我使用 new 将 1000 个 MyObjs 添加到 std::list 中?我看到与普通列表情况相同的用法,而不是 std::unique_ptr 情况。
  • 试试MSVC11,他们优化了不少数据结构。此外,unique_ptr 没有引用计数器。请问MyObj的定义也能发一下吗?
  • 您认为用一个简短的自包含程序来演示这种行为是否可行,以便其他人可以进行试验?
  • 另外,我想知道您是如何确定这一事实的。

标签: c++ unique-ptr


【解决方案1】:

增加的内存可能来自堆效率低下 - 由于内部碎片和 malloc 数据,您必须为分配的每个块支付额外费用。您执行了两倍的分配量,这将招致惩罚。

例如,这个:

for(int i = 0; i < 100; ++i) {
  new int;
}

将使用比这更多的内存:

new int[100];

即使分配的金额相同。


编辑:

我在 Linux 上使用 GCC 使用 unique_ptr 的内存增加了大约 13%。

【讨论】:

  • 我同意在这里使用 std::unique_ptr 不一定有很好的理由(尽管如果没有我提供的更多信息,我不确定你是如何确定的),但我已经加入了将它们用作我的案例对象集合的习惯以及这里堆效率低下的程度令人震惊。此外,std::unique_ptr 的效率远低于直接对象分配,这并不是一个很好的理由。
  • unique_ptr 在这种情况下可能用于强制所有权。如果有人从这个列表中取出对象,那么他们将不得不以其他方式维护它,否则它将超出范围并自动释放内存。出于同样的原因,我个人会自己使用 boost::ptr_list,但它具有更好的语法 IMO。
  • 当您将指向底层对象的指针嵌入到其他对象中时,使用 ptr_list 会更加困难。所有权可以在主结构中维护(这里是一个 std::list of unique_ptrs),同时允许其他对象以除创建/删除之外的所有方式进行修改。
  • @nadime 您可以使用内存池吗?
【解决方案2】:

std::list&lt;MyObj&gt; 包含您的对象的 N 个副本(+ 列表指针所需的信息)。

std::unique_ptr&lt;MyObj&gt; 包含一个指向对象实例的指针。 (它应该只包含一个MyObj*)。

所以std::list&lt;std::unique_ptr&lt;MyObj&gt;&gt; 并不直接等同于您的第一个列表。 std::list&lt;MyObj*&gt; 的大小应与 std::unque_ptr 列表的大小相同。

在验证实现之后,唯一可以嵌入到对象本身指针旁边的东西可能是“删除器”,在默认情况下,它是一个调用 operator delete 的空对象。

您有 Debug 还是 Release 版本?

【讨论】:

  • 我不担心列表本身的大小。我担心我的应用程序的内存使用情况。前面的回答者指出,差异可能是由于堆效率低下,尽管这里问题的严重性令人震惊。
  • 是调试版还是发布版?在发布时,充其量应该有一个删除器。甚至那个也是通过模板编译出来的。
  • 发布,这才叫人震惊。
  • 而 std::list 与 std::list 的大小相同?
  • 这个想法是,当它的堆效率低下时,它也应该在你使用普通的旧指针时出现。 std::list&lt;Foo&gt; l2; for (unsigned int i = 0; i != 100; ++i) l2.push_back(new Foo); 之类的东西你的问题是关于 unique_ptr 效率低下。但也许您对堆分配的评论是正确的,那么这应该表明它。
【解决方案3】:

这不是一个答案,但它不适合评论,它可能是说明性的。

我无法重现该声明 (GCC 4.6.2)。拿这个代码:

#include <memory>
#include <list>

struct Foo { char p[200]; };

int main()
{
  //std::list<Foo> l1(100);

  std::list<std::unique_ptr<Foo>> l2;
  for (unsigned int i = 0; i != 100; ++i) l2.emplace_back(new Foo);
}

仅启用 l1 会产生(在 Valgrind 中):

total heap usage: 100 allocs, 100 frees, 20,800 bytes allocated

仅启用l2 并且循环给出:

total heap usage: 200 allocs, 200 frees, 21,200 bytes allocated

智能指针正好占用 4 × 100 字节。

在这两种情况下,/usr/bin/time -v 都会给出:

Maximum resident set size (kbytes): 3136

此外,pmap 在两种情况下都显示:total 2996K。为了确认,我将对象大小更改为 20000,将元素数量更改为 10000。现在数字是 198404K198484K:正好相差 80000B,每个唯一指针 8B(大概在分配器中进行了一些 8B 对齐)名单)。在相同的更改下,time -v 报告的“最大驻留集大小”现在是 162768164304

【讨论】:

  • 这是显示分配的数量,而不是正在使用的内存量。
  • @Pubby:嗯,很有趣。您将如何衡量总内存使用量?
  • 我不知道最好的方法,但命令提示符给出了一个估计。
  • 当然可以,但我不确定是否有需要。这对我来说不一定是一个大问题,我开始调查的原因是我在 Windows 7 机器和 Windows 2008 服务器机器上的使用情况大不相同。很奇怪。
  • @nadime:我们需要更准确地描述您如何确定内存使用情况以及确切的数字是多少......也许 Process Explorer 有什么有用的说法?