Featured image of post [Compiler 03] 빌드 병렬화: ccache, distcc, sccache

[Compiler 03] 빌드 병렬화: ccache, distcc, sccache

ccache·sccache로 재컴파일 감소, distcc 분산 빌드, -j와 메모리·캐시 균형, CI에서의 캐시 아티팩트·캐시 키·분산 전략을 다룹니다. LTO·PGO와 캐시 키 관계와 언제 ccache/distcc를 쓸지 판단 기준을 제시합니다.

빌드 시간을 줄이려면 캐시와 병렬·분산이 필수입니다. 이 챕터에서는 ccache, sccache, distcc와 CI 전략을 다룹니다.

ccache / sccache로 재컴파일 감소

“Ccache is a compiler cache. It speeds up recompilation by caching previous compilations and detecting when the same compilation is being done again.” — ccache 공식 문서

ccache는 컴파일러(gcc, g++, clang 등)를 감싸서, 동일한 소스·플래그로 컴파일할 때 이전 결과를 재사용합니다. 캐시 hit이 나면 컴파일을 건너뛰고 이전 오브젝트를 복사하므로, 변경이 적은 TU는 거의 즉시 끝나고 전체 빌드 시간이 크게 줄어듭니다. 재빌드·CI에서 코드가 조금만 바뀌었을 때 효과가 큽니다.

sccache는 비슷한 개념으로, 캐시를 로컬뿐 아니라 S3, GCS 등 원격 저장소에 두어 여러 머신·CI 노드가 공유할 수 있게 합니다. 분산 팀이나 CI 팜에서 “다른 브랜치/다른 머신이 이미 같은 TU를 컴파일해 두었으면 그걸 쓰자"라는 전략에 적합합니다. ccache와 마찬가지로 캐시 키에는 소스 해시·컴파일러·플래그가 포함되어야 하므로, LTO·PGO 등 플래그가 다른 빌드와는 캐시가 공유되지 않습니다.

ccache는 컴파일러 앞에 끼워 넣어 사용합니다. CMake에서는 CMAKE_CXX_COMPILER_LAUNCHER로 모든 컴파일 호출을 ccache로 감쌀 수 있습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# ccache 설치 후 캐시 크기·경로 설정
ccache --max-size=10G
export CCACHE_DIR=$HOME/.ccache

# CMake 빌드에 ccache를 런처로 연결
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \
      -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
cmake --build build -j$(nproc)

# sccache는 원격 백엔드(S3 등)를 환경 변수로 지정
export SCCACHE_BUCKET=my-build-cache SCCACHE_REGION=ap-northeast-2
cmake -S . -B build -DCMAKE_CXX_COMPILER_LAUNCHER=sccache

캐시 효과는 ccache -s 통계로 확인합니다. hit률이 낮으면 플래그가 자주 바뀌거나 클린 빌드가 많은 것이므로, 캐시 키 구성을 점검합니다.

1
2
3
4
5
6
$ ccache -s
cache hit (direct)                  1842
cache hit (preprocessed)             137
cache miss                           421
cache hit rate                     82.45 %
cache size                          3.1 GB / 10.0 GB

distcc 등 분산 빌드

distcc는 컴파일 작업을 여러 머신에 나눠 보냅니다. 한 머신이 make -j를 돌리면, 로컬뿐 아니라 distcc로 연결된 원격 노드들도 컴파일 작업을 받아 수행하고, 생성된 오브젝트를 돌려줍니다. 단일 머신의 코어 수보다 총 컴파일 노드 수가 많으면 이론상 빌드 시간을 더 줄일 수 있습니다. 네트워크 지연·공유 스토리지(소스/인클루드 경로) 설정이 필요하고, Windows 지원은 제한적일 수 있습니다. Icecream 등 대안도 있으며, 개념은 비슷합니다.

distcc는 참여 노드를 환경 변수로 지정하고, ccache와 함께 쓰려면 CCACHE_PREFIX=distcc로 캐시 미스만 분산에 보냅니다. 이렇게 하면 캐시 hit는 로컬에서 즉시 끝나고, 실제 컴파일이 필요한 작업만 원격으로 나갑니다.

1
2
3
4
# 분산 노드 목록 지정 후, 노드 총합에 맞춰 -j를 크게 설정
export DISTCC_HOSTS="localhost node1 node2 node3"
export CCACHE_PREFIX=distcc        # 캐시 미스만 distcc로 분산
cmake --build build -j40           # 노드 총 코어 수에 맞춰 크게

분산 빌드는 캐시가 잘 안 먹는 클린 빌드나 대규모 프로젝트에서 유리합니다. 캐시 hit률이 높은 환경에서는 ccache/sccache만으로도 충분한 경우가 많습니다.

병렬 jobs 수(-j)와 메모리·캐시

make -j N, ninja -j N 등으로 동시에 돌릴 컴파일 작업 수를 정합니다. N을 코어 수에 맞추거나 그보다 조금 크게 두면 CPU를 잘 쓰지만, 메모리는 작업당 1–2GB 넘게 쓸 수 있어, N이 너무 크면 메모리 부족(OOM)으로 빌드가 실패할 수 있습니다. 캐시도 여러 작업이 동시에 돌면 공유 캐시를 나눠 쓰므로, 지나치게 큰 -j는 오히려 개별 컴파일이 느려질 수 있습니다. 머신 메모리와 코어 수를 보고 N을 조정하고(예: 코어 수와 같거나 코어 수의 1.5배), CI에서는 해당 러너의 리소스에 맞춰 -j를 고정하는 것이 좋습니다.

아래 표는 메모리·코어에 따른 -j 선택의 예시 기준입니다. 작업당 메모리는 코드(특히 무거운 템플릿·LTO)와 컴파일러에 따라 1–2GB를 넘기도 하므로, 실제로는 빌드 중 메모리 사용을 관찰해 조정합니다.

머신코어RAM권장 -j(예시)위험
로컬 노트북816GB6–8큰 -j 시 OOM
CI 러너48GB3–4LTO 빌드에서 OOM 주의
빌드 서버32128GB32–48캐시 경합

CI에서의 캐시·분산 전략

CI에서 빌드 시간을 줄이는 핵심은 상태를 다음 실행으로 넘기는 것입니다. 로컬과 달리 CI 러너는 매번 깨끗한 환경에서 시작하므로, 캐시를 아티팩트로 저장·복원하지 않으면 매 빌드가 풀 빌드가 됩니다. 그래서 보통 캐시(상태 재사용)를 먼저 적용하고, 그것만으로 부족한 대규모 클린 빌드에서 분산을 추가합니다.

  • 캐시: CI 러너에서 ccache 또는 sccache 디렉터리를 캐시 아티팩트로 저장하고, 다음 파이프라인에서 복원합니다. 캐시 키에는 커밋 해시·빌드 타입(Release/Debug)·컴파일러 버전을 넣어, 변경이 없으면 이전 캐시를 그대로 씁니다. 캐시 크기 제한이 있으면 오래된 항목부터 지워지므로, 중요한 브랜치만 오래 유지하는 정책을 둘 수 있습니다.
  • 분산: CI에서 distcc를 쓰려면 여러 러너를 “컴파일 노드"로 두고, 오케스트레이션과 네트워크 설정이 필요합니다. 대신 빌드 매트릭스(여러 컴파일러·플랫폼을 병렬로 빌드)로 동시에 여러 구성을 돌리고, 각 구성은 단일 러너에서 ccache/sccache로 최대한 캐시를 활용하는 방식이 더 단순한 경우가 많습니다.
  • 점진적 빌드: PR에서는 변경된 파일만 빌드하거나, 메인 브랜치 빌드 산출물을 기반으로 증분 빌드하는 방식으로 CI 시간을 줄일 수 있습니다. 이때 캐시와 증분 빌드가 잘 맞물리도록 설계하면 됩니다.

실전 시나리오: CI에서 캐시 키 설계

캐시 키에는 소스 해시(또는 커밋)·컴파일러 종류·버전·빌드 타입(Release/Debug)·주요 플래그(-O2, -flto 등)를 포함해야 합니다. LTO·PGO를 켜면 오브젝트 내용이 달라지므로, 이들을 켠 빌드와 끈 빌드는 캐시를 공유하지 않도록 키를 구분합니다. sccache를 쓰면 원격 저장소(S3 등)에 캐시를 두어 여러 CI 러너가 공유할 수 있습니다.

GitHub Actions에서는 ccache 디렉터리를 캐시 아티팩트로 저장·복원하고, 캐시 키에 컴파일러·빌드 타입·OS를 포함해 잘못된 재사용을 막습니다. restore-keys로 부분 일치 폴백을 두면 키가 약간 달라도 이전 캐시를 기반으로 시작할 수 있습니다.

1
2
3
4
5
6
7
8
- name: Restore ccache
  uses: actions/cache@v4
  with:
    path: ~/.ccache
    # 컴파일러·빌드타입·OS를 키에 포함 (플래그가 다르면 키 분리)
    key: ccache-${{ runner.os }}-gcc13-Release-${{ github.sha }}
    restore-keys: |
      ccache-${{ runner.os }}-gcc13-Release-

한눈에 보기: 도구별 역할

도구역할적합한 상황
ccache로컬 컴파일 결과 캐시재빌드·소량 변경
sccache원격 공유 캐시(S3 등)CI·분산 팀
distcc컴파일 작업 분산클린 빌드·대규모

판단 기준: 언제 무엇을 쓸지

상황권장비권장
로컬·CI 재빌드ccache 또는 sccache캐시 없이 매번 풀 빌드
CI 캐시캐시 키에 소스·컴파일러·플래그 포함LTO 등 플래그 누락 시 오염
-j 수메모리·코어 고려(코어 수–1.5배)과다 -j로 OOM

자주 하는 실수

  • 캐시 키에 플래그 누락: LTO·PGO·-O2/-O3 등 플래그가 바뀌면 산출물이 달라지므로, 캐시 키에 이들을 넣지 않으면 잘못된 오브젝트가 재사용되어 빌드가 깨지거나 이상 동작할 수 있다. 소스 해시·컴파일러·버전·주요 플래그를 모두 캐시 키에 포함한다.
  • -j 과다 설정: 메모리가 부족한 머신에서 -j를 코어 수보다 훨씬 크게 두면 OOM으로 빌드가 실패한다. 작업당 1–2GB를 쓰는 경우가 있으므로, 메모리와 코어 수를 보고 -j를 조정한다. CI 러너 리소스에 맞춰 -j를 고정해 두는 것이 안전하다.

학습 성과 목표

  • ccache·sccache·distcc의 역할과 차이를 설명할 수 있다.
  • CI에서 캐시·분산 전략을 설계하고, 캐시 키와 -j 설정을 적용할 수 있다.
  • LTO·PGO와 캐시 키의 관계를 설명할 수 있다.

비판적 시각: 한계와 트레이드오프

ccache/sccache는 동일한 소스·플래그일 때만 hit이 난다. 플래그를 자주 바꾸거나 클린 빌드가 많으면 캐시 이득이 작다. distcc는 네트워크·스토리지 설정이 필요하고, -j를 너무 크게 잡으면 OOM으로 실패할 수 있다. “캐시와 병렬만 켜면 빌드가 빨라진다"가 아니라, 자신의 CI·로컬 패턴에 맞춰 캐시 키와 -j를 설계하는 것이 중요하다.

핵심 요약

항목요약
ccache/sccache동일 소스·플래그 재사용으로 빌드 시간 감소
distcc컴파일 작업 분산; 클린·대규모에 유리
CI캐시 아티팩트·캐시 키·-j 설정

다음 장에서는

정적 분석기(Clang Static Analyzer, GCC -fanalyzer)와 성능 관련 경고, CI 연계를 다룹니다.

Static Analyzer (챕터 13)