实施自定义内存分配器

目标 本教程将介绍在编写 ROS 2 C++ 代码时如何使用自定义内存分配器。

辅导水平: 高级

时间 20 分钟

本教程将教您如何为发布者和订阅者集成一个自定义分配器,以便在执行 ROS 节点时不会调用默认的堆分配器。本教程的代码如下 这里.

背景介绍

假设您想编写实时安全代码,而且您听说过调用 因为大多数平台的默认堆分配器都是非确定性的。

默认情况下,许多 C++ 标准库结构在增长时会隐式分配内存,例如 std::vector.不过,这些数据结构也接受 "分配器 "模板参数。如果你为这些数据结构之一指定了一个自定义分配器,它将使用该分配器而不是系统分配器来增长或缩小数据结构。自定义分配器可以在栈上预分配一个内存池,这可能更适合实时应用程序。

在 ROS 2 C++ 客户端库(rclcpp)中,我们遵循与 C++ 标准库类似的理念。发布者、订阅者和执行者都会接受一个 Allocator 模板参数,用于控制该实体在执行过程中的分配。

编写分配器

要编写与 ROS 2 分配器接口兼容的分配器,你的分配器必须与 C++ 标准库分配器接口兼容。

从 C++17 开始,标准库提供了一种名为 std::pmr::memory_resource.该类可用于创建满足最低要求的自定义分配器。

例如,下面的自定义内存资源声明就满足了这些要求(当然,你仍需要在该类中实现所声明的函数):

 自定义内存资源 :  标准::pmr::内存资源
{
私人:
  空白 * do_allocate(标准::size_t 字节数, 标准::size_t 路线) 否决;

  空白 do_deallocate(
    空白 * p, 标准::size_t 字节数,
    标准::size_t 路线) 否决;

  bool do_is_equal(
     标准::pmr::内存资源 及样品; 其他)  无例外 否决;
};

要了解 std::pmr::memory_resourcehttps://en.cppreference.com/w/cpp/memory/memory_resource.

本教程中自定义分配器的完整实现在 https://github.com/ros2/demos/blob/jazzy/demo_nodes_cpp/src/topics/allocator_tutorial_pmr.cpp.

编写一个主要示例

一旦编写了有效的 C++ 分配器,就必须将其作为共享指针传递给发布者、订阅者和执行者。但首先,我们要声明几个别名来缩短名称。

使用 rclcpp::内存策略::内存分配器策略::内存分配策略;
使用 分配 = 标准::pmr::多态分配器<;空白>;;
使用 MessageAllocTraits =
  rclcpp::分配器::分配重装<;std_msgs::信息::UInt32, 分配>;;
使用 MessageAlloc = MessageAllocTraits::分配器类型;
使用 信息删除器 = rclcpp::分配器::删除<;MessageAlloc, std_msgs::信息::UInt32>;;
使用 MessageUniquePtr = 标准::唯一参数<;std_msgs::信息::UInt32, 信息删除器>;;

现在,我们可以使用自定义分配器创建资源:

自定义内存资源 内存资源{};
汽车 分配 = 标准::共享<;分配>;(及样品;内存资源);
rclcpp::PublisherOptionsWithAllocator<;分配>; 出版商选项;
出版商选项.分配器 = 分配;
汽车 出版商 = 网站->;创建出版商<;std_msgs::信息::UInt32>;(
  "allocator_tutorial";, 10, 出版商选项);

rclcpp::带分配器的订阅选项<;分配>; 订阅选项;
订阅选项.分配器 = 分配;
汽车 msg_mem_strat = 标准::共享<;
  rclcpp::消息内存策略::消息内存策略<;
    std_msgs::信息::UInt32, 分配>>;(分配);
汽车 订购者 = 网站->;创建订阅<;std_msgs::信息::UInt32>;(
  "allocator_tutorial";, 10, 回调, 订阅选项, msg_mem_strat);

标准::共享_ptr<;rclcpp::内存策略::内存策略>; 内存策略 =
  标准::共享<;内存分配策略<;分配>>;(分配);

rclcpp::执行器选项 选项;
选项.内存策略 = 内存策略;
rclcpp::执行人::单线程执行器 执行人(选项);

您还必须实例化一个自定义的删除程序和分配器,以便在分配消息时使用:

信息删除器 消息删除程序;
MessageAlloc 消息分配 = *分配;
rclcpp::分配器::为删除程序设置分配器(及样品;消息删除程序, 及样品;消息分配);

将节点添加到执行器后,就可以开始旋转了。我们将使用自定义分配器来分配每条消息:

uint32_t i = 0;
虽然 (rclcpp::好的()) {
  汽车 ptr = MessageAllocTraits::分配(消息分配, 1);
  MessageAllocTraits::建设(消息分配, ptr);
  MessageUniquePtr 信息(ptr, 消息删除程序);
  信息->;数据 = i;
  ++i;
  出版商->;发布(标准::行动(信息));
  rclcpp::sleep_for(10毫秒);
  执行人.旋转();
}

向进程内管道传递分配器

尽管我们在同一个进程中实例化了发布者和订阅者,但我们还没有使用进程内管道。

IntraProcessManager 是一个通常对用户隐藏的类,但为了向其传递自定义分配器,我们需要从 rclcpp Context 中获取该分配器,从而将其公开。IntraProcessManager 使用了多个标准库结构,因此如果没有自定义分配器,它将调用默认的 .

汽车 背景 = rclcpp::背景::获取全球默认上下文();
汽车 选项 = rclcpp::节点选项()
  .背景(背景)
  .使用内部流程通信();
汽车 网站 = rclcpp::节点::共享("allocator_example";, 选项);

请确保在以这种方式构建节点后再实例化发布者和订阅者。

测试和验证代码

你如何知道你的自定义分配器真的被调用了?

最明显的做法是计算调用自定义分配器的 分配调用 函数,并将其与调用 删去.

为自定义分配器添加计数功能非常简单:

空白 * do_allocate(标准::size_t 尺寸, 标准::size_t 路线) 否决
{
  // ...
  num_allocs++;
  // ...
}

空白 do_deallocate(
  空白 * p, 标准::size_t 字节数,
  标准::size_t 路线) 否决
{
  // ...
  num_deallocs++;
  // ...
}

您还可以覆盖全局 删去 经营者

空白 * 操作员 (标准::size_t 尺寸)
{
  如果 (is_running) {
    global_runtime_allocs++;
  }
  返回 标准::调用(尺寸);
}

空白 操作员 删去(空白 * ptr, size_t) 无例外
{
  如果 (ptr != nullptr) {
    如果 (is_running) {
      global_runtime_deallocs++;
    }
    标准::免费的(ptr);
  }
}

空白 操作员 删去(空白 * ptr) 无例外
{
  如果 (ptr != nullptr) {
    如果 (is_running) {
      global_runtime_deallocs++;
    }
    标准::免费的(ptr);
  }
}

其中我们要递增的变量只是全局静态整数,而 is_running 是一个全局静态布尔值,在调用 后旋.

"(《世界人权宣言》) 可执行示例 打印变量的值。要运行示例可执行文件,请使用

ros2 run demo_nodes_cpp allocator_tutorial

或打开进程内管道运行示例:

ros2 run demo_nodes_cpp allocator_tutorial intra

你应该得到这样的数字

全局 new 在旋转过程中被调用了 15590 次
在旋转过程中,全局删除被调用了 15590 次
在旋转过程中,分配器 new 被调用了 27284 次
在旋转过程中,分配器删除被调用了 27281 次

我们已经捕捉到了执行路径上发生的约 2/3 的分配/重新分配,但剩下的 1/3 来自哪里呢?

事实上,这些分配/重新分配源自本例中使用的底层 DDS 实现。

证明这一点不在本教程的讨论范围之内,但您可以查看作为 ROS 2 持续集成测试一部分运行的分配路径测试,该测试会对代码进行回溯,并找出某些函数调用是源于 rmw 实现还是源于 DDS 实现:

https://github.com/ros2/realtime_support/blob/jazzy/tlsf_cpp/test/test_tlsf.cpp#L41

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

TLSF 分配器

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

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

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

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

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