# Async Rust는 MVP 상태를 벗어난 적이 없음

> Clean Markdown view of GeekNews topic #29196. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=29196](https://news.hada.io/topic?id=29196)
- GeekNews Markdown: [https://news.hada.io/topic/29196.md](https://news.hada.io/topic/29196.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2026-05-06T00:03:33+09:00
- Updated: 2026-05-06T00:03:33+09:00
- Original source: [tweedegolf.nl](https://tweedegolf.nl/en/blog/237/async-rust-never-left-the-mvp-state)
- Points: 1
- Comments: 1

## Topic Body

- **Async Rust**는 실행기와 독립적인 코드를 서버와 마이크로컨트롤러에서 함께 돌릴 수 있게 하지만, 컴파일러가 만드는 상태 머신 때문에 특히 임베디드에서 바이너리 크기 증가가 두드러짐
- `bar()`처럼 await 지점이 2개인 단순 예제도 **360줄의 MIR**와 `Unresumed`, `Returned`, `Panicked`, `Suspend0`, `Suspend1` 상태를 만들며, 동기 버전은 23줄만 필요함
- 완료된 future를 다시 poll했을 때 `panic` 대신 `Poll::Pending`을 반환하도록 바꾸면 unsafe 동작 없이 계약을 만족할 수 있고, 실험에서 임베디드 펌웨어의 **바이너리 크기가 2%~5% 감소**함
- await가 없는 `async { 5 }`도 현재는 기본 3개 상태의 상태 머신을 만들지만, 매번 `Poll::Ready(5)`를 반환하도록 최적화하면 임베디드 바이너리 크기가 **0.2% 감소**함
- 제안된 [Project Goal](https://rust-lang.github.io/rust-project-goals/2026/async-statemachine-optimisation.html)은 릴리스 모드의 완료 후 panic 제거, await 없는 async block의 상태 머신 제거, 단일 await future 인라인, 동일 상태 접기를 컴파일러에서 추진하려는 작업임

---

### Async Rust의 컴파일러 수준 비대화 문제
- Async Rust는 실행기(executor)에 독립적인 코드를 서버와 마이크로컨트롤러에서 동시에 실행할 수 있게 해주지만, 작은 마이크로컨트롤러에서는 바이너리 크기 증가가 특히 눈에 띔
- Rust 블로그는 async/await를 **무비용 추상화**로 소개했지만, async는 실제로 많은 비대화(bloat)를 만들며 데스크톱과 서버에도 같은 문제가 있으나 메모리와 연산 자원이 많아 덜 드러남
- async 코드 작성 시 비대화를 피하는 [우회 방법](https://tweedegolf.nl/en/blog/235/debloat-your-async-rust)에 이어, 문제를 컴파일러에서 해결하기 위한 [Project Goal](https://rust-lang.github.io/rust-project-goals/2026/async-statemachine-optimisation.html)이 제출됨
- future가 필요 이상으로 커지고 복사가 많아지는 문제는 범위에서 제외됨
  - 이 문제는 이미 알려져 있으며, 일부를 다루는 PR이 열려 있음: <https://github.com/rust-lang/rust/pull/135527>

### 생성된 future의 구조
- 예제 코드는 `foo()`가 `async { 5 }`를 반환하고, `bar()`가 `foo().await + foo().await`를 수행함
  - Godbolt 예제: [godbolt](https://godbolt.org/z/h6761nrEG)
- `bar`에는 await 지점이 2개 있으므로 상태 머신에 최소 2개 상태가 필요하지만, 실제로는 더 많은 상태가 생성됨
- Rust 컴파일러는 여러 패스에서 MIR를 덤프할 수 있으며, `coroutine_resume` 패스는 마지막 async 전용 MIR 패스임
  - async는 MIR에는 남아 있지만 LLVM IR에는 남지 않으므로, async가 상태 머신으로 변환되는 과정은 MIR 패스에서 일어남
- `bar` 함수는 **360줄의 MIR**를 생성하며, 동기 버전은 **23줄**만 사용함
- 컴파일러가 출력하는 `CoroutineLayout`은 사실상 enum 형태의 상태 집합임
  - `Unresumed`: 시작 상태
  - `Returned`: 완료된 상태
  - `Panicked`: 패닉 이후 상태
  - `Suspend0`: 첫 번째 await 지점이며 `foo` future를 저장함
  - `Suspend1`: 두 번째 await 지점이며 첫 번째 결과와 두 번째 `foo` future를 저장함
- `Future::poll`은 안전한 함수이므로 future가 이미 완료된 뒤 다시 호출되어도 UB를 일으키면 안 됨
  - 현재는 `Suspend1` 이후 `Ready`를 반환하고 future를 `Returned` 상태로 바꿈
  - 이 상태에서 다시 poll하면 panic이 발생함
- `Panicked` 상태는 async 함수가 panic한 뒤 `catch_unwind`로 이를 잡았을 때 해당 future를 다시 poll하지 못하게 하기 위한 상태로 보임
  - panic 이후 future는 불완전한 상태일 수 있으므로 다시 poll하면 UB로 이어질 수 있음
  - 이 메커니즘은 mutex poisoning과 매우 유사함
  - `Panicked` 상태에 대한 이 해석은 확실한 문서를 찾기 어려워 90% 정도 확신하는 수준임

### 완료 후 poll에서 꼭 panic해야 하는가
- `Returned` 상태의 future는 현재 panic하지만, 반드시 그래야 하는 것은 아님
  - 필요한 조건은 UB를 일으키지 않는 것뿐임
- panic은 비교적 비싸며, 최적화로 제거하기 어려운 부작용이 있는 경로를 추가함
- 완료된 future를 다시 poll했을 때 `Poll::Pending`을 반환하면 unsafe 동작 없이 `Future` 타입의 계약을 만족할 수 있음
- 컴파일러를 수정해 이 방식을 실험했을 때, async 임베디드 펌웨어에서 **2%~5%의 바이너리 크기 감소**가 확인됨
- 이 동작은 정수 오버플로의 `overflow-checks = false`처럼 스위치로 제공하는 방식이 제안됨
  - 디버그 빌드에서는 잘못된 동작을 즉시 드러내기 위해 계속 panic함
  - 릴리스 빌드에서는 더 작은 future를 얻을 수 있음
- `panic=abort`를 사용할 때는 `Panicked` 상태 자체를 제거할 수 있을 가능성이 있으며, 그 영향은 추가 검토가 필요함

### await가 없어도 항상 상태 머신이 생성됨
- `foo()`는 `async { 5 }`만 반환하므로 수동 구현의 최적 형태는 상태 없이 항상 `Poll::Ready(5)`를 반환하는 future임
- 하지만 컴파일러가 생성한 MIR에는 `Unresumed`, `Returned`, `Panicked`라는 기본 3개 상태가 여전히 존재함
  - poll 시 현재 상태의 discriminant를 확인하고 분기함
  - 완료 후 다시 poll하면 `` `async fn` resumed after completion `` assert로 panic함
- 이 경우에는 상태 머신을 만들지 않고 매번 `Poll::Ready(5)`를 반환하도록 최적화할 수 있음
- 컴파일러에 이를 실험적으로 적용했을 때 임베디드 바이너리 크기가 **0.2% 감소**함
  - 절감 폭은 크지 않지만 단순한 최적화라 적용 가치가 있을 가능성이 있음
- 이 최적화는 동작을 조금 바꾸지만, 영향을 받는 것은 규약을 지키지 않는 실행기뿐임
  - 현재 컴파일러는 이후 poll에서 panic함
  - 최적화 후에는 future가 항상 `Ready`를 반환함

### LLVM만으로는 충분하지 않음
- MIR 출력이 비효율적이어도 LLVM이 모두 정리해줄 수 있는 경우가 있지만, 조건이 제한적임
  - future가 충분히 단순해야 함
  - `opt-level=3`을 사용해야 함
- future가 복잡해지면 LLVM이 제거하지 못하며, 관용적인 async Rust 코드에서는 future가 깊게 중첩되기 때문에 복잡도가 빠르게 커짐
- 임베디드나 wasm처럼 크기 최적화를 자주 하는 환경에서는 LLVM이 이를 모두 최적화하지 못함
- Godbolt 예제: <https://godbolt.org/z/58ahb3nne>
  - 생성된 어셈블리에서 LLVM은 `foo`가 5를 반환한다는 점은 알지만, `bar`의 답을 10으로 최적화하지 못함
  - `foo`의 poll 함수 호출도 남아 있음
  - 컴파일러가 완전히 파악하지 못하는 잠재적 panic 경로 때문임
  - LLVM은 `foo`가 실제로 한 번만 호출되고 panic하지 않는다는 점을 알지 못함
- IR에서 panic 분기를 주석 처리하면 더 잘 최적화됨: <https://godbolt.org/z/38KqjsY8E>
- LLVM에 사후 최적화를 기대하기보다, 컴파일러가 LLVM에 더 좋은 입력을 제공해야 함

### future 인라인이 잘 되지 않음
- 인라인은 이후 최적화 패스를 가능하게 하므로 중요하지만, 생성된 Rust future는 현재 이른 단계에서 인라인되지 않음
- 각 future가 구현을 얻은 뒤 LLVM과 링커가 인라인 기회를 얻지만, 앞선 문제 때문에 그 시점은 너무 늦음
- 가장 직접적인 인라인 기회는 `bar()`가 단순히 `foo(blah).await`만 수행하는 형태임
  - trait을 사용해 추상화를 만들 때 자주 나타나는 패턴임
  - 현재 컴파일러는 `bar`용 상태 머신을 만들고 그 안에서 `foo` 상태 머신을 호출함
  - 더 효율적으로는 `bar`가 `foo` future 자체가 될 수 있음
- preamble과 postamble이 있는 경우는 더 복잡함
  - 예: `bar(input)`이 `input > 10`으로 `blah`를 만든 뒤 `foo(blah).await`하고 결과에 `* 2`를 적용함
  - async 함수를 다른 시그니처로 변환할 때, 특히 trait 구현에서 흔함
- 이 형태의 `bar`도 자체 async 상태가 필요하지 않음
  - 단일 await 지점을 넘어 보존되는 데이터가 `foo`에 잡힌 값 외에는 없음
  - 다만 `bar`가 단순히 `foo` 자체가 될 수는 없고, 대부분의 상태를 `foo`에 의존할 수 있음
- 수동 구현에서는 `BarFut`가 `Unresumed { input }`와 `Inlined { foo: FooFut }` 상태를 가질 수 있음
  - 첫 poll에서 preamble을 실행해 `foo(blah)`를 만들고 `Inlined` 상태로 바꿈
  - 이후 `foo.poll(cx)` 결과에 postamble을 적용함
- 첫 await 지점 전까지 코드를 미리 실행할 수 있다면 `Unresumed` 상태도 제거할 수 있지만, future는 poll되기 전에는 아무것도 하지 않는다는 점이 [보장](https://doc.rust-lang.org/reference/items/functions.html?highlight=function#r-items.fn.async.future)되므로 바꿀 수 없음
- poll 중인 future의 속성을 질의할 수 있다면 추가 인라인 최적화가 가능함
  - 예를 들어 future가 첫 poll에서 항상 ready를 반환한다는 사실을 알 수 있다면, 호출자 future에서 해당 await 지점의 상태를 만들 필요가 없음
  - 이런 최적화를 재귀적으로 적용하면 많은 future를 훨씬 단순한 상태 머신으로 접을 수 있음
- 현재 `rustc` 구조에서는 각 async block이 개별적으로 변환되고 이후 관련 데이터가 보존되지 않아 이런 질의가 가능하지 않은 것으로 보임
- future 인라인은 아직 실험되지 않았지만, 바이너리 크기와 성능에 큰 도움이 될 것으로 예상됨

### 동일한 상태 접기
- async block의 각 await 지점마다 상태 머신에는 추가 상태가 생김
- 다음과 같은 코드는 자연스럽지만, 두 분기에서 같은 async 함수를 await하므로 동일한 상태가 2개 생김
  - `CommandId::A => send_response(123).await`
  - `CommandId::B => send_response(456).await`
- 이 경우 `CoroutineLayout`에는 `send_response`의 동일한 coroutine 타입을 저장하는 `_s0`, `_s1`이 각각 생기고, `Suspend0`, `Suspend1` 두 상태가 만들어짐
- 이 함수의 MIR는 **456줄**이며, 많은 기본 블록이 사실상 중복됨
- 코드를 먼저 응답 값만 계산한 뒤 한 번만 `send_response(response).await`하도록 수동 리팩터링하면 중복 상태가 없어짐
  - `CommandId::A`는 `123`
  - `CommandId::B`는 `456`
  - 이후 `send_response(response).await`
- 리팩터링 후 `CoroutineLayout`에는 저장된 future가 하나만 있고 `Suspend0` 상태 하나만 남음
- 전체 MIR 길이는 **302줄**로 줄고 중복이 사라짐
- 따라서 동일한 코드 경로와 상태를 찾아 하나로 접는 최적화 패스가 유용해 보임
  - 이 최적화는 future 인라인 패스와 잘 결합될 가능성이 있음

### 실험 링크와 추가 벤치마크
- 두 실험을 함께 적용하면 `smol` 실행기를 사용한 x86 합성 벤치마크에서 약 **3% 성능 향상**이 나옴
- No panics in poll after ready: <https://github.com/rust-lang/rust/compare/main...diondokter:rust:resume-pending>
- No await, no statemachine: <https://github.com/rust-lang/rust/compare/main...diondokter:rust:no-statemachine-when-no-await>

### Project Goal 지원 요청
- 이 작업은 컴파일러에서 진행하기 위해 Project Goal로 제출됨: <https://rust-lang.github.io/rust-project-goals/2026/async-statemachine-optimisation.html>
- 펀딩 없이는 많은 작업을 진행하기 어렵기 때문에, 이 작업의 혜택을 받을 회사나 조직의 부분적 또는 전체 지원이 필요함
- 연락처는 [`dion@tweedegolf.com`](mailto:dion@tweedegolf.com)임
- 작업 범위와 필요한 펀딩 규모는 유연하지만, **€30k**면 전체 또는 상당 부분을 완료할 수 있을 것으로 추정됨

## Comments



### Comment 56884

- Author: neo
- Created: 2026-05-06T00:03:34+09:00
- Points: 1

###### [Lobste.rs 의견들](https://lobste.rs/s/flowcy/async_rust_never_left_mvp_state) 
- 제목만 보고 예상했던 것보다 훨씬 **건설적인 글**이었음
  - 그냥 사실에 가깝다고 봄. **MVP 출시 후 7년**이 지났지만 언어 설계나 컴파일러 구현에서 거의 진전이 없었고, MVP를 주로 만들어낸 사람들이 비슷한 시기에 프로젝트 활동을 줄이면서 이후 전달이 멈춰버린 상태임  
    이 작업을 하려는 사람이 필요한 지원을 받았으면 함

- > I want to work on this in the compiler and as such have submitted it as a [Project Goal](https://rust-lang.github.io/rust-project-goals/2026/async-statemachine-optimisation.html)
  
  >> Stop generating statemachines that don’t have to be there  
  >> Make the compiler’s job easier by removing panic paths and branches  
  >> Make statemachines smaller
  
  이 문제가 다뤄지는 걸 보니 좋음. 지금 **rustc가 LLVM에 너무 많은 코드**를 넘기고 최적화기가 전부 잡아주길 기대한다는 글을 몇 번 봤는데, 특히 이 글은 그 작업을 위한 **자금 지원**도 요청하고 있음

- 맙소사, 내가 멍청했음  
  async는 어떤 형태로든 런타임, 작업 추적, 완료를 확인하는 **폴링**이 필요하니 본질적으로 “비대하다”고 늘 생각했음. 그 오버헤드는 0이 아니니까  
  여기서 말하는 “무비용 추상화”는 언어 기능에 관한 것이고, 덧붙여진 런타임과는 별개라고 여겼음  
  LLVM에 넘기기 전에 **rustc가 무엇을 내보내는지** 살펴볼 생각조차 못 했음

- async Rust에 익숙하지 않은 사람들을 위해:  
  > It's amazing how we can write executor agnostic code that can run concurrently on huge servers and tiny microcontrollers.
  
  이건 정말 맞는 말임. async 호출이 중첩된 트리도 최대 최적화를 거치면 내부에 상태 기계를 가진 **단일 구조체**로 굳어짐. 정말 영리한 방식임

- 릴리스 빌드에서 이 경우에 도달하면 일종의 **교착 상태**가 생기는 건가? 아니면 항상 `Pending`인 작업을 기다리는 태스크들 때문에 누수가 생길 수도 있나?
  - 맞음. 그런 future들은 **멈춘 상태**가 되어 절대 완료되지 않음. 다만 그런 상태는 이미 버그가 있는 저수준 async 코드에서만 도달할 수 있고, 완료된 future를 제대로 추적하지 못하는 코드는 아마 이미 누수와 교착을 만들고 있을 가능성이 큼  
    `.await`로는 잘못된 폴링을 할 수 없음

- 몇 가지 생각이 듦:  
  1. 이 글은 더 많은 최적화 로직을 LLVM 밖으로 빼서 **MIR 계층**으로 옮겨야 한다는 주장처럼 보임. 예를 들어 async 함수 인라이닝이 LLVM보다 MIR에서 더 쉬운 이유는 이해됨. async에 대해 MIR에서 해냈다면, 그 로직을 동기 함수에도 일반화하고 LLVM의 일부 최적화 패스를 제거하면 어떨까 싶음. 큰 작업이라는 건 알고 있고 실용적인 질문이라기보다 방향성에 가까움. 프런트엔드/미들엔드 컴파일러가 어느 정도 복잡해지면 LLVM의 범용 최적화 상당수가 다른 곳으로 옮겨지는 편이 더 나을 수도 있어 보임  
  2. 여전히 **`panic=unwind`** 가 마음에 들지 않음. 일부 테스트 하네스를 제외하면 비용을 상쇄할 만큼 `panic=abort`보다 나은 장점을 본 적이 거의 없음. 테스트 하네스조차도 Linux에서는 난해하게 `clone`을 써서 `pthread_join` 대신 실행 스레드를 `wait`하는 식으로 비슷한 선택 적용이 가능할 것 같음. 이 부분은 내가 틀렸을 수도 있음

- 링크가 다른 사람에게도 방금 죽었나?  
  수정: 블로그 글이 반초쯤 보이다가 **404 페이지**로 넘어감  
  수정 2: 블로그 글 목록으로 들어가서 이것저것 눌러봤고, 목록에 있는 그 글을 열어도 404 페이지로 감. 정적 페이지이거나 적어도 그래야 할 블로그를 어떻게 이렇게 망칠 수 있지?
  - 말투가 조금 불필요하게 무례하고 공격적으로 느껴짐. 웹사이트에도 버그는 생길 수 있고, 보고하는 건 유용하지만 이 댓글은 좀 심술궂게 들림  
    참고로 같은 재현 절차를 따라본 것 같은데 나는 404가 전혀 안 나왔음. 휴대폰과 데스크톱에서, JavaScript를 켜고 끈 상태 모두 시도해 봄. 그래서 겪은 현상이 보기보다 더 복잡했을 수도 있어 보임
