[Uvicorn] bytes에서 bytearray로 변경하여 HTTP 바디 누적 O(n²) → O(n) 개선
PR 링크: encode/uvicorn#2845 상태: Merged | 변경: +6 / -6
들어가며
Uvicorn은 Python의 대표적인 ASGI 서버입니다. HTTP 요청 바디가 chunked transfer encoding으로 분할 전송될 때, 각 청크를 수신하면서 바디를 누적합니다. 기존 구현은 bytes += chunk 패턴을 사용했는데, Python의 bytes는 불변(immutable) 타입이므로 매번 새 객체를 할당하고 전체 내용을 복사해야 합니다. 이 PR은 가변(mutable) 타입인 bytearray로 변경하여 in-place 확장이 가능하도록 합니다.
핵심 코드 분석
Before: bytes += — O(n²)
# h11_impl.py
class RequestResponseCycle:
def __init__(self, ...):
self.body = b"" # 불변 bytes
self.more_body = True
async def receive(self) -> ASGIReceiveEvent:
message: HTTPRequestEvent = {
"type": "http.request",
"body": self.body, # 그대로 전달
"more_body": self.more_body,
}
self.body = b"" # 초기화
return message
bytes는 불변이므로 self.body += chunk가 실행될 때마다:
- 기존
self.body크기 +chunk크기의 새 bytes 객체 할당 - 기존 내용 전체 복사
- 새 chunk 복사
- 이전 bytes 객체 GC 대상으로 전환
n개의 청크가 각각 k바이트라면, 총 복사량은 k + 2k + 3k + ... + nk = O(n²k)입니다.
After: bytearray += — amortized O(1)
# h11_impl.py
class RequestResponseCycle:
def __init__(self, ...):
self.body = bytearray() # 가변 bytearray
self.more_body = True
async def receive(self) -> ASGIReceiveEvent:
message: HTTPRequestEvent = {
"type": "http.request",
"body": bytes(self.body), # ASGI 인터페이스를 위해 변환
"more_body": self.more_body,
}
self.body = bytearray() # 초기화
return message
bytearray는 내부적으로 over-allocation 전략을 사용합니다. bytearray += chunk는 여유 공간이 있으면 memcpy만 수행하고, 공간이 부족하면 현재 크기의 약 1.125배로 재할당합니다. amortized O(1) per append입니다.
httptools_impl.py도 동일하게 변경
# httptools_impl.py
self.body = bytearray() # b"" → bytearray()
message: HTTPRequestEvent = {
"type": "http.request",
"body": bytes(self.body), # self.body → bytes(self.body)
"more_body": self.more_body
}
self.body = bytearray() # b"" → bytearray()
h11과 httptools 두 프로토콜 구현 모두에 적용됩니다.
왜 이게 좋은가
1. 대용량 요청에서의 실질적 효과
파일 업로드나 대규모 JSON 페이로드를 chunked transfer로 받을 때 효과가 극대화됩니다. 10MB 파일을 8KB 청크로 받으면 약 1,250개 청크이며:
- bytes: 약 6.25GB의 누적 복사 (1,250 × 1,251 / 2 × 8KB)
- bytearray: 약 10MB의 총 복사 + 몇 번의 재할당
2. ASGI 인터페이스 호환성
ASGI 명세에서 http.request 메시지의 body는 bytes 타입이어야 합니다. bytes(self.body)로 최종 변환하는 비용은 O(n)이지만, receive()는 누적이 완료된 후 한 번만 호출되므로 전체 비용에 영향이 미미합니다.
3. CPython의 bytes 최적화 한계
CPython에는 bytes +=에 대한 특별 최적화(참조 카운트가 1일 때 in-place 확장 시도)가 있지만, 이는 보장되지 않으며 다른 Python 구현체(PyPy 등)에서는 적용되지 않습니다. bytearray는 모든 구현체에서 일관되게 효율적입니다.
4. 6줄의 변경, 984개 테스트 통과
b"" → bytearray(), self.body → bytes(self.body) 변경만으로 전체 테스트 984개가 통과합니다. 최소한의 변경으로 최대의 효과를 얻는 전형적인 성능 개선입니다.
참고 자료
- Python bytearray 문서 — bytearray의 가변 시퀀스 특성
- CPython bytes 연결 최적화 — bytes_concat의 in-place 최적화 조건
- ASGI HTTP Connection Scope — ASGI HTTP 요청 메시지 명세
관련 포스트
PR Analysis 의 다른글
- 이전글 [triton] AMD AtomicCAS의 Tensor Operand Thread Predicate 수정
- 현재글 : [Uvicorn] bytes에서 bytearray로 변경하여 HTTP 바디 누적 O(n²) → O(n) 개선
- 다음글 [vllm] FlashInfer MoE A2A Kernel - NVLink 기반 Expert Parallelism 통신
댓글