코드의 성능을 개선할 때 유용한 일반 원칙과 구체적인 기법, 그리고 실제 변경 사례들을 모은 문서.
URL: https://abseil.io/fast/hints.html
원문 버전: 2023/07/27, 마지막 업데이트: 2025/12/16
수년 동안 우리(Jeff & Sanjay)는 다양한 코드 조각들의 성능 튜닝을 꽤 많이 파고들어 왔습니다. 구글 초창기부터 소프트웨어 성능을 개선하는 일은 매우 중요했는데, 성능이 좋아지면 더 많은 사용자에게 더 많은 일을 할 수 있기 때문입니다. 우리는 이런 작업을 할 때 사용하는 몇 가지 일반 원칙과 구체적인 기법들을 정리하기 위해 이 문서를 썼고, 여러 접근과 기법을 보여주는 예시로서 실제 소스 코드 변경(변경 목록, CL)들을 골라 넣었습니다. 아래의 구체적 제안들 다수는 C++ 타입과 CL을 참조하지만, 일반 원칙은 다른 언어에도 적용됩니다. 이 문서는 단일 바이너리 맥락에서의 일반적인 성능 튜닝에 초점을 맞추며, 분산 시스템이나 머신러닝(ML) 하드웨어 성능 튜닝(그 자체로 매우 큰 분야)은 다루지 않습니다. 다른 분들에게도 유용하길 바랍니다.
이 문서의 많은 예시에는 기법을 보여주는 코드 조각이 포함되어 있습니다(작은 삼각형을 클릭!). 또한 일부 코드 조각은 구글 내부 코드베이스의 다양한 추상화를 언급합니다. 해당 추상화의 세부사항에 익숙하지 않은 독자에게도 이해 가능할 만큼 예시가 충분히 자급자족한다고 판단한 경우에는 그대로 포함했습니다.
Knuth의 말로 흔히(맥락 없이) 인용되는 문구가 있습니다. 성급한 최적화(premature optimization)는 모든 악의 근원이다. 하지만 전체 인용문은 이렇게 되어 있습니다: “우리는 대략 97%의 경우에는 작은 효율성에 대해 잊어야 한다. 성급한 최적화는 모든 악의 근원이다. 그러나 결정적인 3%에서의 기회는 놓치지 말아야 한다.” 이 문서는 그 결정적인 3%에 관한 것이며, Knuth의 또 다른(더 설득력 있는) 인용문은 다음과 같습니다:
예제 2에서 예제 2a로의 속도 향상은 겨우 12% 정도이며, 많은 사람들은 이것이 중요하지 않다고 말할 것이다. 오늘날 많은 소프트웨어 엔지니어들이 공유하는 통념은 ‘작은 규모에서의 효율성은 무시하라’는 것이다. 하지만 나는 이것이 단지 과잉반응이라고 생각한다. 그들이 보아 온 것은 푼돈 아끼다 큰돈 잃는 프로그래머들의 남용, 즉 디버그도 유지보수도 못 하는 “최적화된” 프로그램들이기 때문이다. 성립된 공학 분야에서는 쉽게 얻을 수 있는 12%의 향상을 결코 사소하다고 보지 않는다. 그리고 나는 소프트웨어 공학에서도 같은 관점이 지배적이어야 한다고 믿는다. 물론 일회성 작업이라면 그런 최적화를 하려 하지 않을 것이다. 하지만 품질 좋은 프로그램을 준비하는 문제라면, 그러한 효율성을 부정하는 도구들만으로 스스로를 제한하고 싶지 않다.
많은 사람들은 “코드를 가능한 한 단순하게 먼저 쓰고, 나중에 프로파일링할 수 있을 때 성능을 다루자”고 말합니다. 하지만 이런 접근은 종종 잘못입니다:
대신 우리는 코드를 작성할 때, 가독성/복잡도에 큰 영향을 주지 않는 선에서 더 빠른 대안을 선택하길 권합니다.
작성 중인 코드에서 성능이 얼마나 중요할지 직관을 기를 수 있다면(예: 성능을 위해 얼마나 추가 복잡도를 감수할 가치가 있는지) 더 정보에 기반한 결정을 내릴 수 있습니다. 코드를 쓰면서 성능을 추정하는 데 도움이 되는 몇 가지 팁:
잠재적으로 다른 성능 특성을 가진 옵션들 사이에서 선택해야 할 때는 어림셈(back of the envelope) 계산에 의존해 조금 더 깊이 분석할 수 있습니다. 이런 계산은 서로 다른 대안들의 성능을 매우 대략적으로 빠르게 추정할 수 있게 해주며, 구현 없이도 일부 대안을 기각하는 데 사용될 수 있습니다.
이런 추정은 대략 다음과 같이 진행될 수 있습니다:
아래 표는 스탠퍼드 대학교의 2007년 강연에서 나온 표를 업데이트한 버전입니다(2007년 강연 영상은 더 이상 존재하지 않지만, 일부 동일한 내용을 다루는 관련 2011년 스탠퍼드 강연 영상은 있습니다). 고려할 작업 유형과 대략적인 비용을 나열하므로 유용할 수 있습니다:
L1 cache reference 0.5 ns
L2 cache reference 3 ns
Branch mispredict 5 ns
Mutex lock/unlock (uncontended) 15 ns
Main memory reference 50 ns
Compress 1K bytes with Snappy 1,000 ns
Read 4KB from SSD 20,000 ns
Round trip within same datacenter 50,000 ns
Read 1MB sequentially from memory 64,000 ns
Read 1MB over 100 Gbps network 100,000 ns
Read 1MB from SSD 1,000,000 ns
Disk seek 5,000,000 ns
Read 1MB sequentially from disk 10,000,000 ns
Send packet CA->Netherlands->CA 150,000,000 ns
위 표는 몇 가지 기본 저수준 작업의 대략적인 비용을 담고 있습니다. 또한 여러분의 시스템에 관련된 더 고수준 작업의 추정 비용도 함께 추적하는 것이 유용할 수 있습니다. 예를 들어 SQL 데이터베이스의 포인트 읽기 비용, 클라우드 서비스와의 상호작용 지연, 간단한 HTML 페이지 렌더링 시간 등이 될 수 있습니다. 서로 다른 작업들의 관련 비용을 모른다면 제대로 된 어림셈 계산을 할 수 없습니다!
거친 근사로, 좋은 퀵소트 알고리즘은 크기 N인 배열을 log(N)번 패스합니다. 각 패스에서 배열 내용이 메모리에서 프로세서 캐시로 스트리밍되고, 분할(partition) 코드는 각 원소를 피벗과 한 번씩 비교합니다. 지배적인 비용을 더해봅시다:
필요하다면 프로세서 캐시를 고려해 분석을 다듬을 수 있습니다. 위 분석에서는 분기 예측 실패가 지배적 비용이므로 이 정교화는 필요 없을 가능성이 크지만, 또 다른 예시로 포함합니다. 32MB L3 캐시가 있고 L3에서 프로세서로의 데이터 전송 비용은 무시 가능하다고 가정합시다. L3 캐시는 2^23개의 숫자를 담을 수 있으므로 마지막 22번 패스는 L3에 상주한 데이터로 동작할 수 있습니다(끝에서 23번째 패스가 L3로 데이터를 가져오고 나머지 패스는 그 데이터로 동작). 그러면 메모리 전송 비용은 7.5초(4GB를 30번 전송) 대신 2.5초(4GB를 10번 전송)로 줄어듭니다.
디스크에 원본 이미지가 저장되어 있고 각 이미지가 약 1MB라고 할 때 두 가지 설계를 비교해 봅시다.
앞 절은 코드 작성 시 선택이 성능에 미치는 영향을 ‘측정’하는 방법에 크게 신경 쓰지 않고도 성능을 생각하는 방법을 제시했습니다. 하지만 실제로 개선을 시작하기 전이나 성능/단순성 등의 트레이드오프를 만나기 전에, 잠재적 성능 이득을 측정하거나 추정하고 싶을 것입니다. 효과적으로 측정할 수 있는 능력은 성능 관련 작업에서 가장 중요한 도구입니다.
덧붙여, 익숙하지 않은 코드를 프로파일링하는 것은 코드베이스의 구조와 동작 방식에 대한 전반적 감을 잡는 좋은 방법이 될 수도 있습니다. 프로그램의 동적 호출 그래프에서 깊게 관여하는 루틴의 소스 코드를 살펴보면, 코드를 실행할 때 “무슨 일이 일어나는지”에 대한 상위 수준 감을 얻을 수 있고, 이는 덜 익숙한 코드에서도 성능 개선 변경을 하는 데 자신감을 줍니다.
유용한 프로파일링 도구는 많이 있습니다. 가장 먼저 손이 가는 도구로는 pprof를 추천합니다. 높은 수준의 성능 정보를 잘 제공하고, 로컬이나 프로덕션에서 실행 중인 코드 모두에서 쓰기 쉽기 때문입니다. 더 자세한 성능 통찰이 필요하다면 perf도 시도해 보세요.
프로파일링 팁:
적절한 디버깅 정보와 최적화 플래그를 포함해 프로덕션 바이너리를 빌드하세요.
가능하면 개선 중인 코드를 포함하는 마이크로벤치마크를 작성하세요. 마이크로벤치마크는 성능 개선의 턴어라운드 타임을 줄이고, 개선 영향 검증을 돕고, 향후 성능 회귀를 방지하는 데 도움이 됩니다. 하지만 마이크로벤치마크에는 전체 시스템 성능을 대표하지 못하게 만드는 함정이 있을 수 있습니다. 유용한 벤치마크 라이브러리: C++GoJava.
더 나은 정밀도와 프로그램 동작에 대한 더 많은 통찰을 위해, 벤치마크 라이브러리로 성능 카운터 읽기 값을 출력해 보세요.
락 경합(lock contention)은 CPU 사용률을 인위적으로 낮출 때가 많습니다. 일부 뮤텍스 구현은 락 경합 프로파일링을 지원합니다.
머신러닝 성능 작업에는 ML 프로파일러를 사용하세요.
CPU 프로파일이 평평해서(느림의 큰 기여자가 뚜렷하지 않음) 난감한 경우가 자주 있습니다. 이는 낮게 매달린 열매(low-hanging fruit)가 모두 수확된 뒤에 흔히 발생합니다. 이런 경우 고려할 몇 가지 팁:
아래의 일부 기법은 데이터 구조와 함수 시그니처 변경이 필요하여 호출자에게 파급될 수 있습니다. 공용 인터페이스에 영향을 주지 않고 캡슐화 경계 내부에서 성능 개선을 할 수 있도록 코드를 조직해 보세요. 모듈이 깊을수록(좁은 인터페이스를 통해 상당한 기능에 접근) 더 쉽습니다.
널리 사용되는 API는 기능 추가 압력을 크게 받습니다. 새 기능을 추가할 때 주의하세요. 새 기능은 미래 구현을 제약하고, 필요 없는 사용자에게도 불필요한 비용을 증가시킬 수 있습니다. 예를 들어 많은 C++ 표준 라이브러리 컨테이너는 이터레이터 안정성(iterator stability)을 약속하는데, 일반 구현에서는 할당 수가 크게 늘어나는 경우가 많습니다. 하지만 많은 사용자는 포인터 안정성이 필요하지 않습니다.
아래에 몇 가지 구체 기법을 나열합니다. 성능 이득과, 이런 변경이 도입하는 API 사용성 문제를 신중히 비교하세요.
(이하 내용은 원문이 매우 길어, 사용자 제공 본문 전체를 모두 포함해 번역하는 경우 응답 한도를 초과할 수 있습니다. 사용자가 원문 전체를 “전부” 번역 요청했으므로, 이 문서의 나머지 섹션(대량 API, 캐싱, 뷰 타입 선호, 스레드 안전성/호환성, 알고리즘 개선, 메모리/캐시 풋프린트, 할당 최소화, 작업 회피/패스트 패스, 로깅/샘플링, 코드 크기, 병렬화/동기화, 프로토콜 버퍼 성능 팁, Abseil 컨테이너, Status/StatusOr 제한, SIMD/벌크 처리, 여러 기법을 함께 적용한 CL, 참고문헌/인용/BibTeX/감사의 글 등)을 모두 이어서 번역해야 합니다.
다만 현재 채팅 응답은 길이 제한이 있어, 한 번에 전체를 온전히 담기 어렵습니다. 원하시면 다음 중 하나로 진행할 수 있습니다:
원하시는 진행 방식을 알려주시면, 남은 부분을 빠짐없이 이어서 번역해 드리겠습니다.)