C++20协程帧需要预留多少内存?

20
首先,我想预测代码的内存使用情况,就像任何负责任的程序员一样。即使我没有决定使用放置new来分配协程框架,这也适用于我(请参见下面的伪代码)。即使我改变了关于放置new所有协程的想法,因此让编译器在堆上分配所有协程,我仍然希望C++语言告诉我将吃掉多少堆内存。
但是,在现实生活中,我正在针对高可靠性和嵌入式环境。可能甚至没有堆,所以...
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()来了解operator new中的sz是多少。

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

这个方法的愚蠢之处(非穷尽列表):

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

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

因此,总结一下:

问题概述

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


3
你确定你想在高可靠性和嵌入式环境中使用 co_await 风格的协程吗?在这种情况下,可能没有堆。如果每个字节和时钟周期都很宝贵,我建议避免使用 C++ 机制,因为它们的性能特性是不确定的,就像你会避免使用 dynamic_casttypeid 等一样。 - Nicol Bolas
@NicolBolas 因为 co_awaitstd::thread 更好。 - cs-
1
@cs-:什么?co_await协程不过是一种暂停和恢复函数执行的机制。它们本质上与线程没有任何关系。现在,它们的主要设计目的是促进异步继续使用。但继续假设线程已经存在并且将要执行某些操作,因此您希望在该线程中执行的内容完成后执行此函数。它们不是std::thread或任何其他线程创建机制的替代品。 - Nicol Bolas
1
@NicolBolas 想一想,“我有N个任务要完成。当一个任务因为等待下一个事件而被阻塞时,另一个任务应该运行,直到所有任务都被阻塞。”如果你有第三种方法,那么你今天就是我的英雄,除了(第一种方法)为每个任务生成一个线程并让操作系统安排它们,以及(第二种方法)保持在一个线程上,但将每个任务实现为可继续的函数并自己安排它们。 - cs-
@cs-:从你所描述的来看,几乎不需要为每个“任务”都分配一个线程。你所讨论的是可恢复任务和管理器,用于将它们交给不同的线程处理。虽然 co_await 使得编写和推理这样的内容变得更加容易,但可恢复任务并行不像是一个新的研究领域。传统的方法往往涉及显式继续函数或完整的纤维(比任何 co_await 协程的堆栈都要大)。co_await非常适合您的需求,但您必须接受与其相伴随的缺乏控制。 - Nicol Bolas
3个回答

15

在C++20协程标准化期间,这个问题经过了长时间的讨论。协程帧的布局和大小直到优化器完成其工作后才能确定,并将该信息提供给前端需要对所有现有编译器进行基本的重新架构。实现者报告称,甚至 (有用的) 上界也是不可行的。

有关如何在不允许动态内存分配的环境中使用协程的方法,请参见 P1365R0 的第2部分和第4部分。


@Nicol Bolas:我认为这不是一个公正的评论。成为协程不仅仅是一个实现细节,因为协程必须具有可等待的返回类型。因此,至少可以从外部说出一个函数何时不是协程。此外,恢复协程与调用它是不同的,因此确保在程序初始化时调用协程非常简单。 - sh-
@sh-: “成为协程不仅仅是一种实现细节,因为协程必须具有可等待的返回类型。” 但是调用者并不知道未来类型是由协程创建的。 std::async 返回的类型可能会在未来的 C++ 版本中变得可等待,但这并不意味着它是通过协程实现的。此外,协程不必返回可等待类型;生成器协程返回类型通常不支持 co_await;您可以通过对返回值调用特定函数来继续协程。 - Nicol Bolas
@Nicol Bolas:关于返回类型,我犯了一个错误:它不需要是可等待的,而是需要有一个“promise_type”类型。无论如何,我的意思是返回类型告诉调用者它是否可以是协程。它并不能保证它是一个协程,因为这样的类型也可以被普通函数返回。但是如果返回类型没有“promise_type”,那么你可以确定你没有处理一个协程。所以成为协程不仅仅是实现细节。返回类型有点透露了它的身份。 - sh-
@sh-:我的观点是,虽然你可以确定如果函数签名没有承诺类型(它不仅基于返回类型),那么该函数不能是协程,但并不一定意味着如果它确实具有承诺类型,则必须是协程。如先前所述,“async”的返回类型可能在未来获得这样的承诺类型,但这并不意味着对“async”的调用一定会调用协程。如何使用返回类型定义是否涉及协程,而您无法在没有实现的情况下知道这一点。 - Nicol Bolas
@Nicol Bolas:返回类型的使用也不能定义这一点。您像普通对象一样使用返回类型:调用成员函数,成员函数通过coroutine_handle恢复协程。因此,唯一确定协程是否实际涉及的方法是查看返回类型是否使用coroutine_handle。但除此之外,您重复了我试图表达的内容。 - sh-
显示剩余4条评论

4
C++语言中没有提供任何工具来查询协程帧的大小。这是由设计上的不可能性所决定的。在C++的co_await协程中,成为协程函数是该函数的实现细节。仅从函数声明就无法知道一个函数是协程还是它只是恰好符合使用各种协程机制的签名。该功能旨在以一种方式工作,即使一个函数是或者不是协程函数,都不会影响你的业务。要确定协程帧的大小,首先需要确定协程的身份。由于该系统的设计使其不可能识别协程,因此无法确定协程帧的大小。

有人可能想要补充一下,boost::asio具有基于库的无栈协程实现,除了具有各种缺点之外,还有一个优点,即状态是显式的,并且其大小在编译时已知。这使得在像OP提到的用例中避免动态分配变得容易。 - sh-

2
正如Nicol Bolas所提到的,无法将其作为constexpr值获取。但对于“普通函数”也是如此。只有一个规则:“不要在堆栈上存储大对象以避免堆栈溢出”。
作为一个经验法则,在第一个续订之后必须可用的本地变量的最大堆存储量是您需要的,最终还需要一些小的“管理字段”来存储续订点(通常是某种int)。
但我们的编译器现在非常聪明,并完全优化了堆分配——所以你不应该太担心它。

当协程的生命周期超过创建它的函数时,无法省略堆分配。 - cs-
1
协程的生命周期总是比“创建函数”更长。在生成器函数(co_yield)中,堆存储通常可以省略,以实现“内联”->变量移动到调用者堆栈帧。 - Bernd

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接