나는 이 점을 다시 지적하고 싶음. 기사에서 Zig가 function coloring 문제를 완전히 해결했다고까지 언급하지만, 나는 동의하지 않음. 유명한 "What color is your function?" 글의 5가지 규칙을 다시 생각해보면, Zig에서는 async/sync/red/blue처럼 색상이 구분되지 않는다 해도 결국 IO 함수와 비IO 함수 두 가지 케이스만 존재함. 함수 호출 방식도 색상에 따라 달라지는 문제를 기술적으로 해결했다지만, 여전히 IO가 필요한 함수에는 IO를 인자로 넘겨줘야 하고, 필요 없는 함수는 받지 않음. 결국 본질은 변하지 않은 느낌임. IO 함수는 IO 함수에서만 호출 가능하고, 이 또한 coloring 문제에서 벗어나지 못함. 물론 새로운 executor를 전달할 수도 있지만, 그게 진짜 바라는 것인지는 의문임. Rust에서도 비슷하게 할 수 있음. 색깔이 있는 함수 콜이 번거롭다는 점도 마찬가지임. 몇몇 핵심 라이브러리 함수가 colored라는 부분은 Zig/Rust 모두 해당되지 않음. Coloring 문제의 본질은 컨텍스트(즉, async executor나 auth, allocator 등)를 필요로 하는 함수가, 호출할 때 반드시 그 컨텍스트를 제공해야 한다는 데 있음. Zig가 진짜 이 부분을 해결했다고 보긴 어려움. 다만, Zig의 추상화는 굉장히 잘 돼 있고 Rust는 이 부분이 모자란 면이 있음. 하지만 function coloring 문제 자체는 여전히 남아있음
전형적인 async function coloring과의 핵심 차이는 Zig의 'Io'가 단순히 비동기 처리를 위한 특수한 값이 아니라, 파일 읽기, 슬립, 시간 받아오기 등 모든 IO를 위해 필연적으로 필요한 값임. 'Io'는 함수의 속성이 아니라 어디든 둘 수 있는 일반 값임. 실제로는, 이런 특징 덕분에 coloring 문제는 해결된 것처럼 보임. 대부분의 코드베이스에서 IO가 이미 스코프 어딘가에 있어서, 정말 순수 계산 함수만 IO가 필요 없게 됨. 만약 어떤 함수가 갑자기 IO가 필요해진다면, 대부분의 경우 'my_thing.io'에서 바로 가져와 쓸 수 있음. Rust처럼 모든 함수에 Allocator를 넘길 필요가 없어서 번거로움이 없음. 즉, 코드 경로가 바뀌어서 IO를 해야 한다면 굳이 함수마다 변경을 퍼트릴 필요 없이 바로 사용 가능함. 원론적으로는 function coloring이 남아 있다는 데 동의하지만, 사실상 모든 함수가 async-colored가 된 셈이기에 실질적인 문제는 거의 없음. 실제로 Zig 개발자들은 Allocator를 명시적으로 넘기는 것이 function coloring 번거로움을 유발하지 않는다고 여김. 'Io'도 마찬가지로 문제가 크지 않을 거라 생각함
중요한 핵심을 언급하지 않은 것 같음. Rust 라이브러리를 쓸 때는 반드시 async/await, tokio, send+sync 같은 조건을 맞춰야 하고, sync API면 async 앱에서는 무용지물이 되는 게 현실임. 반면, Zig의 IO 전달 방식은 이 문제를 근본적으로 해결함. 덕분에 고생하며 procedural macro나 멀티버전을 억지로 구현하지 않아도 되며, 사실상 이런 방식 자체가 라이브러리 멀티버전 문제를 결국 잘 해결하지도 못함. Rust에서 async/sync 혼용 문제에 관한 다양한 논의가 있는데, 다음 링크에도 설명이 있음 https://nullderef.com/blog/rust-async-sync/. 앞으로 Zig가 cooperative scheduling, 고성능 async, 스레드-퍼-코어 async 같은 부분까지 잘 풀 수 있길 바람
나는 범주론 전문가는 아니지만, 결국 이런 컨텍스트 관리의 길을 걷다 보면 IO 모나드에 도달하게 됨. 이 맥락(Context)은 암시적으로 있을 수도 있지만, 컴파일러의 도움을 제대로 받으려면 시스템 내에서 실체로 드러내야 함. 그리고 시스템 프로그래밍 언어들의 야망이 다 Async나 코루틴 무덤에 묻혀왔지만, Andrew가 IO 모나드를 나름 다시 발견해서 제대로 구현한 점은 세대의 희망임. 실제 세계 함수에는 색깔이 존재함. 명확한 이동 규칙을 두거나, 아니면 C++의 co_await, tokio처럼 점점 복잡해지는 길로 빠질 수밖에 없음. 이게 바로 ‘The Way’라고 생각함
모든 함수를 빨갛게(혹은 파랗게) 만드는 간단한 트릭이 있음
var io: std.Io = undefined;
pub fn main() !void {
var impl = ...;
io = impl.io();
}
io를 글로벌 변수로 두고 쓰면 coloring 걱정할 필요 없어짐. 농담이지만, 확실히 'Io' 인터페이스를 써야 한다는 점에서 마찰이 조금은 있지만, 이건 async/await를 쓸 때 발생하는 실질적인 friction과는 본질적으로 다른 문제임. 내가 보기에 function coloring 문제의 핵심은, 코드 재사용이 불가능해지는 async 키워드의 정적인 색깔 부여 때문임. Zig에서는 어떤 함수를 async로 만들거나 아니거나 모두 IO를 인자로 받기 때문에 그 관점에서는 coloring 자체가 무의미함. 두 번째로, async/await를 쓰면 스택 없는 코루틴(즉, 컴파일러에서 컨트롤되는 스택 전환)을 강제로 쓰게 되지만, Zig의 새로운 IO 시스템은 내부적으로 async를 써도 Blocking IO로 동작하게 할 수 있음. 이런 점이 실질적인 function coloring 문제라고 생각함
Go도 “미묘한 coloring” 문제를 겪음. goroutine을 사용할 때는 항상 context 인자를 넘겨주면서 취소를 처리해야 하고, 많은 라이브러리 함수들도 context를 요구하기 때문에 전체 코드가 오염됨. 기술적으로는 context를 안 써도 되지만, context.Background로 무작위로 넘기는 건 권장되지 않는 방법임
함수가 IO 메서드를 직접 호출하면 외부에서 IO를 분리할 수 없는 구조라 sans-io라고 부르기 어렵다는 생각임. 링크에 나온 대로, 바이트 스트림 기반 프로토콜에서는 구현부가 입력/출력 버퍼만 다루고, 네트워크에서 데이터를 받는 부분은 반드시 호출 쪽이 직접 전달해야 진정한 sans-io임. 출력 역시 버퍼에만 쓰거나, 이벤트가 발생할 때 바이트 스트림을 즉시 반환하는 방식이 있음. 반환 방식은 구현 선택이지만, 내부 버퍼는 자동 응답이 필요한 상황에 유용함. 핵심은 IO를 직접 하지 않는 구조임
나는 function coloring의 문제점이, 스택에서 처리하건 스택을 unwind하건 결국 둘 중 하나가 남는다는 데 있다고 생각함. Zig가 coloring 문제 해결을 주장하지만, IO 구현 방식으로 여전히 blocking/thread pool/green thread를 사용할 수 있게 해줌. 근데 이런 blocking IO는 애초에 문제가 아니었음. 글로벌 상태를 안 쓰는 관례를 지키면 거의 모든 언어에서 이 정도는 가능함. stackless coroutine은 아직 미구현인데, ‘나머지 부품만 그리면 완성’ 같은 느낌임. 만약 진짜 보편적 함수 호출을 원한다면, 두 가지 방법이 있다고 생각함
모든 함수를 async로 만들되, 인자 하나로 동기로 실행할지 여부를 넣어 처리하게 함(성능 저하 있음)
각 함수를 두 번 컴파일해서 상황에 맞게 골라 호출하게 함(코드 크기 증가와 함수 포인터 처리의 어려움 있음)
핵심팀은 아니지만, 사용자와 실사용자들이 semiblocking 구현을 충분히 써보고 API를 안정화한 뒤, 바로 그 해법(스택 점프 기반의 진짜 코루틴 삽입)을 적용할 계획이라고 들음. 현재 LLVM의 코루틴 상태머신 컴파일러는 libc나 malloc에 의존하는 문제가 있음. Zig의 새 io 인터페이스가 userland async/await를 지원하기 때문에, 향후에 제대로 된 frame jumping 솔루션이 들어와도 이식이 쉽고 디버깅도 편리함. 코루틴이 어려우면 io API도 소폭 수정으로 버틸 수 있게 해두고, stackless coroutine부터 너무 서두르진 않을 생각임
C#/.NET의 ValueTask<T>도 비슷한 역할을 함. 동기로 끝나면 오버헤드가 없고, 필요할 때만 Task<T>로 사용할 수 있음. 코드는 보통 await만 해두면 되고, 실행 시점에 런타임이나 컴파일러가 알아서 동기/비동기 선택함
Zig를 좋아하지만 green thread(파일버, stackful coroutine)에 집중하는 걸 보니 아쉬운 마음임. Rust도 1.0 이전에 비슷한 Runtime trait를 퍼포먼스 문제로 폐기함. 실제로 OS와 언어, 라이브러리들이 이런 접근의 폐해를 여러 번 배웠고, 관련 자료도 있음 https://www.open-std.org/JTC1/SC22/WG21/docs/papers/2018/p1364r0.pdf. 파일버가 90년대에는 확장성 있는 동시성 처리로 각광받았지만, 현대에는 stackless coroutine, OS/하드웨어 발전 등으로 권장되지 않음. 만약 계속 이대로 간다면 Zig는 Go랑 비슷한 성능에서 한계에 부딪히고, 진정한 퍼포먼스 경쟁자로는 어렵게 됨. std.fs는 퍼포먼스가 필요한 케이스에서 남아있길 바람
우리가 green thread(파일버)에 ‘올인’한다는 인상은 오해임. OP 참고 기사에서 stackless coroutine 기반 구현을 기대한다는 점을 명시적으로 언급했고, 관련 제안도 있음 https://github.com/ziglang/zig/issues/23446. 퍼포먼스는 중요하고, 파일버가 성능적으로 기대 이하라면 보편적으로 쓰이지 않을 것임. 이 기사에서 논의된 내용은 stackless coroutine이 기본 ‘Io’ 구현이 되는 걸 막지 않음
green thread가 퍼포먼스가 나쁘다는 주장에 대해 의문임. 상위 동시성 서버 플랫폼(Go, Erlang, Java)이 모두 green thread를 쓰거나 쓰려고 함. green thread는 C FFI와의 호환 문제로 더 저수준 언어(Rust 등)에서는 적합하지 않을 수 있지만, 퍼포먼스 자체가 항상 문제라고만 보긴 힘듦
여러 선택지 중 하나이기 때문에 ‘all-in’이라고 볼 수 없다고 생각함. 어떤 구현을 택할지는 실행파일에서 결정하고, 라이브러리 코드에서는 결정되지 않음
Rust가 green thread를 제거하고 async runtime으로 교체한 선택과 비슷한 효과를 Zig도 노리고 있음. 핵심은 ‘async=IO, IO=async’인 점을 공식화한 직관임. Rust는 tokio 등 pluggable async runtime, Zig는 pluggable IO runtime을 제공하는 쪽임. 결국 언어에서 런타임을 빼내고, 사용자 영역에서 끼울 수 있게 하면서 모두가 공통된 인터페이스를 공유하는 게 방향임
Zig 같은 시스템 언어에서 흔한 표준 IO 연산에까지 런타임 다형성을 강제하는 건 다소 어색하다고 느낌. 대부분의 실전에서는 IO 구현이 정적으로 확정될 수 있는데 왜 런타임 오버헤드를 강요해야 하는지 의문임
IO에서는 동적 디스패치 오버헤드가 실제로는 거의 미미할 거라 생각함. IO 대상에 따라 다르긴 하겠지만 결과적으로 IO가 CPU 병목이 아닌 경우가 훨씬 많음. 그래서 IO 바운드란 이름도 붙음
“왜 모두에게 런타임 오버헤드를 강제하나?”라는 질문에, 대부분 한 종류의 io만 쓰는 시스템에서는 컴파일러가 double indirection(간접 참조) 비용 자체를 최적화해서 없앨 의도로 보임. 그리고 IO는 어차피 bottleneck이 따로 있어서, 인디렉션 한 번 늘어나는 건 부담이 거의 없음
Zig의 철학상 바이너리 크기에 더 신경을 쓰는 편임. Allocator도 똑같은 트레이드오프가 있는데, 예를 들어 ArrayListUnmanaged는 allocator에 대해 generic하지 않으므로 매 할당마다 dynamic dispatch가 발생함. 실제로는 파일 할당이나 쓰기 비용이 간접 호출 오버헤드를 훨씬 압도함. 이런 바이너리 사이즈에 집착하는 게 Zig 스타일임. 참고로 devirtualization(동적 호출을 정적으로 바꾸는 최적화)은 미신임
런타임 다형성 자체가 본질적으로 나쁜 것은 아님. tight loop에서 브랜치가 생긴다든지, 컴파일러가 인라인 최적화를 못한다든지 그런 상황이 아니면 문제 상황이 아님
새 io 파라미터가 여기저기 드러나는 게 썩 마음에 들진 않지만, 여러 구현(thread 기반, fiber 기반 등)을 쉽게 쓸 수 있고 사용자에게 구현체를 강요하지 않는 점(Allocator 인터페이스처럼)이 아주 마음에 듦. 전체적으로 상당한 개선이고, 여러 stdlib 구현체 중 별도의 오버헤드 없는 동기/블로킹 io 구현이 제공된다면 “쓰지 않는 것에 돈을 내지 않는다”는 Zig 철학을 그대로 따르는 셈임
“쓰지 않는 것에 돈을 내지 않는다”가 정말 가능한가? 팀 규모가 아주 작고 엄청난 규율이 있지 않은 이상 결국 다른 누군가가 쓰게 되고, 나도 비용을 치르게 될 거임. 그리고 io를 계속 넘기는 게 필요한 곳에서 그냥 호출만 하는 것보다 더 귀찮은 것 같음
Zig에서는 io.async가 비동기성(작업의 순서가 보장되지 않을 수 있지만 결과는 올바름)을 표현할 뿐, 동시성(concurrency)을 나타내는 게 아님. 즉, async와 io 호출의 의미를 분리했다는 점이 핵심임. 이 설계가 아주 영리하다고 생각함
IO 인터페이스 덕분에 언어 차원의 vfs(Virtual File System)을 만들 수 있다는 점이 마음에 듦
Hacker News 의견
나는 이 점을 다시 지적하고 싶음. 기사에서 Zig가 function coloring 문제를 완전히 해결했다고까지 언급하지만, 나는 동의하지 않음. 유명한 "What color is your function?" 글의 5가지 규칙을 다시 생각해보면, Zig에서는 async/sync/red/blue처럼 색상이 구분되지 않는다 해도 결국 IO 함수와 비IO 함수 두 가지 케이스만 존재함. 함수 호출 방식도 색상에 따라 달라지는 문제를 기술적으로 해결했다지만, 여전히 IO가 필요한 함수에는 IO를 인자로 넘겨줘야 하고, 필요 없는 함수는 받지 않음. 결국 본질은 변하지 않은 느낌임. IO 함수는 IO 함수에서만 호출 가능하고, 이 또한 coloring 문제에서 벗어나지 못함. 물론 새로운 executor를 전달할 수도 있지만, 그게 진짜 바라는 것인지는 의문임. Rust에서도 비슷하게 할 수 있음. 색깔이 있는 함수 콜이 번거롭다는 점도 마찬가지임. 몇몇 핵심 라이브러리 함수가 colored라는 부분은 Zig/Rust 모두 해당되지 않음. Coloring 문제의 본질은 컨텍스트(즉, async executor나 auth, allocator 등)를 필요로 하는 함수가, 호출할 때 반드시 그 컨텍스트를 제공해야 한다는 데 있음. Zig가 진짜 이 부분을 해결했다고 보긴 어려움. 다만, Zig의 추상화는 굉장히 잘 돼 있고 Rust는 이 부분이 모자란 면이 있음. 하지만 function coloring 문제 자체는 여전히 남아있음
전형적인 async function coloring과의 핵심 차이는 Zig의 'Io'가 단순히 비동기 처리를 위한 특수한 값이 아니라, 파일 읽기, 슬립, 시간 받아오기 등 모든 IO를 위해 필연적으로 필요한 값임. 'Io'는 함수의 속성이 아니라 어디든 둘 수 있는 일반 값임. 실제로는, 이런 특징 덕분에 coloring 문제는 해결된 것처럼 보임. 대부분의 코드베이스에서 IO가 이미 스코프 어딘가에 있어서, 정말 순수 계산 함수만 IO가 필요 없게 됨. 만약 어떤 함수가 갑자기 IO가 필요해진다면, 대부분의 경우 'my_thing.io'에서 바로 가져와 쓸 수 있음. Rust처럼 모든 함수에 Allocator를 넘길 필요가 없어서 번거로움이 없음. 즉, 코드 경로가 바뀌어서 IO를 해야 한다면 굳이 함수마다 변경을 퍼트릴 필요 없이 바로 사용 가능함. 원론적으로는 function coloring이 남아 있다는 데 동의하지만, 사실상 모든 함수가 async-colored가 된 셈이기에 실질적인 문제는 거의 없음. 실제로 Zig 개발자들은 Allocator를 명시적으로 넘기는 것이 function coloring 번거로움을 유발하지 않는다고 여김. 'Io'도 마찬가지로 문제가 크지 않을 거라 생각함
중요한 핵심을 언급하지 않은 것 같음. Rust 라이브러리를 쓸 때는 반드시 async/await, tokio, send+sync 같은 조건을 맞춰야 하고, sync API면 async 앱에서는 무용지물이 되는 게 현실임. 반면, Zig의 IO 전달 방식은 이 문제를 근본적으로 해결함. 덕분에 고생하며 procedural macro나 멀티버전을 억지로 구현하지 않아도 되며, 사실상 이런 방식 자체가 라이브러리 멀티버전 문제를 결국 잘 해결하지도 못함. Rust에서 async/sync 혼용 문제에 관한 다양한 논의가 있는데, 다음 링크에도 설명이 있음 https://nullderef.com/blog/rust-async-sync/. 앞으로 Zig가 cooperative scheduling, 고성능 async, 스레드-퍼-코어 async 같은 부분까지 잘 풀 수 있길 바람
나는 범주론 전문가는 아니지만, 결국 이런 컨텍스트 관리의 길을 걷다 보면 IO 모나드에 도달하게 됨. 이 맥락(Context)은 암시적으로 있을 수도 있지만, 컴파일러의 도움을 제대로 받으려면 시스템 내에서 실체로 드러내야 함. 그리고 시스템 프로그래밍 언어들의 야망이 다 Async나 코루틴 무덤에 묻혀왔지만, Andrew가 IO 모나드를 나름 다시 발견해서 제대로 구현한 점은 세대의 희망임. 실제 세계 함수에는 색깔이 존재함. 명확한 이동 규칙을 두거나, 아니면 C++의 co_await, tokio처럼 점점 복잡해지는 길로 빠질 수밖에 없음. 이게 바로 ‘The Way’라고 생각함
모든 함수를 빨갛게(혹은 파랗게) 만드는 간단한 트릭이 있음
io를 글로벌 변수로 두고 쓰면 coloring 걱정할 필요 없어짐. 농담이지만, 확실히 'Io' 인터페이스를 써야 한다는 점에서 마찰이 조금은 있지만, 이건 async/await를 쓸 때 발생하는 실질적인 friction과는 본질적으로 다른 문제임. 내가 보기에 function coloring 문제의 핵심은, 코드 재사용이 불가능해지는 async 키워드의 정적인 색깔 부여 때문임. Zig에서는 어떤 함수를 async로 만들거나 아니거나 모두 IO를 인자로 받기 때문에 그 관점에서는 coloring 자체가 무의미함. 두 번째로, async/await를 쓰면 스택 없는 코루틴(즉, 컴파일러에서 컨트롤되는 스택 전환)을 강제로 쓰게 되지만, Zig의 새로운 IO 시스템은 내부적으로 async를 써도 Blocking IO로 동작하게 할 수 있음. 이런 점이 실질적인 function coloring 문제라고 생각함
Go도 “미묘한 coloring” 문제를 겪음. goroutine을 사용할 때는 항상 context 인자를 넘겨주면서 취소를 처리해야 하고, 많은 라이브러리 함수들도 context를 요구하기 때문에 전체 코드가 오염됨. 기술적으로는 context를 안 써도 되지만, context.Background로 무작위로 넘기는 건 권장되지 않는 방법임
sans-io라는 개념은 Rust 등에서 이미 논의된 바 있는데, 참고 링크는 https://www.firezone.dev/blog/sans-io, https://sans-io.readthedocs.io/, https://news.ycombinator.com/item?id=40872020임
나는 function coloring의 문제점이, 스택에서 처리하건 스택을 unwind하건 결국 둘 중 하나가 남는다는 데 있다고 생각함. Zig가 coloring 문제 해결을 주장하지만, IO 구현 방식으로 여전히 blocking/thread pool/green thread를 사용할 수 있게 해줌. 근데 이런 blocking IO는 애초에 문제가 아니었음. 글로벌 상태를 안 쓰는 관례를 지키면 거의 모든 언어에서 이 정도는 가능함. stackless coroutine은 아직 미구현인데, ‘나머지 부품만 그리면 완성’ 같은 느낌임. 만약 진짜 보편적 함수 호출을 원한다면, 두 가지 방법이 있다고 생각함
모든 함수를 async로 만들되, 인자 하나로 동기로 실행할지 여부를 넣어 처리하게 함(성능 저하 있음)
각 함수를 두 번 컴파일해서 상황에 맞게 골라 호출하게 함(코드 크기 증가와 함수 포인터 처리의 어려움 있음)
핵심팀은 아니지만, 사용자와 실사용자들이 semiblocking 구현을 충분히 써보고 API를 안정화한 뒤, 바로 그 해법(스택 점프 기반의 진짜 코루틴 삽입)을 적용할 계획이라고 들음. 현재 LLVM의 코루틴 상태머신 컴파일러는 libc나 malloc에 의존하는 문제가 있음. Zig의 새 io 인터페이스가 userland async/await를 지원하기 때문에, 향후에 제대로 된 frame jumping 솔루션이 들어와도 이식이 쉽고 디버깅도 편리함. 코루틴이 어려우면 io API도 소폭 수정으로 버틸 수 있게 해두고, stackless coroutine부터 너무 서두르진 않을 생각임
C#/.NET의 ValueTask<T>도 비슷한 역할을 함. 동기로 끝나면 오버헤드가 없고, 필요할 때만 Task<T>로 사용할 수 있음. 코드는 보통 await만 해두면 되고, 실행 시점에 런타임이나 컴파일러가 알아서 동기/비동기 선택함
Zig를 좋아하지만 green thread(파일버, stackful coroutine)에 집중하는 걸 보니 아쉬운 마음임. Rust도 1.0 이전에 비슷한 Runtime trait를 퍼포먼스 문제로 폐기함. 실제로 OS와 언어, 라이브러리들이 이런 접근의 폐해를 여러 번 배웠고, 관련 자료도 있음 https://www.open-std.org/JTC1/SC22/WG21/docs/papers/2018/p1364r0.pdf. 파일버가 90년대에는 확장성 있는 동시성 처리로 각광받았지만, 현대에는 stackless coroutine, OS/하드웨어 발전 등으로 권장되지 않음. 만약 계속 이대로 간다면 Zig는 Go랑 비슷한 성능에서 한계에 부딪히고, 진정한 퍼포먼스 경쟁자로는 어렵게 됨. std.fs는 퍼포먼스가 필요한 케이스에서 남아있길 바람
우리가 green thread(파일버)에 ‘올인’한다는 인상은 오해임. OP 참고 기사에서 stackless coroutine 기반 구현을 기대한다는 점을 명시적으로 언급했고, 관련 제안도 있음 https://github.com/ziglang/zig/issues/23446. 퍼포먼스는 중요하고, 파일버가 성능적으로 기대 이하라면 보편적으로 쓰이지 않을 것임. 이 기사에서 논의된 내용은 stackless coroutine이 기본 ‘Io’ 구현이 되는 걸 막지 않음
green thread가 퍼포먼스가 나쁘다는 주장에 대해 의문임. 상위 동시성 서버 플랫폼(Go, Erlang, Java)이 모두 green thread를 쓰거나 쓰려고 함. green thread는 C FFI와의 호환 문제로 더 저수준 언어(Rust 등)에서는 적합하지 않을 수 있지만, 퍼포먼스 자체가 항상 문제라고만 보긴 힘듦
여러 선택지 중 하나이기 때문에 ‘all-in’이라고 볼 수 없다고 생각함. 어떤 구현을 택할지는 실행파일에서 결정하고, 라이브러리 코드에서는 결정되지 않음
Rust가 green thread를 제거하고 async runtime으로 교체한 선택과 비슷한 효과를 Zig도 노리고 있음. 핵심은 ‘async=IO, IO=async’인 점을 공식화한 직관임. Rust는 tokio 등 pluggable async runtime, Zig는 pluggable IO runtime을 제공하는 쪽임. 결국 언어에서 런타임을 빼내고, 사용자 영역에서 끼울 수 있게 하면서 모두가 공통된 인터페이스를 공유하는 게 방향임
자료(P1364R0)는 논쟁적이었고, 나는 특정 접근법을 없애기 위해 동기부여된 주장이라고 생각함. 논의 자료로는 https://old.reddit.com/r/cpp/comments/1jwlur9/stackful_coroutines_faster_than_stackless/, https://old.reddit.com/r/programming/comments/dgfxde/fibers_arent_useful_for_much_any_more/f3bmpww/ 등도 참고 가능함
Zig 같은 시스템 언어에서 흔한 표준 IO 연산에까지 런타임 다형성을 강제하는 건 다소 어색하다고 느낌. 대부분의 실전에서는 IO 구현이 정적으로 확정될 수 있는데 왜 런타임 오버헤드를 강요해야 하는지 의문임
IO에서는 동적 디스패치 오버헤드가 실제로는 거의 미미할 거라 생각함. IO 대상에 따라 다르긴 하겠지만 결과적으로 IO가 CPU 병목이 아닌 경우가 훨씬 많음. 그래서 IO 바운드란 이름도 붙음
“왜 모두에게 런타임 오버헤드를 강제하나?”라는 질문에, 대부분 한 종류의 io만 쓰는 시스템에서는 컴파일러가 double indirection(간접 참조) 비용 자체를 최적화해서 없앨 의도로 보임. 그리고 IO는 어차피 bottleneck이 따로 있어서, 인디렉션 한 번 늘어나는 건 부담이 거의 없음
Zig의 철학상 바이너리 크기에 더 신경을 쓰는 편임. Allocator도 똑같은 트레이드오프가 있는데, 예를 들어 ArrayListUnmanaged는 allocator에 대해 generic하지 않으므로 매 할당마다 dynamic dispatch가 발생함. 실제로는 파일 할당이나 쓰기 비용이 간접 호출 오버헤드를 훨씬 압도함. 이런 바이너리 사이즈에 집착하는 게 Zig 스타일임. 참고로 devirtualization(동적 호출을 정적으로 바꾸는 최적화)은 미신임
런타임 다형성 자체가 본질적으로 나쁜 것은 아님. tight loop에서 브랜치가 생긴다든지, 컴파일러가 인라인 최적화를 못한다든지 그런 상황이 아니면 문제 상황이 아님
새 io 파라미터가 여기저기 드러나는 게 썩 마음에 들진 않지만, 여러 구현(thread 기반, fiber 기반 등)을 쉽게 쓸 수 있고 사용자에게 구현체를 강요하지 않는 점(Allocator 인터페이스처럼)이 아주 마음에 듦. 전체적으로 상당한 개선이고, 여러 stdlib 구현체 중 별도의 오버헤드 없는 동기/블로킹 io 구현이 제공된다면 “쓰지 않는 것에 돈을 내지 않는다”는 Zig 철학을 그대로 따르는 셈임
Zig에서는 io.async가 비동기성(작업의 순서가 보장되지 않을 수 있지만 결과는 올바름)을 표현할 뿐, 동시성(concurrency)을 나타내는 게 아님. 즉, async와 io 호출의 의미를 분리했다는 점이 핵심임. 이 설계가 아주 영리하다고 생각함
IO 인터페이스 덕분에 언어 차원의 vfs(Virtual File System)을 만들 수 있다는 점이 마음에 듦
나는 Zig를 배우려 ssh 서버를 간단히 만들어봤음. 이번 IO/이벤트 루프 구조 덕분에 코드의 흐름을 한결 쉽게 이해할 수 있었음. Andy에게 감사함
글이 너무 잘 쓰였고, 매우 흥미롭게 봤음. 특히 WebAssembly에서의 시사점이 기대됨. WASI를 userspace에서 쓸 수도 있고, Bring Your Own IO도 가능한 구조라니 정말 재미있다는 생각임