단순한 CRUD 웹사이트를 예로 들며 대부분의 실제 프로그램이 I/O 병목이라고 하는 건, 실제 프로그램을 서버로만 보는 관점임. 서버 밖에도 I/O 병목인 프로그램이나 구간은 있지만, 브라우저 레이아웃 엔진, 3D 렌더러, 워드프로세서, 신경망처럼 그렇지 않은 프로그램도 많음.
서버에서도 맞지 않는 얘기임. DB 조회는 약 5ms인데 평균 응답은 약 500ms나 걸리곤 함. 백엔드 개발에서는 I/O 병목이라는 통념이 강하지만, 그 사이에 있는 작은 CPU 연산들과 부가 비용을 너무 당연하게 받아들임.
서버에서도 이미 오래전부터 맞지 않는 얘기임. SSD는 빠르고, 많은 프로그램은 I/O 처리 경로조차 CPU 병목임. 디스크에 0만 쓰는 mkfile(8)도 macOS에서는 시스템 호출 때문에 심각한 CPU 병목이 생김. mkfile(8) is severely syscall limited on OS X
Wunderlist의 iOS·macOS 클라이언트를 측정할 때도 JSON 읽기의 병목이 I/O가 아니라 CPU여서 놀랐음. 측정 없이 성능을 논하거나 손대면 이 글처럼 터무니없이 잘못된 결론에 이르기 쉬움.
Swift의 기본 JSON 처리는 10MB/s, 설계를 개선한 내 Objective-C 구현은 객체 할당 속도에 가까운 200MB/s였음. 당시 SSD도 2GB/s였으니 Swift는 CPU 99.5%·I/O 0.5%, 내 구현도 CPU 90%·I/O 10%에 해당함. Somewhat Less Lethargic JSON Support for iOS/macOS, Part 1: The Status Quo
이런 격차는 David Lemire가 simdjson을 만든 계기이기도 함. simdjson은 실제로 I/O가 병목이 될 정도로 빠르지만, 당시에는 객체를 직접 생성하는 훅이 없어 파일 전체를 중간 표현으로 파싱한 뒤 다시 순회해야 했음. 지금도 그런지는 확인하지 못했음.
LLM을 제외하더라도 머신러닝 프로그램의 비중이 커지는 상황이라 더 우습게 느껴짐. LLM 이전에도 데이터에 CRUD 화면을 붙이는 프로그램은 비교적 단순했음. 사용 시간의 대부분을 차지할 수는 있지만, 내 주변에서 개발 시간은 위에 나온 것처럼 프로그램과 대규모 시스템의 더 어려운 부분에 주로 들어갔다고 봄.
CRUD 서비스에 치우친 논지는 제쳐두더라도, --help만 입력해도 지연이 바로 느껴지는 CLI에서는 느린 언어가 이미 불리함. 어째서인지 Node.js는 예외임.
“느린 언어는 이미 빠른 언어에 작업을 위임한다”지만, 언어 사이에서 데이터를 주고받기는 쉽지 않음. 파일 형식 변환처럼 FFI 경계에서 문자열을 넣고 문자열을 받으면 끝나는 작업만 있는 게 아님. Python JSON 파서를 rapidjson이나 ujson으로 바꿔도 Python 객체 생성에 시간이 많이 들며, msgspec을 써도 Rust로 다시 쓰는 것보다는 상당한 부가 비용이 남음.
캐시도 어디에 두느냐가 중요함. Python에서는 프로세스 내부 캐시가 오래 유지되지 않고 동시 실행 단위 사이에 공유되지 않아 memcached나 Redis를 택하게 됨. Rust 웹서비스에서는 그런 제약 없이 메모리 내 해시 맵만으로도 상당히 멀리 갈 수 있음.
순진하게 작성한 Rust 코드가 숙련자가 작성한 C 코드보다 30% 빨랐다는 예도 Rust 표준 라이브러리의 설계 덕분이지, 느린 언어에서도 같은 방식이 통한다는 뜻은 아님. 언어 선택은 생태계와 쉽게 구현할 수 있는 설계의 선택임. 나는 허용할 만한 성능을 내려고 Rust보다 Python에서 훨씬 더 자주 무리해야 했음.
“성능이 필요해지면 느린 부분만 C나 Rust로 다시 쓰면 된다”며 Python을 고른 프로젝트를 고치는 데 경력의 상당 부분을 보냈음. 연산 자체는 빨라져도 언어 간 데이터 변환 비용이 늘어 오히려 성능이 떨어지는 경우도 봤음.
이 접근이 통하려면 Python 코드 전반에서 다른 언어의 네이티브 자료구조를 사용해야 하는데, 그 정도로 크게 바꿀 거라면 다중 언어용 맞춤 빌드 도구까지 유지하기보다 아예 그 언어로 옮기는 편이 나음.
Python 성능을 높이려다 추상화 제거, 코드 복사·붙여넣기, newtype 래퍼 포기처럼 좋지 않은 소프트웨어 공학 관행을 반복해서 택해야 했음. 이제는 꼭 필요한 경우가 아니라면 이런 절충을 하고 싶지 않음.
“느린 고수준 언어는 프로그램이 하는 일을 한눈에 볼 수 있어 최적화가 쉽다”는 주장에는 동의하기 어려움. 어셈블리나 C와 비교하면 그럴 수 있지만, 그 차이가 단순히 저수준 언어이기 때문인지는 확실하지 않음.
Python 등을 최적화할 때는 오히려 추상화 아래에서 실제로 무슨 일이 벌어지는지 알아내려고 언어나 패키지와 씨름하게 됨. 상속, 간접 참조, 멀리 떨어진 코드 사이의 숨은 영향이 많으면 전체 구조가 흐려짐. 특히 Julia가 떠오름. 한 번 읽으면 될 파일을 반복문에서 수천 번 읽는 일도 숨어들기 쉬움.
핵심 원인은 동적 타이핑이나 부실한 타입 시스템, 컴파일 시점에 검증할 수 없는 런타임 디스패치에 더 가깝다고 봄. 기존 설계를 풀어헤칠 때는 일부를 저수준 동작이 잘 드러나는 언어로 다시 쓰는 것이 도움이 됨. Rust의 소유권 시스템은 얽힌 의존성을 드러내고, 저수준 제어와 고수준 추상화를 오가기 쉬워 이런 작업에 잘 맞음.
언어와 관계없이 프로그램이 어디에 시간을 쓰는지 추측해서 맞히는 비율은 내 경우 약 50% 임. 나머지 절반은 측정 결과에 완전히 놀라게 됨. 실제 일을 하는 몇 줄에 도달하기까지 지나야 하는 추상화 계층이 너무 많은 것도 언어를 가리지 않는 느린 코드의 특징임.
오히려 Rust 같은 언어에 더 잘 맞는 얘기라고 봄. 컴파일러가 가진 추가 정보 덕분에 C보다 빨라지는 경우도 있기 때문임.
큰 설계 변경이나 알고리즘 변경이 중요하다는 점은 맞음. 하지만 Ruby, Lua, Perl, PHP나 대부분의 Python 코드를 다른 언어로 다시 쓰면 그 자체로 훨씬 빨라지는 경우가 많음. 글은 이를 빠뜨리는 데 그치지 않고 적극적으로 배척하는데, 의도적으로 외면한다고밖에 보기 어려움.
언어별 성능 비교를 보면 됨. https://benchmarksgame-team.pages.debian.net/benchmarksgame/.
어차피 다시 쓸 거라면 재설계도 함께 하면 됨. AI 이전이나 지금이나, 나는 보통 세 번째 구현쯤에서 설계가 정리됐다고 느낌.
성능에 민감한 데이터 분석·수치 계산 Python 사용자 입장에서도 살펴볼 필요가 있음. NumPy를 단순히 Python에 C를 덧붙인 것으로 보고 성능을 전부 C 덕분으로 돌리면, 이 시스템에 축적된 설계의 역할을 과소평가하게 됨.
나는 이를 제한된 연산 영역으로 이해하는 편이 유용하다고 봄. 무거운 계산은 C나 Fortran에, 프로그램 구조와 업무 로직은 Python에 맡긴다는 설명만으로는 충분하지 않음. 이 도구들은 계산을 효율적으로 표현하도록 진화했지만, 지원하는 설계 범위를 벗어나는 일도 드물지 않음.
Python의 자동 정수 승격이나 메모리 배치 제어 부재 같은 제약은 well-documented되어 있음. 구현 개선과 JIT 등이 이를 얼마나 극복할지는 지켜봐야 하지만, CPython은 역사적으로 여러 기본 연산이 빠르지 않았음.
PyData Amsterdam에서 발표했던 그룹별 가중 요약 통계에서는 pandas DataFrame library를 단순하게 쓴 방식, 복잡하게 우회한 방식, cython을 활용한 방식으로 바꾸며 대략 1배 → 10배 → 100배의 성능을 얻었음. Python and pandas로 시뮬레이터를 만들 때도 도구가 표현하기 어려운 계산 때문에 복잡한 우회를 감수하게 됨.
Python은 계속 빨라지지만 일부 기본 연산은 끝내 빠르지 않을 수 있고, 도구의 설계를 모르는 단순한 접근은 느린 프로그램으로 이어지기 쉬움. 반면 설계를 고려한 접근이 반드시 엄청난 노력을 요구하는 것은 아니며, 그렇게 만든 시스템은 C++ 시스템만큼 빠르거나 더 빠를 수도 있음.
최근 참여한 시뮬레이터 프로젝트도 처음부터 우리가 Python과 C·Cython·C++·Rust·Zig 등을 조합해 만들었다면 확장과 디버깅이 쉽고 코드도 적으면서, 단순하게 만들어도 최소 10배, 조금 더 다듬으면 100~1,000배 빨랐을 것이라고 자주 얘기했음. Python 자체가 빠른 언어는 아니고 한 줄씩 다른 언어로 옮기기만 해도 빨라질 프로그램도 많지만, Python 사용이 느린 시스템을 필연적으로 만드는 것은 아님. 빠른 시스템에 필요한 설계 노력도 C++보다 반드시 크지는 않음.
“LLM에 서비스를 다른 언어로 다시 쓰게 했더니 빨라졌다”는 사례가 많은 건, 오히려 프로그램 설계를 고민하고 싶지 않아서라고 봄. 공정한 비교인 경우는 드묾.
프로그래밍에서 정말 어려운 부분은 설계이고, 문법에 맞춰 코드를 적는 부분은 상대적으로 덜 어려운 셈임.
아키텍처와 설계가 핵심임. 프로세스, 데이터 흐름, 패턴은 특정 기술보다 훨씬 중요함.
언어를 빠른 언어와 느린 접착용 언어로만 나누기보다, Java 같은 별도 범주도 봐야 함. Java에서는 라이브러리와 네트워킹 프레임워크까지 순수 Java로 작성하는 것이 일반적이며, 하드웨어나 플랫폼에 의존하는 일부만 네이티브 코드로 처리함. 성능이 중요한 소프트웨어도 Java나 다른 JVM 언어로 작성함.
제네릭, 애너테이션, 리플렉션, 적당한 동적 기능을 제공하는 편리한 고수준 언어라 프레임워크와 추상화를 만들기 좋음. 만능은 아니지만 업무용 소프트웨어와 서버 서비스에 잘 맞음.
내가 잘 아는 거대한 기업용 독점 소프트웨어도 WebSphere 노드의 수직 확장만으로 잘 버팀. 정작 모두를 괴롭히는 건 무거운 OLAP 작업에 OLTP를 심하게 오용하고 스키마도 부실하게 만든 관계형 DB임.
핵심을 놓치고 있음. 느린 언어가 아니라 느린 구현이 존재함. 구현은 시간이 지나면서 더 빠른 것으로 교체할 수 있음.
그렇게 보기는 어려움. CPython의 인터프리터와 표준 라이브러리가 개선되거나 Jython, IronPython, PyPy로 바꾸면 같은 언어 안에서도 성능 특성이 크게 달라질 수 있음. 하지만 언어의 의미론이 만드는 한계까지 구현만으로 없앨 수는 없음.
느슨한 타이핑이나 동적 타이핑은 구현에 제약을 주고, 단형화할 수 없는 동적 디스패치에는 피할 수 없는 비용이 생김. 메모리 할당을 추상화하면 개발자가 쓸 수 있는 최적화 수단도 줄어듦.
동적 언어에 정적 타입을 필수로 넣거나 GC 언어를 수동 메모리 관리로 바꾸는 것은 하위 호환성을 깨는 대규모 버전 변경을 넘어, 같은 언어라고 부르기 어려울 정도의 변화임. 빠른 언어로 확장을 작성하는 것도 기존 언어의 의미론이 목표 달성에 부족하다고 인정하고 그 밖으로 나가는 행위임.
따라서 느린 언어가 없다는 데 동의하지 않음. 모든 언어는 핵심 의미론과 제공하려는 추상화 때문에 이상적인 구현에서도 달성 가능한 성능에 제약을 가함.