컨텍스트 스위칭이 함수 호출 수준으로 거의 무료라고 하지만, 실제로는 분기 예측기(branch predictor)가 깨지는 등의 미묘한 비용이 있음
Zig의 async 설계가 하드웨어 call/return 쌍을 사용하는지, 아니면 간접 점프 기반으로 번역되는지 확실하지 않음
완벽한 벤치마크를 하려면 두 작업 간 지속적인 스위칭이 있는 프로그램과 완전히 동기적인 프로그램의 총 실행 시간을 비교해야 함. 이건 꽤 까다로운 일임
스택리스 코루틴에서 호출 스택의 맨 아래에서 두 작업을 계속 전환하고, 스택 전환 코드가 인라인되어 있다면 call/ret 불일치 패널티를 대부분 피할 수 있음
컴파일러를 제어할 수 있다면, I/O 코드의 call/ret을 명시적 점프로 바꾸는 것도 가능함
장기적으로는 CPU가 스택풀 코루틴을 더 잘 예측하도록 메타 예측기(meta-predictor) 를 도입하길 바람
Zig에는 현재 async가 언어 차원에서 사라졌고, OP가 직접 유저 공간에서 태스크 스위칭을 구현한 것임
단순한 코루틴 간 ping-pong 테스트를 했을 때, 다른 솔루션과 비교해 믿기 어려운 수치를 얻은 적이 있음
Zig에 곧 새로운 async가 추가될 예정이라, 본격적으로 파기 전에 기다리는 중임
관련 글: Zig new async I/O
Stackful 코루틴은 RAM이 충분할 때 의미가 있음
나는 Zig를 임베디드(ARM Cortex-M4, 256KB RAM) 환경에서 사용 중이며, C와의 인터롭에서 메모리 안전성을 확보하기 위해 씀
Rust처럼 색이 있는 async를 더 선호함. 동기 코드처럼 보이는 마법 같은 느낌이 좋지만, 큰 코드베이스에서는 어떤 함수가 blocking인지 구분하기 어려워지는 게 문제임
사실 모든 동기 코드는 소프트웨어가 만든 환상(illusion) 임
CPU는 I/O에서 실제로 블록되지 않으며, OS 스레드 자체가 OS가 구현한 스택풀 코루틴임
언어 차원에서 이 환상을 더 효율적으로 구현할 수 있을 뿐, 본질은 동일함
새로운 Zig IO는 Rust보다 더 세련된 방식으로 색을 입힌(colored) 구조가 될 예정임
함수가 I/O를 수행하는지 여부에 따라 색이 결정되고, 호출 시점에서 async 여부를 명시함
Zig는 함수 호출 시 필요한 스택 크기를 계산하는 기능도 목표로 하고 있어, 스택풀 코루틴의 RAM 낭비 문제를 줄일 수 있을 것으로 기대함
그래서 Zig가 I/O를 명시적으로 표현하려는 이유가 바로, 어떤 함수가 blocking인지 추적할 수 있게 하려는 것임
Zig를 지금 도입하기엔 시기상조라는 의견이 있음. I/O 모델이 크게 바뀌는 중이라 몇 년은 걸릴 거라는 인상임
나도 2020년에 비슷한 이유로 Zig를 떠났음.
하지만 프로젝트는 여전히 활발하고, 빠른 출시보다 올바른 설계를 우선시하는 점은 긍정적으로 봄
지금은 Go나 C를 쓰며 1.0을 기다리는 중임
몇 년은 금방 지나감. Zig는 이미 충분히 쓸 만한 언어임. 쓸 사람은 쓰고, 아닐 사람은 안 씀
실제로는 나쁜 시기가 맞음. 0.16에서 큰 I/O 변경이 예정되어 있고, 저자조차 최신 기능을 아직 사용하지 않음
나도 I/O 중심 작업을 위해 0.16을 기다릴 예정임
하지만 I/O 관련 작업이라면 Zig 0.15의 버퍼드 리더/라이터 인터페이스를 사용하면 큰 변화는 없을 것임
오히려 지금이 틀렸다고 봄. Zig는 언어 자체가 급변하는 게 아니라, 새로운 강력한 std.Io API를 추가하는 중임
기존 코드는 그대로 동작하고, 새 API는 더 인체공학적이고 성능이 좋음
나도 기존 프로젝트를 새 Reader/Writer API로 옮기며 코드가 훨씬 깔끔해졌음
콜백 기반 async가 표준이 된 이유가 여전히 의문임
libtask 같은 접근이 훨씬 깔끔해 보임
Rust도 콜백 기반 async를 채택했는데, 이유를 잘 모르겠음
참고: libtask
스택리스 코루틴은 언어 내부에서 구현 가능하고, 기존 기능과의 예측 가능한 상호작용이 장점임
하지만 스택을 직접 다루면 예외 처리, GC, 디버거 등과 충돌할 수 있음
LLVM 수준에서 이런 변경을 병합하기도 어렵기 때문에, 언어 설계자 입장에서는 현실적인 제약이 많음
Microsoft가 C++ 표준용으로 연구한 결과, 스택리스 코루틴이 메모리 오버헤드가 훨씬 적고, 실행기 설계의 자유도가 높다는 결론이 나왔음
zio나 libtask 방식의 단점은 스택 크기를 직접 추정해야 한다는 점임
너무 작으면 오버플로, 너무 크면 메모리 낭비임
플랫폼마다 필요한 스택 크기가 달라서 이식성 문제도 있음
Zig 이슈 #157이 해결되면 이 접근이 더 나아질 것 같음
libtask 같은 경우 스레드 스택 크기가 모호하고, 일반적인 async 상태보다 훨씬 큼
Rust의 async는 콜백이 아니라 폴링(polling) 기반임
즉, async를 구현하는 세 가지 방식이 있음
콜백 기반 (Node.js, Swift)
스택풀 기반 (Go, libtask)
폴링 기반 (Rust)
Rust는 정적 상태 머신으로 변환되어 런타임이 폴링함
스택풀은 메모리 낭비가 크고, 스택 크기 관리가 어려움
Rust는 이를 피하기 위해 스택리스 구조를 채택했고, Zig는 두 방식을 모두 선택할 수 있게 할 예정임
참고: zio coroutine 코드
TCP 읽기가 한 달 동안 블록될 수도 있는데, I/O 타임아웃 인터페이스는 어떻게 되는지 궁금함
TCP 소켓에서 setsockopt로 읽기/쓰기 타임아웃을 설정할 수 있음
Zig는 POSIX API 레이어를 제공함
참고: setsockopt 문서
현재 Zig의 std.Io.Reader는 타임아웃을 인식하지 못함
Python의 asyncio.timeout처럼 동작하는 구조를 구상 중임
예시 코드:
var timeout: zio.Timeout = .init;
defer timeout.cancel(rt);
timeout.set(rt, 10);
const n = try reader.interface.readVec(&data);
Hacker News 의견
Zig의 async 설계가 하드웨어 call/return 쌍을 사용하는지, 아니면 간접 점프 기반으로 번역되는지 확실하지 않음
완벽한 벤치마크를 하려면 두 작업 간 지속적인 스위칭이 있는 프로그램과 완전히 동기적인 프로그램의 총 실행 시간을 비교해야 함. 이건 꽤 까다로운 일임
컴파일러를 제어할 수 있다면, I/O 코드의 call/ret을 명시적 점프로 바꾸는 것도 가능함
장기적으로는 CPU가 스택풀 코루틴을 더 잘 예측하도록 메타 예측기(meta-predictor) 를 도입하길 바람
관련 글: Zig new async I/O
나는 Zig를 임베디드(ARM Cortex-M4, 256KB RAM) 환경에서 사용 중이며, C와의 인터롭에서 메모리 안전성을 확보하기 위해 씀
Rust처럼 색이 있는 async를 더 선호함. 동기 코드처럼 보이는 마법 같은 느낌이 좋지만, 큰 코드베이스에서는 어떤 함수가 blocking인지 구분하기 어려워지는 게 문제임
CPU는 I/O에서 실제로 블록되지 않으며, OS 스레드 자체가 OS가 구현한 스택풀 코루틴임
언어 차원에서 이 환상을 더 효율적으로 구현할 수 있을 뿐, 본질은 동일함
함수가 I/O를 수행하는지 여부에 따라 색이 결정되고, 호출 시점에서 async 여부를 명시함
Zig는 함수 호출 시 필요한 스택 크기를 계산하는 기능도 목표로 하고 있어, 스택풀 코루틴의 RAM 낭비 문제를 줄일 수 있을 것으로 기대함
하지만 프로젝트는 여전히 활발하고, 빠른 출시보다 올바른 설계를 우선시하는 점은 긍정적으로 봄
지금은 Go나 C를 쓰며 1.0을 기다리는 중임
나도 I/O 중심 작업을 위해 0.16을 기다릴 예정임
기존 코드는 그대로 동작하고, 새 API는 더 인체공학적이고 성능이 좋음
나도 기존 프로젝트를 새 Reader/Writer API로 옮기며 코드가 훨씬 깔끔해졌음
libtask 같은 접근이 훨씬 깔끔해 보임
Rust도 콜백 기반 async를 채택했는데, 이유를 잘 모르겠음
참고: libtask
하지만 스택을 직접 다루면 예외 처리, GC, 디버거 등과 충돌할 수 있음
LLVM 수준에서 이런 변경을 병합하기도 어렵기 때문에, 언어 설계자 입장에서는 현실적인 제약이 많음
너무 작으면 오버플로, 너무 크면 메모리 낭비임
플랫폼마다 필요한 스택 크기가 달라서 이식성 문제도 있음
Zig 이슈 #157이 해결되면 이 접근이 더 나아질 것 같음
즉, async를 구현하는 세 가지 방식이 있음
Rust는 정적 상태 머신으로 변환되어 런타임이 폴링함
스택풀은 메모리 낭비가 크고, 스택 크기 관리가 어려움
Rust는 이를 피하기 위해 스택리스 구조를 채택했고, Zig는 두 방식을 모두 선택할 수 있게 할 예정임
참고: zio coroutine 코드
setsockopt로 읽기/쓰기 타임아웃을 설정할 수 있음Zig는 POSIX API 레이어를 제공함
참고: setsockopt 문서
Python의
asyncio.timeout처럼 동작하는 구조를 구상 중임예시 코드:
사실 그게 가장 어려운 부분임
참고: zio.dev
하지만 Zig는 저수준 언어임에도 불구하고 고수준 API를 깔끔하게 표현할 수 있어서 인상적이었음
Zig와 Go 모두에 Qt 바인딩이 새로 생겼음
나는 Rust용 바인딩을 원함. cxx-qt가 유일하게 유지되는 프로젝트지만, QML이나 CMake는 쓰고 싶지 않음. Rust + Cargo만으로 Qt를 쓰고 싶음