【问题标题】:Performance of smart pointer and raw pointer in containers容器中智能指针和原始指针的性能
【发布时间】:2022-01-23 06:58:52
【问题描述】:

我很好奇这个问题的答案,因为我主要使用容器。 在矢量或地图容器中的最少 100 个(最多 10k)个元素中使用哪一个更合乎逻辑?

  • std:::vector<std::unique_ptr<(struct or class name)>>
  • std:::vector<std::shared_ptr<(struct or class name)>>
  • std:::vector<(struct or class name)*>

机器细节:FreeBSD 12.1 + clang-devel 或 gcc11。

【问题讨论】:

  • 默认选择第四个选项:std:::vector<(struct or class name)>
  • @davidhigh 为什么?每当我查看大型项目(如游戏)开发人员通常使用std::vector<(struct or class name)*> 时,我都不完全理解这一点,这是什么原因?
  • 这取决于classstruct 是什么以及您如何使用它以及如何使用向量。
  • 在上面展开:除非你有一个非常好的理由,比如多态性,否则让容器包含数据。更少的指针意味着更少的指针追逐和更好的缓存使用。
  • 不完全。我们要去的地方是你应该有充分的理由。有很多好的理由,不胜枚举,而多态性只是一个很常见的理由。在 cmets 的大小、稀缺性和数据隐藏等方面提出了很好的理由。你会发现更多。但如果你没有充分的理由,让容器完成它的工作并包含它。如果没有明显的原因,测试和分析会告诉您是否需要替代方案。

标签: c++ c++20


【解决方案1】:

这确实是基于意见的,但我将描述我使用的经验法则。

std:::vector<(struct or class name)> 是我的默认设置,除非我有该选项不满足的特定要求。更具体地说,这是我的首选,除非以下条件中的至少一个为真;

  • struct or class name 是多态的,派生自 struct or class name 的类的实例需要存储在向量中。
  • struct or class name 不符合三规则(C++11 之前)、五规则(来自 C++11)或零规则
  • 有特定的要求来动态管理struct or class name 实例的生命周期

上述标准相当于“如果struct or class name 满足成为标准容器元素的要求,则使用std::vector<(struct or class name)>”。

如果struct or class name 是多态的并且要求向量包含派生类的实例,我的默认选择是std:::vector<std::unique_ptr<(struct or class name)> >。即问题中没有提到任何选项。

如果std:::vector<(struct or class name)>std:::vector<std::unique_ptr<(struct or class name)> > 都无法满足向量中对象的生命周期管理特殊要求,我只会跳过该选择。

实际上,以上满足了绝大多数现实世界的需求。

如果需要两段不相关的代码来控制存储在向量中的对象的生命周期,那么(并且只有这样)我会考虑std:::vector<std::shared_ptr<(struct or class name)> >。前提是会有一些代码无法访问我们的向量,但可以通过(例如)传递std::shared_ptr<(struct or class name)> 来访问其元素。

现在,我遇到了在我的经验中非常罕见的情况 - 要求 来管理 std:::vector<(struct or class name)>std:::vector<std::unique_ptr<(struct or class name)> > 或未正确处理的对象的生命周期std:::vector<std::shared_ptr<(struct or class name)> >.

在这种情况下,并且只有在这种情况下,我才会——而且只有在我绝望的时候——使用std:::vector<(struct or class name)*>。这是要尽可能避免的情况。为了让您了解我认为此选项有多糟糕,众所周知,我会更改 other 系统级要求以避免此选项。我像瘟疫一样避免使用此选项的原因是,有必要编写和调试显式管理每个struct or class name 的生命周期的每一段代码。这包括在各处编写new 表达式,确保每个new 表达式最终都与相应的delete 表达式匹配。此选项还意味着需要调试手写代码以确保没有对象是 deleted 两次(未定义行为)并且每个对象都是 deleted 一次(即避免泄漏)。换句话说,这个选项需要付出很多努力,而且 - 在非平凡的情况下 - 真的很难正确工作。

【讨论】:

  • +1 回答开头提到的 3 点。但是,如果您还提供一些关于这些方法之间可能的性能差异的见解,那就更好了。
  • 加一,但不应该是“除非(至少)三个条件之一为真”吗?例如,您可以拥有多态性,同时又完全符合五规则,但您仍然可以使用指针。
  • @davidhigh 想一想,你是对的。我会尽快更新。
  • @digito_evo 性能差异(在速度或资源使用方面)实际上并没有多大意义。如果对象向量合适,则使用指针(或智能指针)会增加额外的间接级别,因此不太可能提高性能。如果需要间接(例如,该类是多态的),那么它是必需的。与合适的智能指针相比,原始指针通常不会提供令人眼花缭乱的性能改进,而不会接受代码行为不正确(或努力纠正该问题)的重大风险。
  • 这个答案增加了对使用原始指针的不必要的恐惧。当没有所有权时,像std::vector<whatever*> 这样的指针列表非常好。也就是说,当另一个不相关的容器拥有对象时,保证指针不会悬空。在复杂的系统中,这样的指针列表应该一直出现。
【解决方案2】:

从正确的行为开始,而不是表现。

  1. 您的容器是否拥有您的对象?如果不是,请使用原始指针。如果是,请使用智能指针。但哪些?见下文。
  2. 是否需要支持多个包含同一个对象的容器,不清楚先删除哪个容器?如果两者的答案都是“是”,请使用shared_ptr。否则,请使用unique_ptr

稍后,如果您发现访问智能指针浪费太多时间(不太可能),请将智能指针替换为原始指针以及高度优化的内存管理,您必须根据您的具体需求来实现。


如 cmets 中所述,您可以在没有指针的情况下执行此操作。所以,在应用这个答案之前,问问自己为什么需要指针(我猜答案是多态性,但不确定)。

【讨论】:

  • 非常感谢,我的类或结构中没有多态性,我想我会使用std::vector<(class or struct name)>
  • 保证最后不被删除的应该只是原始指针。如果保证最后删除任何一个,那应该是一个 unique_ptr。如果一个组中的一个保证最后被删除,但你不知道哪个,那些是 shared_ptr。
【解决方案3】:

如果不了解上下文以及结构/类的运行方式,很难为您的问题提供可靠的解决方案。
但我仍然想提供一些有关智能指针的基本信息,希望您能做出明智的决定。

一个例子:

#include <iostream>
#include <vector>
#include <memory>

int main( )
{
    struct MyStruct
    {
        int a;
        double b;
    };

    std::cout << "Size of unique_ptr: " << sizeof( std::unique_ptr< MyStruct > ) << '\n';
    std::cout << "Size of shared_ptr: " << sizeof( std::shared_ptr< MyStruct > ) << '\n';
    std::cout << '\n';

    std::vector< std::unique_ptr<MyStruct> > vec1; // a container holding unique pointers
    std::vector< MyStruct* > vec2; // another container holding raw pointers

    vec1.emplace_back( std::make_unique<MyStruct>(2, 3.6) ); // deletion process automatically handled
    vec2.emplace_back( new MyStruct(5, 11.2) ); // you'll have to manually delete all objects later

    std::cout << vec1[0]->a << ' ' << vec1[0]->b << '\n';
    std::cout << vec2[0]->a << ' ' << vec2[0]->b << '\n';
}

可能的输出:

Size of unique_ptr: 8
Size of shared_ptr: 16

2 3.6
5 11.2

检查程序集输出here 并比较两个容器。正如我所见,它们生成的代码完全相同。

unique_ptr 非常快。我不认为它有任何开销。但是,shared_ptr 由于其引用计数机制而有一些开销。但它仍然可能比手写引用计数系统更有效。不要低估 STL 中提供的设施。在大多数情况下使用它们,除了 STL 不能完全执行您需要的特定任务的情况。

说到性能,std::vector&lt;(struct or class name)&gt; 在大多数情况下更好,因为所有对象都存储在连续的堆内存块中,并且不需要取消引用它们。
但是,当使用指针容器时,您的对象将分散在堆内存中,并且您的程序对缓存的友好性会降低。

【讨论】:

  • 例如非常感谢你!你觉得std::vector&lt;(struct or class name)&gt;std::vector&lt;std::unique_ptr&lt;(struct or class name)&gt;&gt; 的性能如何?
  • @Wayne Cox 是的,它可以明显更快。阅读我的更新答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-08-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-03-04
相关资源
最近更新 更多