实现自定义内存分配器

本教程将教您如何为发布者和订阅者集成自定义分配器,以便在执行ROS节点时永远不会调用默认的堆分配器。本教程的代码可用 here[待校准@6887]

背景

假设您想要编写实时安全代码,并且您已经听说了在实时关键部分调用 "new" 的许多危险,因为大多数平台上的默认堆分配器是不确定的。 [待校准@6888]

默认情况下,许多cstandard标准库结构会随着其增长隐式分配内存,例如 “std:: vector”。然而,这些数据结构也接受 "Allocator" 模板参数。如果您为这些数据结构之一指定自定义分配器,它将为您使用该分配器而不是系统分配器来增加或缩小数据结构。您的自定义分配器可以在堆栈上预先分配一个内存池,这可能更适合实时应用程序。 [待校准@6889]

在ROS 2 cclient客户端库 (rclcpp) 中,我们遵循与cstandard标准库类似的理念。发布者、订阅者和执行程序接受分配器模板参数,该参数控制该实体在执行期间进行的分配。 [待校准@6890]

编写分配器 [待校准@6891]

要编写与ROS 2的分配器接口兼容的分配器,您的分配器必须与c standard标准库分配器接口兼容。 [待校准@6892]

C11 11库提供了一些调用ed allocator_traits 的东西。C11 11标准规定,自定义分配器只需要满足用于以标准方式分配和解除分配内存的最小要求集。 allocator_traits 是一种通用结构,它基于用最小要求编写的分配器来填充分配器的其他质量。 [待校准@6893]

例如,自定义分配器的以下声明将满足 allocator_traits (当然,您仍然需要在此结构中实现声明的函数): [待校准@6894]

template <class T>
struct custom_allocator {
  using value_type = T;
  custom_allocator() noexcept;
  template <class U> custom_allocator (const custom_allocator<U>&) noexcept;
  T* allocate (std::size_t n);
  void deallocate (T* p, std::size_t n);
};

template <class T, class U>
constexpr bool operator== (const custom_allocator<T>&, const custom_allocator<U>&) noexcept;

template <class T, class U>
constexpr bool operator!= (const custom_allocator<T>&, const custom_allocator<U>&) noexcept;

然后,您可以访问由 allocator_traits 填写的其他函数和分配器成员,如: “std:: allocator_properties <custom_allocator<T >>:: 构造 (...)” [待校准@6895]

要了解 allocator_traits 的全部功能,请参见https://en.cppreference.com/w/cpp/memory/ 分配器特性。 [待校准@6896]

然而,一些只支持部分c11 11的编译器,如GCC 4.8,仍然需要分配器实现大量样板代码来使用标准库结构,如向量和string因为这些结构内部不使用 allocator_traits 。因此,如果您使用的编译器支持部分c11 11,您的分配器将需要看起来更像这样: [待校准@6897]

template<typename T>
struct pointer_traits {
  using reference = T &;
  using const_reference = const T &;
};

// Avoid declaring a reference to void with an empty specialization
template<>
struct pointer_traits<void> {
};

template<typename T = void>
struct MyAllocator : public pointer_traits<T> {
public:
  using value_type = T;
  using size_type = std::size_t;
  using pointer = T *;
  using const_pointer = const T *;
  using difference_type = typename std::pointer_traits<pointer>::difference_type;

  MyAllocator() noexcept;

  ~MyAllocator() noexcept;

  template<typename U>
  MyAllocator(const MyAllocator<U> &) noexcept;

  T * allocate(size_t size, const void * = 0);

  void deallocate(T * ptr, size_t size);

  template<typename U>
  struct rebind {
    typedef MyAllocator<U> other;
  };
};

template<typename T, typename U>
constexpr bool operator==(const MyAllocator<T> &,
  const MyAllocator<U> &) noexcept;

template<typename T, typename U>
constexpr bool operator!=(const MyAllocator<T> &,
  const MyAllocator<U> &) noexcept;

编写示例main [待校准@6898]

编写有效的calloc分配器后,必须将其作为共享指针传递给发布者、订阅者和执行程序。 [待校准@6899]

auto alloc = std::make_shared<MyAllocator<void>>();
auto publisher = node->create_publisher<std_msgs::msg::UInt32>("allocator_example", 10, alloc);
auto msg_mem_strat =
  std::make_shared<rclcpp::message_memory_strategy::MessageMemoryStrategy<std_msgs::msg::UInt32,
  MyAllocator<>>>(alloc);
auto subscriber = node->create_subscription<std_msgs::msg::UInt32>(
  "allocator_example", 10, callback, nullptr, false, msg_mem_strat, alloc);

std::shared_ptr<rclcpp::memory_strategy::MemoryStrategy> memory_strategy =
  std::make_shared<AllocatorMemoryStrategy<MyAllocator<>>>(alloc);
rclcpp::executors::SingleThreadedExecutor executor(memory_strategy);

您还需要使用分配器分配沿着执行代码路径传递的任何消息。 [待校准@6900]

auto alloc = std::make_shared<MyAllocator<void>>();

实例化节点并将执行程序添加到节点后,就该旋转了: [待校准@6901]

uint32_t i = 0;
while (rclcpp::ok()) {
  msg->data = i;
  i++;
  publisher->publish(msg);
  rclcpp::utilities::sleep_for(std::chrono::milliseconds(1));
  executor.spin_some();
}

将分配器传递给进程内管道 [待校准@6902]

即使我们在同一进程中实例化了发布者和订阅者,我们也尚未使用进程内管道。 [待校准@6903]

内部进程管理器是一个通常对用户隐藏的类,但是为了将自定义分配器传递给它,我们需要通过从rclcpp上下文中获取它来公开它。Intrprocessmanager使用多个标准库结构,因此如果没有自定义分配器,它将调用默认的new。 [待校准@6904]

auto context = rclcpp::contexts::default_context::get_global_default_context();
auto ipm_state =
  std::make_shared<rclcpp::intra_process_manager::IntraProcessManagerState<MyAllocator<>>>();
// Constructs the intra-process manager with a custom allocator.
context->get_sub_context<rclcpp::intra_process_manager::IntraProcessManager>(ipm_state);
auto node = rclcpp::Node::make_shared("allocator_example", true);

以这种方式构造节点后,确保实例化发布者和订阅者。 [待校准@6905]

测试和验证代码 [待校准@6906]

您怎么知道您的自定义分配器实际上正在被调用? [待校准@6907]

显而易见的事情是计算对自定义分配器的 allocatedeallocate 函数的调用,并将其与对 newdelete 的调用进行比较。 [待校准@6908]

向自定义分配器添加计数非常容易: [待校准@6909]

T * allocate(size_t size, const void * = 0) {
  // ...
  num_allocs++;
  // ...
}

void deallocate(T * ptr, size_t size) {
  // ...
  num_deallocs++;
  // ...
}

您还可以覆盖全局新建和删除运算符: [待校准@6910]

void operator delete(void * ptr) noexcept {
  if (ptr != nullptr) {
    if (is_running) {
      global_runtime_deallocs++;
    }
    std::free(ptr);
    ptr = nullptr;
  }
}

void operator delete(void * ptr, size_t) noexcept {
  if (ptr != nullptr) {
    if (is_running) {
      global_runtime_deallocs++;
    }
    std::free(ptr);
    ptr = nullptr;
  }
}

我们递增的变量只是全局静态整数,而 is_running 是全局静态bool,在调用 spin 之前就被切换。 [待校准@6911]

[需手动修复的语法] example executable 打印变量的值。要运行示例可执行文件,请使用: [待校准@6912]

allocator_example

或者,使用进程内管道运行示例: [待校准@6913]

allocator_example intra-process

你应该得到如下数字: [待校准@6914]

Global new was called 15590 times during spin
Global delete was called 15590 times during spin
Allocator new was called 27284 times during spin
Allocator delete was called 27281 times during spin

我们发现在执行路径上发生了大约2/3的分配/解除分配,但是剩下的1/3来自哪里? [待校准@6915]

事实上,这些分配/解除分配源于本示例中使用的底层DDS实现。 [待校准@6916]

证明这超出了本教程的范围,但是您可以检查作为ROS 2持续集成测试一部分运行的分配路径的测试,它回溯代码并确定某些函数调用是起源于rmw实现还是DDS实现: [待校准@6917]

https://github.com/ros2/realtime_support/blob/狡猾/tlsf_cpp/测试/test_tlsf.cpp # L41 [待校准@6918]

请注意,此测试不是使用我们刚刚创建的自定义分配器,而是TLSF分配器 (见下文)。 [待校准@6919]

TLSF分配器 [待校准@6920]

ROS 2为TLSF (两级分离匹配) 分配器提供支持,该分配器旨在满足实时要求: [待校准@6921]

https://github.com/ros2/realtime_support/树/狡猾/tlsf_cpp [待校准@6922]

有关TLSF的更多信息,请参阅http://www.gii.upv.es/tlsf/ [待校准@6923]

请注意,TLSF分配器是根据双GPL/LGPL许可获得许可的。 [待校准@6924]

一个完整的工作示例使用TLSF分配器: https://github.com/ros2/realtime_support/blob/狡猾/tlsf_cpp/例子/allocator_example.cpp [待校准@6925]