GN⁺: 2024년 1백만 동시 작업 실행에 필요한 메모리 용량
(hez2010.github.io)벤치마크
-
코루틴이란?
- 코루틴은 프로그램의 실행을 일시 중지하고 재개할 수 있는 컴퓨터 프로그램 구성 요소로, 협력적 멀티태스킹을 위한 서브루틴을 일반화한 것임.
- 협력 작업, 예외, 이벤트 루프, 반복자, 무한 리스트 및 파이프와 같은 프로그램 구성 요소 구현에 적합함.
-
Rust
- 두 가지 프로그램을 작성함:
tokio
와async_std
를 사용한 프로그램. - 둘 다 Rust에서 일반적으로 사용되는 비동기 런타임임.
- 두 가지 프로그램을 작성함:
-
C#
- C#은 Rust와 유사하게
async/await
를 지원함. - .NET 7부터 NativeAOT 컴파일을 제공하여 VM 없이도 관리 코드 실행이 가능함.
- C#은 Rust와 유사하게
-
NodeJS
- 비동기 작업을 위해
Promise.all
을 사용함.
- 비동기 작업을 위해
-
Python
-
asyncio
모듈을 사용하여 비동기 작업을 수행함.
-
-
Go
- 고루틴을 사용하여 동시성을 구현하며,
WaitGroup
을 사용하여 작업을 대기함.
- 고루틴을 사용하여 동시성을 구현하며,
-
Java
- JDK 21부터 가상 스레드를 제공하며, 이는 고루틴과 유사한 개념임.
- GraalVM을 사용하여 네이티브 이미지를 생성할 수 있음.
테스트 환경
- 하드웨어: 13세대 Intel(R) Core(TM) i7-13700K
- 운영체제: Debian GNU/Linux 12 (bookworm)
- Rust: 1.82.0
- .NET: 9.0.100
- Go: 1.23.3
- Java: openjdk 23.0.1
- Java (GraalVM): java 23.0.1
- NodeJS: v23.2.0
- Python: 3.13.0
결과
-
최소 메모리 사용량
- Rust, C# (NativeAOT), Go는 네이티브 바이너리로 컴파일되어 적은 메모리를 사용함.
- Java (GraalVM 네이티브 이미지)도 좋은 성능을 보였으나, 다른 정적 컴파일 언어보다는 더 많은 메모리를 사용함.
-
10K 작업
- Rust는 메모리 사용량이 거의 증가하지 않음.
- C# (NativeAOT)도 적은 메모리를 사용함.
- Go는 예상보다 많은 메모리를 사용함.
-
100K 작업
- Rust와 C#이 좋은 성능을 보임.
- C# (NativeAOT)이 Rust보다 적은 메모리를 사용함.
-
100만 작업
- C#이 모든 언어를 압도하며 가장 적은 메모리를 사용함.
- Rust도 메모리 효율성이 뛰어남.
- Go는 다른 언어에 비해 메모리 사용량이 많음.
결론
- 많은 동시 작업은 복잡한 작업을 수행하지 않더라도 상당한 메모리를 소모할 수 있음.
- .NET과 NativeAOT의 개선이 눈에 띄며, GraalVM으로 빌드된 Java 네이티브 이미지도 메모리 효율성이 뛰어남.
- 고루틴은 여전히 자원 소비 면에서 비효율적임.
부록
- Rust (tokio)에서
join_all
대신for
루프를 사용하여 메모리 사용량을 절반으로 줄임. Rust가 이번 벤치마크에서 절대적인 선두를 차지함.
Hacker News 의견
-
벤치마크가 Node와 Go의 비동기 처리 방식의 차이를 제대로 반영하지 못함. Node는
Promise.all
을 사용하고 Go는 고루틴을 사용하여 차이가 있음. 비동기 I/O와 CPU 바운드 작업의 메모리 사용량 차이를 비교하는 것이 흥미로울 것임 -
"10초 동안 대기하는 작업"과 "10초 후에 깨우는 작업"의 차이를 설명함. Go 코드의 메모리 사용량이 다른 코드와 비교하여 차이가 큼
-
Go와 Node의 공정한 비교를 위해 타이머를 스케줄링하는 고루틴과 타이머 신호를 처리하는 고루틴을 사용하는 방법을 제안함. Node에 Bun과 Deno가 포함되지 않은 점이 이상하다고 언급함
-
많은 동시 작업이 메모리를 많이 소비할 수 있지만, 작업당 데이터가 몇 KB 이상이면 스케줄러의 메모리 오버헤드는 무시할 수 있을 정도임
-
"동시 작업"의 정의에 따라 메모리 사용량이 달라질 수 있음. 효율적인 구현에서는 1M 동시 작업에 약 200MB가 필요함
-
Go가 메모리 사용량에서 Java에 비해 2배 이상 뒤처진다는 점을 지적하며, 벤치마크가 실제 프로그램을 대표하지 않는다고 언급함
-
간단한 코드로 언어를 비교하는 것이 개발자에게 불공평할 수 있으며, 실제 작업을 추가하여 메모리 사용량과 스케줄링 차이를 측정할 것을 권장함
-
벤치마크가 종종 오류로 가득 차 있으며, 이러한 벤치마크를 게시하는 사람들의 동기를 이해하지 못하겠다고 말함
-
Java 벤치마크가 잘못되었을 가능성이 있으며,
ArrayList
의 초기 크기를 지정하지 않아 불필요한 객체가 많이 생성됨 -
Rust의 비동기 코드가 예상보다 빠르게 완료되는 이유를 설명함.
tokio::time::sleep()
이 미래가 생성된 시점을 추적하기 때문임