【问题标题】:How much memory must be reserved for a C++20 coroutine frame?必须为 C++20 协程帧保留多少内存?
【发布时间】:2020-10-23 13:45:29
【问题描述】:

首先我想预测我的代码的内存使用情况,就像任何负责任的程序员应该做的那样。即使我 决定使用 placement new 分配我的协程帧,这也适用,就像我一样(见下面的伪代码)。 即使我改变了关于放置新所有协程的想法,因此我让编译器在堆上分配我的所有协程,我仍然希望 C++ 语言告诉我我要堆多少吃饱了。

但是,IRL,我的目标是高可靠性和嵌入式环境。甚至可能没有堆,所以...

struct coroutine_return_type
{
  struct promise_type
  {
    void *operator new(std::size_t sz, char *buf, std::size_t szbuf)
    {
      if (sz > szbuf)
        throw std::bad_alloc{};
      return buf;
    }
    void operator delete(void *)
    {
    }
    // ...
  };
  // ...
};

coroutine_return_type my_coroutine(char *, std::size_t)
{
  // The arguments, char * and std::size_t,
  // have been fowrarded to promise_type::operator new
  // but here in the coroutine body they aren't used again...
  for ( ; ; )
    co_yield /* something */;
}

struct coroutine_instance_type
{
  char my_coroutine_frame[ /* WHAT? */ ];
  coroutine_return_type my_coroutine_instance;
  coroutine_instance_type()
    : my_coroutine_instance{my_coroutine(my_coroutine_frame, sizeof(my_coroutine_frame))}
  {
    // ...
  }
  // ...
};

我想要什么

我想要一个编译时表达式来返回我的协程大小的上限,以替换 /* WHAT? */

愚蠢的解决方案

有一种明显愚蠢的方法可以(不完全)做我想做的事:

  1. 子类 std::bad_alloc。然后我的operator new 中的throw std::bad_alloc{} 变为throw std::my_bad_alloc{sz}。 catch 块可以调用my_bad_alloc_instance.get_parameter() 来了解szoperator new 中的内容。

  2. 调用my_coroutine(nullptr, 0) 并捕获异常。

这有什么愚蠢的(非详尽列表):

它不是编译时表达式,因为它必须使用throw“返回”它的值,而throw 永远不能在编译时表达式中使用。但是在我的伪代码中替换 /* WHAT? */ 需要是编译时表达式。

这是一个样本,而不是上限。假设协程框架的实际分配大小取决于运行时的条件。 (现在,我不希望在我的 IRL 应用程序中实际出现针对不同运行时条件的不同协程大小,但根据 C++ 标准,这似乎是可能的。)在这种情况下,仅了解实际传递给operator new 的大小是不够的。相反,所需的表达式必须返回一个上限,关于可以传递给operator new

所以,总结一下:

问题摘要

C++ 语言提供了哪些工具来查询协程帧的大小?理想的工具应该是用于为协程分配非堆内存的编译时表达式,或者,同样的工具也可以用于限制堆的数量。

【问题讨论】:

  • "我的目标是高可靠性和嵌入式环境。甚至可能没有堆" 你确定要使用co_await 风格的协程吗在这样的环境中?如果每个字节和周期都那么宝贵,我会避免性能特征不确定的 C++ 机制。就像您要避免使用 dynamic_casttypeid 等一样。
  • @NicolBolas 因为co_await 胜过std::thread
  • @cs-: ...什么? co_await 协程只不过是一种暂停和恢复函数执行的机制。它们本质上与线程无关。现在,它们的主要设计目的是促进异步延续的使用。但是延续从假设一个线程已经存在并且将要做某事开始,所以你希望这个函数在那个线程中执行的事情完成之后执行。它们不能替代 std::thread 或任何其他线程创建机制。
  • @NicolBolas 想一想,“我有 N 项家务要做。当一项家务为下一个事件而阻塞时,另一项家务应该一直运行到所有家务都阻塞为止。”如果你有第三种方式,那么你今天将成为我的英雄,在(第一种方式)为每个杂务生成一个线程并让操作系统调度它们并(第二种方式)保持在一个线程上但将每个实现为可连续的函数和调度之后他们自己。
  • @cs-:你所描述的几乎不需要给每个“家务”赋予它自己的线程。您所说的是可恢复的任务和将它们移交给不同线程的管理器。虽然co_await 使此类事情(更)易于编码和推理,但可恢复任务并行性并不是一个新的研究领域。传统方法往往涉及显式延续函数或成熟的纤程(比任何co_await 协程的堆栈大得多)。 co_await 非常适合您的需求,但您必须接受随之而来的缺乏控制。

标签: c++ c++20 c++-coroutine


【解决方案1】:

在 C++20 协程的标准化过程中对此进行了长时间的辩论。在优化器完成其工作之前,无法确定协程框架的布局和大小,并且将这些信息提供给前端将需要对所有现有编译器进行基本的重新架构。实施者报告说,甚至(有用的)上限都不可行。

请参阅P1365R0 的第 2 部分和第 4 部分,讨论在不允许动态内存分配的环境中使用协程的方法。

【讨论】:

  • 感谢您的论文。我将总结其中最有用的建议:使用堆,但在程序启动时要做的第一件事是创建每个协程,然后永远不要删除它们(也永远不要在堆上分配任何其他东西)。这满足了高可靠性,因为堆的不可靠性来自于尝试在碎片内存中分配大对象。因此,如果您从一开始就从未对堆进行分段,那么您就可以侥幸逃脱。一个理智的分配器甚至可以按顺序分配协程。
  • @cs-: "程序启动时要做的第一件事是创建每个协程,然后永远不要删除它们" 如果一个协程调用另一个协程会发生什么?还是协程递归调用自己(100% OK)?您可以以这种方式编码,但它非常脆弱,需要遵守非常特殊的纪律。你不能只在适当的地方使用co_await;每次使用它时,您都必须考虑所有可能的调用图等等。
  • @NicolBolas 嗯,不...这门学科只针对协程创建,只要您在启动时知道需要什么协程,创建就很简单。协程resumption.不会有额外的限制
  • @cs-:我想你可能对co_await 协程的工作方式感到困惑,因为“创建”是一个非常短暂的过程。当您调用一个函数作为该函数作为协程的实现细节时,就会“创建”一个协程。一个协程被恢复......好吧,只要你的承诺/未来对象要求。因此,如果您正在按照您的描述进行操作,则不能像常规函数那样简单地 调用 (协程应该工作的方式)。您必须遵守纪律,避免以通常调用其他函数的方式调用协程函数;你必须与未来互动。
  • @Nicol Bolas:我不认为这是一个公平的评论。作为协程不仅仅是一个实现细节,因为协程必须有一个可等待的返回类型。所以至少你可以从外面说一个函数不是协程。此外,恢复协程与调用它是不同的,因此确保在程序初始化时调用协程非常简单。
【解决方案2】:

C++语言提供了哪些工具来查询协程帧的大小?

没有。你想要的东西在设计上是不可能的。

co_await C++ 中的协程的设计方式是,协程是函数的实现细节。仅仅从一个函数声明中,不可能知道一个函数是否是一个协程,或者它是否恰好有一个可以使用各种协程机制的签名。该功能旨在以这样一种方式工作,即如果一个函数是或不是协程,它实际上与您无关。

能够确定协程框架的大小首先需要能够识别协程。而且由于系统的设计使得这是不可能的......好吧,你就是这样。

【讨论】:

  • 有人可能想补充一点,boost::asio 有一个基于库的无堆栈协程实现——除了有各种缺点之外——它的优点是状态是显式的,并且它的大小在编译时是已知的.这使得在 OP 提到的用例中避​​免动态分配变得容易。
【解决方案3】:

正如 Nicol Bolas 所提到的,不可能将其作为 constexpr 值。但这对于“正常功能”也是不可能的。只有一条规则“不要在堆栈上存储大对象以避免堆栈溢出”。

作为经验,所需堆存储的最大值是在第一次继续之后必须可用的局部变量的大小,最终是一些小的“管理字段”来存储连接点(通常是某种整数)。

但我们的编译器现在非常聪明,可以完全优化堆分配 - 所以您不必太担心。

【讨论】:

  • 当协程的寿命必须比创建它的函数长时,堆分配不能被忽略。
  • 协程的寿命总是比“创建函数”长。在“内联”-> 变量被移动到调用者堆栈帧之后,在生成器函数 (co_yield) 的情况下,堆存储通常可以省略
猜你喜欢
  • 2015-08-04
  • 2021-07-22
  • 1970-01-01
  • 1970-01-01
  • 2012-03-25
  • 1970-01-01
  • 2010-12-22
  • 2011-08-29
相关资源
最近更新 更多