警告

您正在阅读的 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/eloquent/tlsf_cpp/test/test_tlsf.cpp#L41

请注意,本次测试使用的不是我们刚刚创建的自定义分配器,而是 TLSF 分配器(见下文)。

TLSF 分配器

ROS 2 支持 TLSF(两级隔离拟合)分配器,该分配器旨在满足实时要求:

https://github.com/ros2/realtime_support/tree/eloquent/tlsf_cpp

有关 TLSF 的更多信息,请参阅 http://www.gii.upv.es/tlsf/

需要注意的是,TLSF 分配器是按照 GPL/LGPL 双重许可协议授权的。

这里有一个使用 TLSF 分配器的完整工作示例: https://github.com/ros2/realtime_support/blob/eloquent/tlsf_cpp/example/allocator_example.cpp