警告
您正在阅读的 ROS 2 文档版本已达到 EOL(生命周期结束),不再受官方支持。如果您想了解最新信息,请访问 Jazzy.
实施自定义内存分配器
本教程将教您如何为发布者和订阅者集成一个自定义分配器,以便在执行 ROS 节点时不会调用默认的堆分配器。本教程的代码如下 这里.
背景介绍
假设您想编写实时安全代码,而且您听说过在实时关键部分调用 "new "会有很多危险,因为大多数平台的默认堆分配器都是非确定的。
默认情况下,许多 C++ 标准库结构在增长时会隐式分配内存,例如 std::vector
.不过,这些数据结构也接受 "分配器 "模板参数。如果你为这些数据结构之一指定了一个自定义分配器,那么系统将使用该分配器而不是系统分配器来增大或缩小数据结构。自定义分配器可以在栈上预分配一个内存池,这可能更适合实时应用程序。
在 ROS 2 C++ 客户端库(rclcpp)中,我们遵循与 C++ 标准库类似的理念。发布者、订阅者和执行者都会接受一个 Allocator 模板参数,用于控制该实体在执行过程中的分配。
编写分配器
要编写与 ROS 2 分配器接口兼容的分配器,你的分配器必须与 C++ 标准库分配器接口兼容。
C++11 库提供了名为 分配器属性
.C++11 标准规定,自定义分配器只需满足最基本的要求,即可用于以标准方式分配和取消分配内存。 分配器属性
是一种通用结构,它可以根据按照最低要求编写的分配器来填充分配器的其他特性。
例如,自定义分配器的以下声明将满足 分配器属性
(当然,您仍然需要在此结构中实现已声明的函数):
模板 <;类 T>;
结构 自定义分配器 {
使用 值类型 = T;
自定义分配器() 无例外;
模板 <;类 U>; 自定义分配器 (缢 自定义分配器<;U>&;) 无例外;
T* 分配 (标准::size_t n);
空白 调用 (T* p, 标准::size_t n);
};
模板 <;类 T, 类 U>;
常式 bool 操作员== (缢 自定义分配器<;T>&;, 缢 自定义分配器<;U>&;) 无例外;
模板 <;类 T, 类 U>;
常式 bool 操作员!= (缢 自定义分配器<;T>&;, 缢 自定义分配器<;U>&;) 无例外;
然后,您就可以访问由 分配器属性
就像这样 std::allocator_traits<custom_allocator<T>>::construct(...)
要了解 分配器属性
见 https://en.cppreference.com/w/cpp/memory/allocator_traits .
不过,一些仅支持部分 C++11 的编译器(如 GCC 4.8)仍要求分配器执行大量模板代码,以处理向量和字符串等标准库结构,因为这些结构不使用 分配器属性
内部。因此,如果您使用的是部分支持 C++11 的编译器,您的分配器需要看起来更像这样:
模板<;类型 T>;
结构 指针属性 {
使用 参照 = T 及样品;;
使用 const_reference = 缢 T 及样品;;
};
// 避免使用空的特殊化声明 void 的引用
模板<>;
结构 指针属性<;空白>; {
};
模板<;类型 T = 空白>;
结构 我的分配器 : 公 指针属性<;T>; {
公:
使用 值类型 = T;
使用 size_type = 标准::size_t;
使用 点子 = T *;
使用 const_pointer = 缢 T *;
使用 difference_type = 类型 标准::指针属性<;点子>::difference_type;
我的分配器() 无例外;
~我的分配器() 无例外;
模板<;类型 U>;
我的分配器(缢 我的分配器<;U>; 及样品;) 无例外;
T * 分配(size_t 尺寸, 缢 空白 * = 0);
空白 调用(T * ptr, size_t 尺寸);
模板<;类型 U>;
结构 重订 {
类型化 我的分配器<;U>; 其他;
};
};
模板<;类型 T, 类型 U>;
常式 bool 操作员==(缢 我的分配器<;T>; 及样品;,
缢 我的分配器<;U>; 及样品;) 无例外;
模板<;类型 T, 类型 U>;
常式 bool 操作员!=(缢 我的分配器<;T>; 及样品;,
缢 我的分配器<;U>; 及样品;) 无例外;
编写一个主要示例
一旦编写了有效的 C++ 分配器,就必须将其作为共享指针传递给发布者、订阅者和执行者。
汽车 分配 = 标准::共享<;我的分配器<;空白>>;();
汽车 出版商 = 网站->;创建出版商<;std_msgs::信息::UInt32>;("allocator_example";, 10, 分配);
汽车 msg_mem_strat =
标准::共享<;rclcpp::消息内存策略::消息内存策略<;std_msgs::信息::UInt32,
我的分配器<>>>;(分配);
汽车 订购者 = 网站->;创建订阅<;std_msgs::信息::UInt32>;(
"allocator_example";, 10, 回调, nullptr, 错误, msg_mem_strat, 分配);
标准::共享_ptr<;rclcpp::内存策略::内存策略>; 内存策略 =
标准::共享<;内存分配策略<;我的分配器<>>>;(分配);
rclcpp::执行人::单线程执行器 执行人(内存策略);
您还需要使用分配器来分配沿执行代码路径传递的任何信息。
汽车 分配 = 标准::共享<;我的分配器<;空白>>;();
实例化节点并将执行器添加到节点后,就可以开始旋转了:
uint32_t i = 0;
虽然 (rclcpp::好的()) {
信息->;数据 = i;
i++;
出版商->;发布(信息);
rclcpp::水电::sleep_for(标准::计时器::毫秒数(1));
执行人.旋转();
}
向进程内管道传递分配器
尽管我们在同一个进程中实例化了发布者和订阅者,但我们还没有使用进程内管道。
IntraProcessManager 是一个通常对用户隐藏的类,但为了向其传递自定义分配器,我们需要从 rclcpp Context 中获取该分配器,从而将其公开。IntraProcessManager 使用了多个标准库结构,因此如果没有自定义分配器,它将调用默认的 new。
汽车 背景 = rclcpp::背景::默认上下文::获取全球默认上下文();
汽车 ipm_state =
标准::共享<;rclcpp::进程内管理器::进程内管理器状态<;我的分配器<>>>;();
// 使用自定义分配器构建进程内管理器。
背景->;获取子上下文<;rclcpp::进程内管理器::进程内管理器>;(ipm_state);
汽车 网站 = rclcpp::节点::共享("allocator_example";, 真);
请确保在以这种方式构建节点后再实例化发布者和订阅者。
测试和验证代码
你如何知道你的自定义分配器真的被调用了?
最明显的做法是计算调用自定义分配器的 分配
和 调用
函数,并将其与调用 新
和 删去
.
为自定义分配器添加计数功能非常简单:
T * 分配(size_t 尺寸, 缢 空白 * = 0) {
// ...
num_allocs++;
// ...
}
空白 调用(T * ptr, size_t 尺寸) {
// ...
num_deallocs++;
// ...
}
您还可以覆盖全局新建和删除操作符:
空白 操作员 删去(空白 * ptr) 无例外 {
如果 (ptr != nullptr) {
如果 (is_running) {
global_runtime_deallocs++;
}
标准::免费的(ptr);
ptr = nullptr;
}
}
空白 操作员 删去(空白 * ptr, size_t) 无例外 {
如果 (ptr != nullptr) {
如果 (is_running) {
global_runtime_deallocs++;
}
标准::免费的(ptr);
ptr = nullptr;
}
}
其中我们要递增的变量只是全局静态整数,而 is_running
是一个全局静态布尔值,在调用 后旋
.
"(《世界人权宣言》) 可执行示例 打印变量的值。要运行示例可执行文件,请使用
分配器示例
或打开进程内管道运行示例:
分配器示例 过程内
你应该得到这样的数字
全球 新 是 人称 15590 时代 期间 自旋全球 删去 是 人称 15590 时代 期间 自旋分配器 新 是 人称 27284 时代 期间 自旋分配器 删去 是 人称 27281 时代 期间 后旋
我们已经捕捉到了执行路径上发生的约 2/3 的分配/重新分配,但剩下的 1/3 来自哪里呢?
事实上,这些分配/重新分配源自本例中使用的底层 DDS 实现。
证明这一点不在本教程的讨论范围之内,但您可以查看作为 ROS 2 持续集成测试一部分运行的分配路径测试,该测试会对代码进行回溯,并找出某些函数调用是源于 rmw 实现还是源于 DDS 实现:
https://github.com/ros2/realtime_support/blob/dashing/tlsf_cpp/test/test_tlsf.cpp#L41
请注意,本次测试使用的不是我们刚刚创建的自定义分配器,而是 TLSF 分配器(见下文)。
TLSF 分配器
ROS 2 支持 TLSF(两级隔离拟合)分配器,该分配器旨在满足实时要求:
https://github.com/ros2/realtime_support/tree/dashing/tlsf_cpp
有关 TLSF 的更多信息,请参阅 http://www.gii.upv.es/tlsf/
需要注意的是,TLSF 分配器是按照 GPL/LGPL 双重许可协议授权的。
这里有一个使用 TLSF 分配器的完整工作示例: https://github.com/ros2/realtime_support/blob/dashing/tlsf_cpp/example/allocator_example.cpp