[C++ 20] Coroutines #1 - 배경 지식 편
C++ 2024. 9. 21. 17:38 |코루틴의 원리를 이해하기 위한 배경지식
1. 연관된 용어 정리
함수(Function) : 정의된 작업을 수행하는 독립적인 코드 블록
int mul(int a, int b);
int mul(int a, int b) {
return a * b;
}
루틴(Routine) : 정해진 작업 절차(Sequence)를 의미하며 일련의 명령들의 집합체
int mul(int,int) PROC
mov DWORD PTR [rsp+16], edx ; 두 번째 인자(b)를 스택에 저장
mov DWORD PTR [rsp+8], ecx ; 첫 번째 인자(a)를 스택에 저장
mov eax, DWORD PTR a$[rsp] ; a 값을 eax 레지스터로 로드
imul eax, DWORD PTR b$[rsp] ; eax와 b 값을 곱하여 결과를 eax에 저장
ret 0 ; 함수 반환
int mul(int,int) ENDP
명령(Instruction) : 기계의 동작(behavior)을 추상화한 것, 즉, 기계 상태의 전이(Transition) 동작 지시
호출(Invocation) : 루틴의 시작점으로 Jump
활성화(Activation) : 루틴 안의 임의 지점으로 Jump
중단(Suspension) : 현재 루틴을 종결하지 않고, 다른 루틴의 임의 지점으로 Jump
종결(Finalization) : 현재 루틴의 끝에 도달하면 루틴 상태의 소멸 및 정리
서브 루틴(Subroutine)이란?
- 호출(Invocation) or 종결(Finalization)할 수 있는 루틴을 말한다.
- 호출되면 실행을 시작하고 완료 후 호출자에게 제어를 반환한다.
- 함수는 서브 루틴의 범주에 포함된다.
// 서브루틴(Subroutine): 호출/종결할 수 있는 루틴
// get_zero 서브루틴 정의
int get_zero(void) PROC
xor eax, eax ; eax 레지스터를 0으로 초기화
ret 0 ; 0을 반환하고 서브루틴 종료 : Return(Finalize)
int get_zero(void) ENDP
// 메인 루틴
main PROC
$LN3:
mov QWORD PTR [rsp+16], rdx
mov DWORD PTR [rsp+8], ecx
sub rsp, 40 ; 스택 공간 확보
call int get_zero(void) ; get_zero 서브루틴 호출 : Call(Invoke)
add rsp, 40 ; 스택 정리
ret 0 ; 메인 루틴 종료
main ENDP
// _formal$ = 48
// _formal$ = 56
// 설명:
// 1. 서브루틴은 호출(Invoke)과 종결(Finalize)이 가능한 루틴입니다.
// 2. get_zero 서브루틴은 0을 반환하는 간단한 함수입니다.
// 3. 메인 루틴에서 get_zero 서브루틴을 호출(call)합니다.
// 4. 서브루틴 호출 전후로 스택 조정 작업이 이루어집니다.
// 5. ret 명령어로 각 루틴이 종료(Finalize)됩니다.
2. 그래서 서브루틴이 코루틴과 무슨 관련이 있는 거야?
1) 서브 루틴(Subroutines)
- 서브 루틴은 진입점이 하나이며, 종료 시 항상 호출자로 돌아간다.
- 실행 중 상태를 유지하지 않으며, 매번 호출 시 처음부터 실행된다.
- 호출 스택을 사용하여 호출-반환 구조를 관리한다.
- 서브루틴 실행 흐름 :
호출 → 실행 → 반환의 단일 흐름 - Subroutine과 Caller 간에 별다른 상태를 유지할 필요가 없어 호출 스택(Call Stack)만 사용하면 충분하다.
2) 코루틴(Coroutine)
- 실행을 중간에 중단(Suspend)해 두고
나중에 중단된 지점(Suspesion Point)부터 재개(Resume)할 수 있는 일반화된 서브루틴이다. - 때문에 여러 중단점과 진입점을 가질 수 있다.
즉, 실행 흐름의 상태를 유지하며, 중단된 지점부터 재개할 수 있다. - 코루틴은 자발적으로 실행을 중단하고 제어를 Caller측에 넘기는 방식을 사용한다.
즉, 제어를 명시적으로 양보할 때만 다른 작업이 실행되는 방식이기 때문에
'협력적 멀티태스킹을 가능케 한다'라고 표현 한다. - 코루틴 실행 흐름 :
호출 → 실행 → 중단 → ... → 재개 → 실행 → 중단 → ... → 완료의 복잡한 흐름 - 코루틴은 중단과 재개 사이에 상태를 유지해야 하기 때문에 코루틴 프레임이 추가로 필요하다.
2. 호출 스택(Call Stack)
1) 호출 스택(Call Stack)
- 호출 스택은 함수 프레임을 관리하는 방법 중 하나다.
호출 시 프레임을 Push하여 스택에 새로운 함수 프레임 추가하고,
반환 시 프레임을 Pop하여 스택에서 함수 프레임 제거한다. - 함수 호출 과정 예시:
1. main() 함수 시작 : main함수 프레임 push
2. main() 프레임에서 foo() 함수 호출 : foo함수 프레임 push
3. foo() 함수내에서 bar() 함수 호출 : bar함수 프레임 push
4. bar() 함수 실행 완료 후 반환 : bar함수 프레임 pop
5. foo() 함수 실행 완료 후 반환 : foo함수 프레임 pop
6. main() 함수에서 다시 bar()함수 호출 : bar함수 프레임 push
7. bar()함수 실행 완료 후 반환 : bar함수 프레임 pop
2) 함수 종류
- 프레임 함수(Frame Function)
: 프레임 함수는 자신만의 스택 프레임(Stack Frame)**을 만드는 함수다.
프레임 함수가 하는 일은 다음과 같다.
- 지역 변수를 저장
- 다른 함수를 호출할 때 그 함수의 리턴 주소와 매개변수 등을 저장하기 위한 스택 공간 할당
- 비휘발성 레지스터를 저장
- 예외 발생 시 이를 복구하거나 처리하기 위해 필요한 정보 저장
원리는 다음과 같다.
함수의 *프롤로그(prologue)에서 스택 포인터(RSP 혹은 ESP)를 조정하여 스택 프레임을 설정하고,
*에필로그(epilogue)에서 스택 포인터를 복원하여 프레임을 제거하는 식이다. - 리프 함수 (Leaf Function)
: 리프 함수는 다른 함수를 호출하지 않는 함수를 의미한다.
리프 함수는 다른 함수를 호출하지 않기 때문에 별도의 스택 프레임을 만들지 않아도 된다.
즉, 스택 프레임 없이도 동작할 수 있다.
리프 함수는 성능 최적화에 유리하며,
함수 호출 시 스택 포인터의 변경을 최소화하기 때문에 더 효율적이다. - 함수의 프롤로그(Prologue)
: 프롤로그는 함수가 시작될 때 수행되는 코드로, 스택을 설정하는 작업을 한다.
프롤로그에서 하는 일은
호출자(Caller)의 레지스터와 반환 주소를 스택에 저장,
스택 포인터를 이동하여 스택 프레임을 할당,
함수에서 사용할 지역 변수 공간을 스택에 확보이다.
예시 (x86 어셈블리) :
push rbp ; 이전 베이스 포인터를 저장
mov rbp, rsp ; 스택 포인터를 새 베이스 포인터로 설정
sub rsp, 0x20 ; 지역 변수 공간을 스택에 할당
예시 (x64 어셈블리) :
mov [rsp+8],rcx ; store parameter in shadow space if necessary
push r14 ; save any non-volatile registers to be used
push r13 ;
sub rsp,size ; allocate stack for local variables if needed
lea r13,[bias+rsp] ; use r13 as a frame pointer with an offset
- 함수의 에필로그(Epilogue)
: 에필로그는 함수가 종료될 때 수행되는 코드로, 스택을 정리하는 작업을 한다.
주로 하는 일은
- 스택 포인터를 복원하여 스택 프레임을 해제
- 이전의 레지스터와 리턴 주소를 복원
- 함수 호출을 끝내고 호출자로 복귀
예시 (x86 어셈블리) :
add rsp, 0x20 ; 할당된 스택 프레임 해제
pop rbp ; 이전 베이스 포인터 복원
ret ; 호출한 함수로 복귀
예시 (x64 어셈블리) :
프레임 포인터 레지스터가 있든 없든 사용할 수 있는 형식
lea rsp,[r13-bias] ; this is not part of the official epilogue
add rsp,size ; the official epilogue starts here
pop r13
pop r14
ret
프레임 포인터 레지스터가 있는 경우 사용해야하는 형식
The following can also be used provided that a frame pointer register has been established:
lea rsp,[r13+size-bias]
pop r13
pop r14
ret
3) Call Stack Unwinding
코루틴 함수에서 발생한 예외를 어떻게 처리하는지에 대한
배경 지식을 전달할 목적으로 소개하는 부분이며
몰라도 코루틴 사용에 큰 지장은 없으니 넘어가도 된다.
: 위 그림은 함수 호출 시 스택이 일반적으로 어떻게 사용되는지 보여준다.
함수가 호출되면 8바이트짜리 반환 주소가 스택에 자동으로 푸쉬되며,
함수는 사용할 비휘발성 레지스터를 저장한다.
로컬 변수를 위한 추가 공간도 할당할 수 있으며,
필요한 경우 프레임 포인터 레지스터를 할당할 수도 있다.
unwind data를 만들 때 고려해야 할 스택 프레임에는 두 가지 유형이 있다.
Static 방식 :
스택에 고정된 공간만 할당하고
다른 함수를 호출하는 동안을 제외하고는
함수 본문 내에서 스택 포인터의 값이 고정된 상태로 유지된다.
이 유형의 스택 프레임에서는 프롤로그 끝에 있는 스택 포인터 값이
나중에 설명하는 Unwind primitive 및 macro에서 offset의 기준으로 사용된다.
이때 스택 포인터 값은 16바이트로 정렬되어야 합니다.
Dynamic 방식 :
스택 공간이 동적으로 할당되므로
스택 포인터 값을 정적으로 예측할 수 없고
unwind offset의 베이스로 사용할 수 없다.
이러한 상황에서는 프레임 포인터 레지스터를 사용하여 이 기준 주소를 제공해야 한다.
여기서 unwind offset의 기준은 스택에서 고정 할당 영역의 하단이며,
일반적으로 프레임 레지스터가 할당될 때 스택 포인터의 값이다.
이 값은 16바이트 정렬되어야 하며,
오프셋이 있는 unwind macro를 사용하기 전에 할당되어야 한다.
프레임 포인터 레지스터에서 단일 바이트 오프셋(-128 ~ \+127)으로
최대량의 데이터에 액세스 할 수 있도록 하기 위해서는
할당된 영역의 중앙으로 값을 오프셋 하는 것이 일반적이다("바이어스").
프레임 포인터 레지스터와 16바이트의 배수여야 하는 이 오프셋의 신원은
unwind data에 기록되어 스택 프레임 기본 주소를 프레임 레지스터의 값에서 계산할 수 있도록 한다.
3. 그래서 코루틴(Coroutine)은 어떻게 다른데?
쓰레드(Thread)들은 하나의 프로세스에 대하여
공통된 메모리 공간(Data Section, Code Section, Heap 등)을 공유하지만
Stack은 예외이다.
즉, 쓰레드 마다 독립적인 스택 프레임(Stack Frame)을 갖는다.
코루틴도 마찬가지로 독립적인 코루틴 프레임(Coroutine Frame)이 필요하다.
C++ 코루틴은 컴파일러의 도움으로 이를 달성하는 방식을 취하며
스택 프레임과 코루틴 프레임을 이용하여 Caller와 코루틴 간의 제어를 주고 받는다.
즉, 코드 작성자 관점에서의 Context switching을 구현한다.
이 방식 덕분에 코드가 마치 Multi-threading으로 동작하는 것처럼 보인다.
4. 코루틴 프레임(Coroutine Frame)
코루틴은 스택 프레임과 코루틴 프레임을 이용하여 Context switching을 구현한다고 했다.
코루틴 함수는 함수가 반환하지 않아도
코루틴 함수 본문 프레임과 외부(Caller 측) 프레임을 넘나들며 명령을 수행할 수 있다.
코루틴 프레임과 외부 스택 프레임의 중단 포인트(suspension point)를 저장해 놓고
왔다갔다하므로써 Context Switching 느낌을 구현한 것이다.
코루틴 프레임은 2가지 정보를 따로 저장하기 위한 추가 공간이다.
첫째, 코루틴의 실행 상태를 저장할 공간.
둘째, 코루틴 함수가 반환하기 전까지 스택 프레임을 넘나들면서도 로컬 변수들을 안전하게 저장하기 위한 공간.
5. HALO(Heap Allocation eLision Optimization)가 뭐야?
코루틴 프레임을 위한 공간이 스택에 할당될 수도 있고, 힙에 할당될 수도 있다.
C++을 포함한 대부분의 언어 표준에서는 코루틴 프레임을 힙 영역에 할당하라고 명시하지 않았으나,
대부분의 컴파일러들은 이 힙 할당에 default로 하고 있다.
당연하게도 현대 컴파일러들은 항상 힙에 할당하지만은 않는다.
자동 혹은 수동으로 특정 상황에서 최적화를 통해 코루틴 프레임을 스택 할당하기도 한다.
이를 HALO(Heap Allocation eLision Optimization)라고 한다.
최적화 과정 예시
#include <memory>
class MyClass {
int data;
public:
MyClass(int d) : data(d) {}
};
std::unique_ptr<MyClass> create_object() {
return std::make_unique<MyClass>(42);
}
int main() {
auto obj = create_object();
// 사용 코드...
return 0;
}
// HALO 적용 후 컴파일러가 내부적으로 변환하는 과정을 의사 코드로 표현:
int main() {
alignas(MyClass) char buffer[sizeof(MyClass)];
MyClass* obj = new(buffer) MyClass(42);
std::unique_ptr<MyClass> smart_ptr(obj);
// 사용 코드...
smart_ptr->~MyClass(); // 명시적 소멸자 호출
return 0;
}
- 함수 분석:
create_object() 함수에서 std::unique_ptr가 생성되자마자 반환하고 있다.
즉, 내부에서 객체를 생성하여 즉시 반환하는 것을 컴파일러가 분석해 냈다. - 최적화 결정:
함수가 인라인되지 않았고, 객체 생성이 함수 내에서 직접 이루어지기 때문에
컴파일러는 이 함수에 HALO를 적용할 수 있다고 판단. - 호출 지점 변환:
컴파일러는 main() 함수에서 create_object()를 호출하는 부분을 변환하여
대신 MyClass 객체를 직접 main() 함수의 스택에 생성하도록 코드를 수정한다. - 메모리 할당:
MyClass 객체를 위한 메모리를 스택에 할당한다.
이는 alignas(MyClass) char buffer[sizeof(MyClass)];와 같은 형태로 표현될 수 있습니다. - 객체 생성:
할당된 메모리에 MyClass 객체를 직접 생성.
이는 new(buffer) MyClass(42);와 같은 placement new를 사용하는 것과 유사하다. - 스마트 포인터 설정:
생성된 객체를 가리키는 std::unique_ptr를 생성하는데
std::unique_ptr는 스택에 생성된 객체를 가리키게 된다. - 소멸자 호출 보장:
컴파일러는 스코프 끝에서 객체의 소멸자가 호출되도록 보장한다.
이는 smart_ptr->~MyClass();와 같은 형태로 명시적 소멸자 호출을 삽입하는 것과 유사하다. - 메모리 관리 최적화:
힙 할당과 해제가 완전히 제거됩니다.
모든 메모리 관리가 스택에서 이루어집니다. - 예외 안전성 유지:
컴파일러는 예외 발생 시에도 객체가 적절히 소멸되도록 보장한다.
이는 기존의 std::unique_ptr 동작과 동일한 안전성을 제공한다. - 코드 생성:
최종적으로, 컴파일러는 이러한 최적화가 적용된 기계어 코드를 생성한다. - 정리 :
원래는 힙에 할당되었어야 할 MyClass객체를
스택에 할당해도 됨을 명확히 분석하고 스택할당을 하는 최적화 과정이다.
RVO와 혼돈하지 말아야 한다.
(참고 : RVO는 스택 기반 객체에 대한 오래된 최적화 기법이며,
C++ 17부터 필수 구현 사항으로 채택된 방식이다.)
이 과정을 통해 HALO는 다음과 같은 이점을 제공한다:
- 힙 할당/해제 오버헤드 제거
- 캐시 지역성 향상
- 메모리 단편화 감소
- 전체적인 성능 향상
주의할 점은 이 모든 과정이 컴파일러 내부에서 자동으로 이루어진다는 것이다.
덕분에 개발자는 원래의 코드를 그대로 작성하면 되며, 컴파일러가 적절한 상황에서 이 최적화를 적용한다.
이 HALO가 경우에 따라서는 성능 저하 이슈를 발생시키기도 하니 알아두자.
6. C++ 컴파일러가 코루틴을 구성하는 메커니즘
C++ 컴파일러가 코루틴 프레임을 구성하는 기본 메커니즘을 알아보자.
다음과 같은 코루틴 작업을 정의하였다.(실제로는 더 많은 중단점과 지역 변수가 존재하겠지만 설명에 지장 없음)
task<void> func()
{
int mylocal = 10;
co_return;
}
C++ 컴파일러는 co_return 키워드를 보고 다음과 비슷한 방식의 코드를 변환/생성한다.
// Struct representing the coroutine state
struct func_frame
{
task<void>::promise_type __promise;
int __step = 0;
decltype(__promise.initial_suspend()) __initial_suspend_awaiter;
decltype(__promise.final_suspend()) __final_suspend_awaiter;
// Structure to hold local and temporary variables
struct
{
// Local and temporary variables reside here
int mylocal;
} local_and_temps;
// Function to resume coroutine execution
void resume()
{
switch(__step)
{
case 0:
// co_await __promise.initial_suspend();
__initial_suspend_awaiter = __promise.initial_suspend();
if (!__initial_suspend_awaiter.await_ready())
{
__step = 1;
__initial_suspend_awaiter.await_suspend();
return;
}
case 1:
__initial_suspend_awaiter.await_resume();
// .. func body
mylocal = 10;
// co_return
__promise.return_void();
// co_await __promise.final_suspend();
__final_suspend_awaiter = __promise.final_suspend();
if (!__final_suspend_awaiter.await_ready())
{
__final_suspend_awaiter.await_suspend();
return;
}
delete this;
}
}
};
// Coroutine function transformed into a coroutine frame
task<void> func()
{
func_frame * frame = task<void>::promise_type::operator new(func_frame);
task<void> ret = frame->__promise.get_return_object()
frame->resume();
return ret;
}
- 코루틴 함수 func()를 실행하면 가장 먼저
코루틴 실행에 필요한 코루틴 프레임을 할당한다.(promise_type의 new연산자는 명시적/암시적 정의가 가능) - 다음으로 코루틴의 상태와 결과를 반환할 코루틴 객체를 get_return_object()라는 함수를 통해 생성한다.
이 객체는 코루틴 핸들을 가지고 코루틴 프레임 소통하는 역할을 한다.(코루틴 상태 추적) - 코루틴 함수 바디 본문의 실행 시작을 위해 코루틴 프레임의 resume()을 호출한다.
(참고로 만약 변수가 많고, 중단 지점이 여러개라면 local_and_temps에 더 많은 멤버가 존재할 것이다.)
이런식으로 컴파일러는 코루틴의 생명 주기(lifetime)와 프레임 레이아웃이 명확히 결정되면
HALO(coroutine Heap Allocation eLision Optimization) 기술의 적용 가능 여부를 확인한다.
최적화된 코루틴은 로컬 변수 내에 일반 함수 또는
다른 코루틴 일 수 있는 func_frame의 복사본을 포함시킬 수 있게 된다.
※ 코루틴 프레임 할당 최적화 관련 오해
간혹 최초의 중단 지점에서 온디맨드로 코루틴 프레임을 할당할 수 있다고 오해하는 경우가 있다.
이것이 불가능한 이유를 설명하겠다.
- 먼저, 모든 로컬 변수와 임시 변수는 이미 함수 시작 시 메모리에 존재해야 하고,
그 상태를 추적하고 유지하는 코루틴 프레임도 미리 할당되어 있어야 한다. - 중단 지점에서 코루틴 프레임을 동적으로 할당하려면,
함수 내에서 사용된 모든 로컬 변수와 임시 변수를 프레임에 옮겨서 관리해야 한다.
오버헤드도 매우 커질뿐만 아니라 최적화 가능성도 크게 줄어들게 된다. - ABI(Application Binary Interface) 경계 문제 :
다른 바이너리 모듈이나 외부 라이브러리의 함수를 호출하는 경우
즉, ABI 경계를 넘을 때는 서로 다른 컴파일러나 빌드 시스템이 생성한 코드 간의 상호작용이 발생한다.
C++에서 ABI 경계를 넘을 때 문제가 발생하는 이유는 컴파일러가 해당 호출의 내부 구현이나 메모리 관리 방식, 메모리 레이아웃 불일치, 호출 규약을 완전히 파악할 수 없기 때문에 컴파일러 최적화가 크게 제한되게 된다.
7. Stackful Coroutine vs Stackless Coroutine
대표적인 코루틴 타입 2가지에 대해 알아보자.
- Stackful Coroutine 방식 :
- 각 Coroutine마다 별도의 스택 메모리를 할당해야 하므로 메모리 사용량이 크다.
- Context Switcing 시 전체 스택을 저장/복원하므로 Context Switcing 비용이 크다.
- 대신 구현이 상대적으로 간단하다.
- 임의의 함수 호출 지점에서 yield가 가능하다. - Stackless Coroutine 방식 :
- Coroutine은 자체 스택을 가지지 않으므로 스택 메모리를 적게 차지한다.
대신 Coroutine의 상태를 저장하기 위한 최소한의 프레임 객체는 필요하다.
- Context Switcing 시 최소한의 상태만 저장/복원하기 때문에 Context Switcing 비용이 적다.
- 구현이 더 복잡하기 때문에 컴파일러의 지원이 필요하다.
- 일반적으로는 특정 지점에서만 yield가 가능하다.
C++에서 Stackless 방식 Coroutine을 채택한 이유(N4402 인용) :
재개 가능 함수(Resumable functions) 개정의 설계 목표는
C++ 언어와 표준 라이브러리를 확장하여 다음 특성을 가진 코루틴을 지원하는 것이다:
- 확장성이 뛰어남(수십억 개의 동시 코루틴까지).
- 함수 호출 오버헤드와 비슷한 비용으로 매우 효율적인 작업 재개 및 일시 중단.
- 오버헤드 없이 기존 facilities과의 원활한 상호 작용.
- 라이브러리 설계자가 코루틴 라이브러리를 개발할 수 있는 개방형 코루틴 머신으로
- 제너레이터, 고루틴, 태스크 등과 같은 다양한 고수준 시맨틱을 노출합니다.
- 예외가 금지되거나 사용할 수 없는 환경에서도 사용 가능
'확장성과 오버헤드 없이 기존 것들과의 원활한 상호 작용'이라는 설계 목표
(즉, 기존 라이브러리 및 OS API에 대한 기존 라이브러리 및 OS API를 제한 없이 호출하는 것)를 위해서는
'Stackless Coroutine'이 필요하다.
모든 코루틴에 대해 기본 스택을 예약하는 일반적인 'Stackful Coroutine'의 경우
32비트 주소 공간에서(Windows의 경우 기본 1MB, Linux의 경우 기본 2MB)는
사용 가능한 모든 가상 메모리를 단 몇 천 개의
코루틴으로 32비트 주소 공간에서 사용 가능한 가상 메모리를 소진하게 될 수 있다.
'Stackful Coroutine'은 가상 메모리를 소모하는 것 외에도 메모리 조각화로 이어진다.
왜냐하면 일반적인 스택 구현에서는 가상 메모리를 예약하는 것 외에도
플랫폼이 먼저 두 페이지의 스택(하나는 스택으로 사용되는 읽기/쓰기 액세스,
다른 하나는 가드 페이지 역할을 하는 자동 스택 증가를 구현하기 위해)을 커밋하는데,
코루틴의 상태를 저장하기 위해 필요한 메모리가 몇 바이트 되지도 않는 상황에서도 그러하다.
'Split-stack'을 사용하는 것과 같은 완화 접근 방식을 사용하려면
전체 프로그램(호출하는 모든 라이브러리 및 호출하는 모든 OS 기능 포함)을 분할 스택으로 컴파일하거나,
컴파일되지 않은 코드를 호출할 때 런타임 페널티가 발생한다.
이러한 작은 고정 크기 스택을 사용하는 것과 같은 완화 접근 방식은
이러한 스택에서 호출할 수 있는 것을 제한한다.
코루틴에서 호출할 수 있는 함수가 작은 고정 스택에 할당된 것보다 더 많은 메모리를
소비하지 않도록 보장해야 한다.
참고 : Goroutine(GO언어의 코루틴)은 예전에 (Segmented Stack)을 사용하여
stackful coroutine 방식의 스택 관리를 최적화했었다.
그러나 이방식은 문제가 많아 지금은 Copying Stack방식으로 개선하였다.
더 자세히 알고 싶다면 참고 문헌 참조.
- 제안서
- N4736(C++ Extension for coroutines), N4723
- N4402(Resumable Functions), N4134, N3977, N3858
- ISO/IEC 14882:2020 - Programming Language C++
- C++20 표준을 정의하는 주요 문서
- 섹션 9 "Coroutines"에서 coroutine 관련 내용
- N4860 - Working Draft, Standard for Programming Language C++
- C++20 표준의 최종 작업 초안
- https://isocpp.org/files/papers/N4860.pdf
- P0912R5 - Coroutines
- C++20에 coroutine을 도입하는 제안서
- https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0912r5.html
- N4775 - Working Draft, C++ Extensions for Coroutines
- Coroutines TS (Technical Specification)의 작업 초안
- https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/n4775.pdf
- P1056R1 - C++ Coroutines: A Negative Overhead Abstraction
- Coroutine의 성능 특성에 대한 자세한 설명을 제공
- https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1056r1.html
- CppCoreGuidelines
- C++ 코어 가이드라인에서 coroutine 관련 모범 사례
- https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines
'C++' 카테고리의 다른 글
[C++ 20] Coroutines #3 - 요약 정리 (0) | 2024.09.22 |
---|---|
[C++ 20] Coroutines #2 - 표준 요약 (0) | 2024.09.21 |
[C++] 코루틴(Coroutine) 분석(작성 중) (0) | 2024.09.15 |
[C++] 자주 발생하는 표준 컨데이너 관련 Error (0) | 2023.11.19 |
Uniform Initialization와 std::initializer_list (0) | 2023.08.29 |