智能指针和原始指针在容器中的性能
Performance of smart pointer and raw pointer in containers
我很好奇这个问题的答案,因为我主要使用容器。
在 vector 或 map 容器中使用最少 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。
如果不了解上下文和您的 struct/class 运作方式,很难为您的问题提供可靠的解决方案。
但我还是想提供一些关于智能指针的基本信息,希望你能做出明智的决定。
一个例子:
#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<(struct or class name)>
在大多数情况下更好,因为所有对象都存储在连续的堆内存块中,并且不需要取消引用它们。
但是,当使用指针容器时,您的对象将分散在堆内存中,并且您的程序对缓存的友好性将降低。
从正确的行为开始,而不是表现。
- 您的容器是否拥有您的对象?如果不是,请使用原始指针。如果是,请使用智能指针。但是哪些呢?见下文。
- 是否需要支持多个包含相同对象的容器,不清楚先删除哪个容器?如果两者的答案都是“是”,请使用
shared_ptr
。否则,使用 unique_ptr
.
稍后,如果您发现访问智能指针会浪费太多时间(不太可能),请将智能指针替换为原始指针并结合高度优化的内存管理,您将不得不根据您的具体需求实施。
如评论中所述,您可以在没有指针的情况下完成。所以,在应用这个答案之前,问问自己为什么你需要指针(我猜答案是多态性,但不确定)。
这确实是基于意见的,但我将描述我使用的经验法则。
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)> >
。即问题中提到的 none 个选项。
如果 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)>
.
来访问其元素
现在,我遇到了这种情况,这在我的经验中非常罕见 - 有 要求 来管理 [=10= 未正确处理的对象的生命周期]、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
表达式匹配。此选项还意味着需要调试手写代码以确保没有对象被 delete
d 两次(未定义的行为)并且每个对象都被 delete
d 一次(即避免泄漏)。换句话说,这个选项需要付出很多努力,而且 - 在非常重要的情况下 - 真的很难正常工作。
我很好奇这个问题的答案,因为我主要使用容器。 在 vector 或 map 容器中使用最少 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。
如果不了解上下文和您的 struct/class 运作方式,很难为您的问题提供可靠的解决方案。
但我还是想提供一些关于智能指针的基本信息,希望你能做出明智的决定。
一个例子:
#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<(struct or class name)>
在大多数情况下更好,因为所有对象都存储在连续的堆内存块中,并且不需要取消引用它们。
但是,当使用指针容器时,您的对象将分散在堆内存中,并且您的程序对缓存的友好性将降低。
从正确的行为开始,而不是表现。
- 您的容器是否拥有您的对象?如果不是,请使用原始指针。如果是,请使用智能指针。但是哪些呢?见下文。
- 是否需要支持多个包含相同对象的容器,不清楚先删除哪个容器?如果两者的答案都是“是”,请使用
shared_ptr
。否则,使用unique_ptr
.
稍后,如果您发现访问智能指针会浪费太多时间(不太可能),请将智能指针替换为原始指针并结合高度优化的内存管理,您将不得不根据您的具体需求实施。
如评论中所述,您可以在没有指针的情况下完成。所以,在应用这个答案之前,问问自己为什么你需要指针(我猜答案是多态性,但不确定)。
这确实是基于意见的,但我将描述我使用的经验法则。
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)> >
。即问题中提到的 none 个选项。
如果 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)>
.
现在,我遇到了这种情况,这在我的经验中非常罕见 - 有 要求 来管理 [=10= 未正确处理的对象的生命周期]、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
表达式匹配。此选项还意味着需要调试手写代码以确保没有对象被 delete
d 两次(未定义的行为)并且每个对象都被 delete
d 一次(即避免泄漏)。换句话说,这个选项需要付出很多努力,而且 - 在非常重要的情况下 - 真的很难正常工作。