code

스택리스 C ++ 20 코 루틴이 문제입니까?

codestyles 2020. 12. 30. 08:11
반응형

스택리스 C ++ 20 코 루틴이 문제입니까?


다음을 기반으로 C ++ 20의 코 루틴이 스택이없는 것처럼 보입니다.

https://en.cppreference.com/w/cpp/language/coroutines

여러 가지 이유로 우려됩니다.

  1. 임베디드 시스템에서 힙 할당은 종종 허용되지 않습니다.
  2. 낮은 수준의 코드에서는 co_await 중첩이 유용 할 것입니다 (스택리스 코 루틴이 이것을 허용한다고 생각하지 않습니다).

스택리스 코 루틴을 사용하면 최상위 루틴 만 일시 중단 될 수 있습니다. 해당 최상위 루틴에 의해 호출 된 루틴은 자체적으로 일시 중지되지 않을 수 있습니다. 이것은 범용 라이브러리 내의 루틴에서 일시 중지 / 재개 작업을 제공하는 것을 금지합니다.

https://www.boost.org/doc/libs/1_57_0/libs/coroutine/doc/html/coroutine/intro.html#coroutine.intro.stackfulness

  1. 사용자 지정 할당 자와 메모리 풀링이 필요하기 때문에 더 자세한 코드.

  2. 작업이 운영 체제가 메모리 풀링없이 일부 메모리를 할당 할 때까지 기다리는 경우 더 느립니다.

이러한 이유를 감안할 때, 현재 코 루틴이 무엇인지에 대해 제가 정말 틀렸기를 바랍니다.

질문은 세 부분으로 구성됩니다.

  1. C ++가 스택리스 코 루틴을 사용하는 이유는 무엇입니까?
  2. 스택리스 코 루틴에 상태를 저장하기위한 할당과 관련하여. 코 루틴 생성에 일반적으로 사용되는 힙 할당을 피하기 위해 alloca ()를 사용할 수 있습니까?

코 루틴 상태는 비 배열 연산자 new를 통해 힙에 할당됩니다. https://en.cppreference.com/w/cpp/language/coroutines

  1. C ++ 코 루틴에 대한 내 가정이 잘못된 이유는 무엇입니까?

편집하다:

나는 지금 코 루틴에 대한 cppcon 회담을 진행하고 있으며, 내 질문에 대한 답을 찾으면 게시 할 것입니다 (지금까지는 아무것도 없습니다).

CppCon 2014 : Gor Nishanov "Await 2.0 : Stackless Resumable Functions"

https://www.youtube.com/watch?v=KUhSjfSbINE

CppCon 2016 : James McNellis "C ++ 코 루틴 소개"

https://www.youtube.com/watch?v=ZTqHjjm86Bw


앞으로 :이 게시물이 단지 "코 루틴"이라고 말할 때 저는 특정 C ++ 20 기능이 아니라 코 루틴 개념언급하고 있습니다. 이 기능에 대해 이야기 할 때 " co_await"또는 "co_await 코 루틴"이라고합니다.

동적 할당

Cppreference는 때때로 표준보다 더 느슨한 용어를 사용합니다. co_await기능으로 동적 할당이 '필요'합니다. 이 할당이 힙에서 나왔는지 아니면 정적 메모리 블록에서 나왔는지 또는 할당 제공자에게 문제가되는 것입니다. 이러한 할당은 임의의 상황에서 제거 될 수 있지만 표준에서이를 설명하지 않기 때문에 co_await 코 루틴이 동적으로 메모리를 할당 할 수 있다고 가정해야합니다.

co_await 코 루틴에는 사용자가 코 루틴 상태에 대한 할당을 제공하는 메커니즘이 있습니다. 따라서 원하는 특정 메모리 풀에 대해 힙 / 프리 저장소 할당을 대체 할 수 있습니다.

co_await기능은 사용 가능한 모든 개체 및 기능에 대한 사용 지점에서 자세한 정보 제거 하도록 잘 설계되었습니다 co_await. co_await기계는 믿을 수 없을만큼 여러 종류의 객체 사이의 상호 작용이 많은, 복잡하고 복잡한된다. (가) / 이력서 포인트를 정지에서, 그것은 언제나 처럼 보인다 co_await <some expression>. 기다릴 수있는 객체와 약속에 할당 자 지원을 추가하려면 약간의 장황함이 필요하지만 그 장황함은 그러한 것들이 사용되는 곳 밖에서 살아갑니다.

alloca코 루틴을 사용 하는 것은 ... 대부분co_await. 이 기능에 대한 논의는 기능을 숨기려고하지만 문제는 co_await기능이 비동기 사용을 위해 설계 되었기 때문입니다 . 이것이 의도 된 목적입니다. 함수 실행을 중지하고 잠재적으로 다른 스레드에서 해당 함수의 재개를 예약 한 다음 결국 생성 된 값을 코 루틴을 호출 한 코드에서 다소 멀어 질 수있는 일부 수신 코드로 이동합니다.

alloca코 루틴의 호출자가 다른 스레드에서 값을 생성 할 수 있도록 무엇이든 할 수 있도록 허용 / 권장되기 때문에 특정 사용 사례에는 적합하지 않습니다. alloca따라서에 의해 할당 된 공간 은 더 이상 존재하지 않으며 그 안에있는 코 루틴에게는 좋지 않습니다.

또한 이러한 시나리오의 할당 성능은 일반적으로 다른 고려 사항에 의해 왜소해질 수 있습니다. 프로세스가 제공하고 있습니다. 따라서이 경우 동적 할당이 필요하다는 사실은 실질적인 고려 사항이 아닙니다.

이제이 있습니다 원위치 할당이 적합 할 상황. 생성기 사용 사례는 기본적으로 함수를 일시 중지하고 값을 반환 한 다음 함수가 중단 된 지점을 선택하고 잠재적으로 새 값을 반환하려는 경우입니다. 이러한 시나리오에서 코 루틴을 호출하는 함수의 스택은 확실히 주변에있을 것입니다.

co_await이러한 시나리오를 지원 co_yield하지만 최소한 표준 측면에서 최적이 아닌 방식으로 지원합니다. 이 기능은 업 앤 아웃 서스펜션을 위해 설계 되었기 때문에 서스펜션 다운 코 루틴으로 전환하면 동적 일 필요가없는이 동적 할당을 갖는 효과가 있습니다.

이것이 표준이 동적 할당을 요구하지 않는 이유입니다. 컴파일러가 제너레이터 사용 패턴을 감지 할만큼 똑똑하다면 동적 할당을 제거하고 로컬 스택에 공간을 할당 할 수 있습니다. 그러나 다시 말하지만 이것은 컴파일러가 할 수 있는 것이지 반드시해야하는 것이 아닙니다.

이 경우 alloca기반 할당이 적절합니다.

표준에 들어간 방법

짧은 버전은 그 뒤에있는 사람들이 작업을했고 대안 뒤에있는 사람들은하지 않았기 때문에 표준에 들어갔다는 것입니다.

모든 코 루틴 아이디어는 복잡하며 항상 구현 가능성에 대한 질문이있을 것입니다. 예를 들어, " 재개 가능한 기능 "제안은 멋져 보였고 표준에서보고 싶었을 것입니다. 그러나 실제로 컴파일러에서 구현 한 사람은 없습니다 . 그래서 아무도 그것이 실제로 당신이 할 수있는 일이라는 것을 증명할 수 없었습니다. 아 물론, 그것은 소리를 구현 가능하지만 그것이 의미하지 않는다 이다 구현 가능.

마지막으로 "구현 가능한 사운드"가 기능 채택의 기초로 사용되었을 때 일어난 일을 기억하십시오 .

구현할 수 있는지 모르는 경우 표준화하고 싶지 않습니다. 그리고 의도 한 문제가 실제로 해결되는지 여부를 모르는 경우 무언가를 표준화하고 싶지 않습니다.

Microsoft의 Gor Nishanov와 그의 팀은 co_await. 그들은 몇 년 동안 이를 수행하여 구현 등을 개선했습니다. 다른 사람들은 실제 프로덕션 코드에서 구현을 사용했고 그 기능에 상당히 만족하는 것처럼 보였습니다. Clang은 심지어 그것을 구현했습니다. 개인적으로 싫지만 성숙한 기능 이라는 co_await것은 부인할 수 없습니다 .

대조적으로, 1 년 전에 경쟁 아이디어로 제기 된 "코어 코 루틴"대안은 구현하기 어려웠 기 때문에 부분적으로co_await 견인력을 얻지 못했습니다 . 이것이 채택 된 이유 입니다. 사람들이 원하고 코드를 개선 할 수있는 능력이 입증 된 성숙하고 건전한 도구이기 때문입니다.co_await

co_await모두를위한 것은 아닙니다. 개인적으로 섬유는 내 사용 사례에 훨씬 더 잘 작동하므로 많이 사용하지 않을 것입니다. 그러나 특정 사용 사례에는 매우 좋습니다.


저는 32kb의 RAM이있는 작은 하드 실시간 ARM Cortex-M0 타겟에서 스택리스 코 루틴을 사용합니다. 여기서 힙 할당자가 전혀 없습니다. 모든 메모리는 정적으로 사전 할당됩니다. 스택리스 코 루틴은 성가신 코 루틴이며 이전에 사용했던 스택 형 코 루틴은 제대로 작동하는 데 어려움이 있었으며 본질적으로 구현 별 동작에 전적으로 기반한 해킹이었습니다. 그런 혼란에서 표준을 준수하는 이식 가능한 C ++로 이동하는 것은 훌륭했습니다. 누군가 돌아갈 것을 제안 할지도 모른다는 생각에 떨린다.

  • 스택리스 코 루틴은 힙 사용을 의미하지 않습니다 . 코 루틴 프레임이 할당되는 방식 완전히 제어 할 수 있습니다 (프로 미스 void * operator new(size_t)유형의 멤버를 통해 ).

  • co_await 잘 중첩 될 수 있습니다 . 사실 일반적인 사용 사례입니다.

  • 스택 형 코 루틴은 해당 스택도 어딘가에 할당해야 하며 스레드의 기본 스택을 사용할 수 없다는 것은 아이러니 할 수 있습니다 . 이러한 스택은 힙에서 블록을 가져온 다음 세분화하는 풀 할당자를 통해 힙에 할당됩니다.

  • 스택리스 코 루틴 구현은 operator new프라 미스가 전혀 호출되지 않도록 프레임 할당을 제거 할 수있는 반면, 스택 형 코 루틴은 필요 여부에 관계없이 항상 코 루틴에 대한 스택을 할당합니다. 컴파일러가 코 루틴 런타임을 제거하여 (적어도 C / C ++가 아님).

  • 컴파일러가 코 루틴의 수명이 호출자의 범위를 벗어나지 않는다는 것을 증명할 수있는 스택을 사용하여 할당을 정확하게 제거 할 수 있습니다. 그리고 그것이 당신이 사용할 수있는 유일한 방법 alloca입니다. 따라서 컴파일러가 이미 처리합니다. 얼마나 멋진가요!

    이제 컴파일러가 실제로이 제거를 수행 할 필요는 없지만 AFAIK 모든 구현은 "증거"가 얼마나 복잡 할 수 있는지에 대한 몇 가지 정상적인 제한과 함께이 작업을 수행합니다. 어떤 경우에는 결정 가능한 문제 (IIRC)가 아닙니다. 또한 컴파일러가 예상대로 수행했는지 여부를 쉽게 확인할 수 있습니다. 특정 promise 유형을 가진 모든 코 루틴이 중첩 전용이라는 것을 알고 있다면 (작은 임베디드 프로젝트에서 합리적 일뿐만 아니라!) operator newpromise 유형에서 선언 할 수 있지만 선언 할 수는 없습니다. 정의하면 컴파일러가 "goofed up"된 경우 코드가 링크되지 않습니다.

    특정 컴파일러 구현에 pragma를 추가하여 컴파일러가이를 증명할만큼 영리하지 않더라도 특정 코 루틴 프레임이 이스케이프되지 않음을 선언 할 수 있습니다. 내 사용으로 인해 아직이를 작성하는 사람이 있는지 확인하지 않았습니다. 경우는 컴파일러가 항상 옳은 일을 할만큼 합리적입니다.

    호출자에게서 돌아온 후에는 alloca로 할당 된 메모리를 사용할 수 없습니다. alloca실제로 의 사용 사례는 gcc의 가변 크기 자동 배열 확장을 표현하는 약간 더 이식 가능한 방법입니다.

In essentially all implementations of stackful coroutines in C-like lanaguages, the one and only supposed "benefit" of stackfull-ness is that the frame is accessed using the usual base-pointer-relative addressing, and push and pop where appropriate, so "plain" C code can run on this made-up stack, with no changes to code generator. No benchmarks support this mode of thinking, though, if you have lots of coroutines active - it's a fine strategy if there's a limited number of them, and you have the memory to waste to start with.

Stack has to be overallocated, decreasing locality of reference: a typical stackful coroutine uses a full page for the stack at the minimum, and the cost of making this page available is not shared with anything else: the single coroutine has to bear it all. That's why it was worthwhile to develop stackless python for multiplayer game servers.

If there's a couple of couroutines only - no problem. If you've got thousands of network requests all handled by stackful coroutines, with a light networking stack that doesn't impose overhead that monopolizes the performance, the performance counters for cache misses will make you cry. As Nicol has stated in the other answer, this becomes somewhat less relevant the more layers there are between the coroutine and whatever asynchronous operation it's handling.

It has been long since any 32+-bit CPU had performance benefits inherent to memory access via any particular addressing mode. What matters is cache-friendly access patterns and leveraging prefetch, branch prediction and speculative execution. Paged memory and its backing store are just two further levels of cache (L4 and L5 on desktop CPUs).

  1. Why would C++ choose to use stackless coroutines? Because they perform better, and no worse. On the performance side, there can be only benefits to them. So it's a no-brainer, performance-wise, to just use them.

  2. Can I use alloca() to avoid any heap allocations that would normally be used for the coroutine creation. No. It'd be a solution to a nonexistent problem. Stackful coroutines don't actually allocate on the existing stack: they create new stacks, and those are allocated on the heap by default, just as C++ coroutine frames would be (by default).

  3. Are my assumptions about the c++ coroutines wrong, why? See above.

  4. More verbose code because of the need for custom allocators and memory pooling. If you want stackful coroutines to perform well, you'll be doing the same thing to manage the memory areas for the stacks, and it turns out that it's even harder. You need to minimize memory waste, and thus you need to minimally overallocate the stack for the 99.9% use case, and deal somehow with coroutines that exhaust this stack.

    One way I have dealt with it in C++ was by doing stack checks in branch points where code analysis indicates more stack may be needed, then if the stack would overflow, an exception was thrown, the coroutine's work undone (the design of the system had to support it!), and then the work restarted with more stack. It's an easy way to quickly lose benefits of tightly packed stack-fuls. Oh, and I had to provide my own __cxa_allocate_exception for that to work. Fun, eh?

One more anecdote: I'm playing with using coroutines inside Windows kernel-mode drivers, and there the stacklessness does matter - to the extent that if the hardware allows, you can allocate the packet buffer and the coroutine's frame together, and these pages are pinned when they are submitted to the network hardware for execution. When the interrupt handler resumes the coroutine, the page is there, and if the network card allows, it could even prefetch it for you so it'll be in the cache. So that works well - it's just one use case, but since you wanted embedded - I've got embedded :).

It's perhaps not common to think of drivers on desktop platforms as "embedded" code, but I see lots of similarities, and an embedded mindset is needed. Last thing you want is kernel code that allocates too much, especially if it would add per-thread overhead. A typical desktop PC has a few thousand threads present, and a lot of them are there to handle I/O. Now imagine a diskless system that uses iSCSI storage. On such a system, anything I/O bound that's not bound to USB or GPU will be bound to the network hardware and the networking stack.

Finally: Trust benchmarks, not me, and read Nicol's answer too!. My perspective is shaped by my use cases - I can generalize, but I claim no first-hand experience with coroutines in "generalist" code where performance is of less concern. Heap allocations for stackless coroutines are very often hardly noticeable in performance traces. In general-purpose application code, it's rarely going to be a problem. It does get "interesting" in library code, and some patterns have to be developed to allow the library user to customize this behavior. These patterns will be found and popularized as more libraries use C++ coroutines.


stackless coroutines

  • stackless coroutines (C++20) do code transformation (state machine)
  • stackless in this case means, that the application stack is not used to store local variables (for instance variables in your algorithm)
  • otherwise the local variables of the stackless coroutine would be overwritten by invocations of ordinary functions after suspending the stackless coroutine
  • stackless coroutines do need memory to store local variables too, especially if the coroutine gets suspended the local variables need to be preserved
  • for this pupose stackless coroutines allocate and use a so-called activation record (equivalent to a stack frame)
  • the activation record must not reside on the the thread's primary stack
  • suspending from a deep call stack is only possible if all functions in between are stackless coroutines too (viral; otherwise you would get a corrupted stack)
  • a stackless coroutine can not outlive its caller/creator
  • some clang developers are sceptical that the Heap Allocation eLision Optimization (HALO) can always be applied

stackfull coroutines

  • in its essence a stackful coroutine simply switches stack and instruction pointer
  • allocate a side-stack that works like a ordinary stack (storing local variables, advancing the stack pointer for called functions)
  • the side-stack needs to be allocated only once (can also be pooled) and all subsequent function calls are fast (because only advancing the stack pointer)
  • each stackless coroutines requires its own activation record -> called in a deep call chain a lot activation records have to be created/allocated
  • stackful coroutines allow to suspend from a deep call chain while the functions in between can be ordinary functions (not viral)
  • a stackfull coroutine can outlive its caller/creator
  • one version of the skynet benchmarks spawns 1 million stackful coroutines and shows that stackful coroutiens are very efficient (outperforming version using threads)
  • a version of the skynet benchmark using stackless coroutiens was not implemented yet
  • boost.context represents the thread's primary stack as a stackful coroutine/fiber - even on ARM
  • boost.context supports on demand growing stacks (GCC split stacks)

ReferenceURL : https://stackoverflow.com/questions/57163510/are-stackless-c20-coroutines-a-problem

반응형