GN⁺: Go로 10억 행 처리하기 도전: 9개의 방법으로 1분45초에서 4초로 단축
(benhoyt.com)- 1BRC : 10억행이 있는 텍스트 파일에서 온도 측정값을 읽어 관측소별 최소/평균/최대 온도를 계산하는 코드를 작성하는 챌린지
- 2024년 1월 1일부터 1월 31일까지 진행했으며, 최신 Java를 최대한 활용하는 것이 목표였음
- 이에 대해 사람들이 관심을 가지고 다양한 언어(Rust,Go,C++,SQL)로 도전하기 시작
- Go로 작성한 9가지 솔루션에 대해서 상세 소개 (느린것부터 빠른 것순으로)
기본 측정값
-
cat
명령어를 사용하여 10억행 텍스트데이터(13GB) 데이터를 읽는 데 걸리는 시간은 1.052초임. - 실제로 파일을 처리하는
wc
명령어는 거의 1분이 걸림(55.710초). - AWK 솔루션을 사용하여 문제를 해결하는 데 걸리는 시간은 7분 35초임.
솔루션 1: 간단하고 관용적인 Go
- Go 표준 라이브러리를 사용한 첫 번째 솔루션은 1분 45초가 걸림.
-
bufio.Scanner
로 줄을 읽고,strings.Cut
으로 ';'를 기준으로 분리함. -
strconv.ParseFloat
로 온도를 파싱하고, Go 맵을 사용하여 결과를 누적함.
솔루션 2: 포인터 값이 있는 맵
- 맵에서 두 번의 해싱을 피하기 위해
map[string]*stats
를 사용함. - 포인터 값을 사용하여 시간을 1분 45초에서 1분 31초로 단축함.
솔루션 3: strconv.ParseFloat
피하기
-
strconv.ParseFloat
대신 사용자 정의 코드를 사용하여 온도를 파싱함. - 시간을 1분 31초에서 55.8초로 단축함.
솔루션 4: 고정 소수점 정수 사용
- 온도를 정수로 표현하여 부동 소수점 연산을 피함.
- 시간을 55.8초에서 51.0초로 단축함.
솔루션 5: bytes.Cut
피하기
- ';'를 찾기 위해 전체 스테이션 이름을 스캔하는 대신 끝에서부터 파싱함.
- 시간을 51.0초에서 46.0초로 단축함.
솔루션 6: bufio.Scanner
피하기
-
bufio.Scanner
를 제거하고 파일을 큰 청크로 읽음. - 시간을 46.0초에서 41.3초로 단축함.
솔루션 7: 사용자 정의 해시 테이블
- Go의 맵 대신 사용자 정의 해시 테이블을 구현함.
- 시간을 41.3초에서 25.8초로 단축함.
솔루션 8: 청크 병렬 처리
- 간단하고 관용적인 코드를 병렬화하여 시간을 1분 45초에서 24.3초로 단축함.
솔루션 9: 모든 최적화 및 병렬 처리
- 모든 최적화를 병렬 처리와 결합하여 시간을 24.3초에서 3.99초로 단축함.
결과 테이블
- 모든 Go 솔루션과 가장 빠른 Go 및 Java 솔루션을 비교한 표 제공.
- Go 버전 중 가장 빠른 것은 2.90초, Java 버전은 0.953초로 처리함.
- 1초도 안걸리는 Java버전은 Thomas Wuerthinger(GraalVM 제작자)가 한 것으로, 이 분야 전문가이기 때문에 가능한듯
최종 코멘트
- 일상적인 프로그래밍 작업에서는 간단하고 관용적인 코드가 좋은 출발점임.
- 데이터 처리 파이프라인을 구축하는 경우, 코드를 4배 또는 26배 빠르게 만들면 사용자 만족도를 높이고 컴퓨팅 비용을 절약할 수 있음.
- 런타임이나 인터프리터를 구축하는 경우, 성능 향상이 중요함.
GN⁺의 의견
- 이 기사는 Go 언어를 사용하여 대규모 데이터 처리를 최적화하는 다양한 방법을 탐구함으로써, 성능 최적화에 대한 흥미로운 사례 연구를 제공함.
- 최적화 과정에서 Go의 표준 라이브러리를 넘어서 사용자 정의 해시 테이블과 같은 데이터 구조를 구현하는 것이 중요한 역할을 함을 보여줌.
- 병렬 처리의 효과를 강조하며, 단일 코어 최적화와 병렬화를 결합하여 놀라운 성능 향상을 달성함.
- 이 기사는 성능에 민감한 애플리케이션을 개발하는 소프트웨어 엔지니어에게 유용한 인사이트를 제공함.
- 이러한 최적화가 실제 프로덕션 환경에서 얼마나 유용할지는 사용 사례에 따라 다를 수 있음. 모든 애플리케이션에 이러한 수준의 최적화가 필요하지 않을 수 있음.
Hacker News 의견
- 첫 번째 사용자는 데이터 조작을 위한 코드 최적화 경험이 없었기 때문에,
cat
,wc
등을 사용해 기본 측정값을 얻는 첫 번째 섹션이 특히 흥미로웠다고 언급함. 이러한 방법은 "합리적인" 범위를 얻는 쉬운 방법이라고 생각함. - 두 번째 사용자는 Polars 라이브러리를 사용한 경우의 처리 시간이 33초임을 언급하며, 가장 빠른 수작업 최적화 솔루션에 근접하는 가장 간단한 솔루션에 대해 관심을 표현함.
- 세 번째 사용자는 Go 언어의 성능 분석 보고서가 혼란스럽다고 언급하며, 특정 코드 라인의 실행 시간이 직관적이지 않을 경우, 데이터가 예측하기 어렵고 분기 예측기가 잘못 예측할 수 있다고 설명함.
- 네 번째 사용자는 Go 언어로 1BRC(1 Billion Row Challenge)를 수행한 결과를 공유하며, Go 언어 특유의 최적화 기법들을 배웠다고 언급함. 예를 들어,
unsafe.Pointer
를 사용한 경계 검사 없는 메모리 읽기, 표준 라이브러리의bytes
와bits
패키지 함수들이 어셈블리로 작성됨, 가비지 컬렉션을 끄는 설정, 스레드에 고루틴을 고정하는 방법 등이 있음. - 다섯 번째 사용자는 쉘 스크립트 개발자가 다른 언어 개발자들이 준비하는 동안 이미 특정한 10억 행의 데이터 처리를 완료했을 것이라고 주장함.
- 여섯 번째 사용자는 데이터베이스가 애플리케이션 코드보다 빠르고, 덜 복잡하며, 데이터 업데이트에 더 강건하다고 주장하며, 데이터베이스에서 더 많은 작업을 수행해야 한다고 강조함.
- 일곱 번째 사용자는 2010년에 PostgreSQL을 사용해 환경 캐나다의 기후 데이터 2억 7천만 행을 조회하는 웹 앱을 개발했으며, 이 소프트웨어가 수상한 경험을 공유함. 이 앱은 1분 이내에 보고서를 생성할 수 있도록 최적화되었음.
- 여덟 번째 사용자는 Go 언어에서 병렬 코드가 여전히 Go의 관용적인 코드 스타일을 유지한다는 사실이 멋지다고 언급함.
- 아홉 번째 사용자는 대규모 텍스트 파일을 CLI에서 다룰 때 유니코드 파싱을 생략하면
awk
,grep
등이 한 차원 빠르다고 언급하며,awk
솔루션에LC_ALL=C
를 추가하면 1분 이내로 처리 시간을 단축할 수 있다고 주장함. - 마지막 사용자는 가장 빠른 Java 버전이 가장 빠른 Go 버전보다 빠르다는 것이 흥미롭다고 언급하며, 자바 가상 머신(JVM)의 성능이 상당히 좋다고 평가함.
좋은 글 공유 감사합니다. 한때 시스템 최적화에 미쳐있던 때가 떠오르네요 ㅎㅎ
개발 경력이 쌓이면서 최고로 최적화된 코드는 유지보수가 힘들어서 조직 환경에서는 운용하기가 힘든 경험을 많이 하다보니 점차 최적화의 길에서 멀어지게 되었네요.(갑자기 개인 회고)