C++ 컴파일 시간 최적화 | 모듈·PCH·ccache·분산 빌드 [#51-5]

C++ 컴파일 시간 최적화 | 모듈·PCH·ccache·분산 빌드 [#51-5]

이 글의 핵심

빌드 속도 향상: C++20 모듈, 미리 컴파일된 헤더, ccache, distcc, 병렬 빌드. 10만 줄 이상의 C++ 프로젝트에서 이런 경험을 해보셨나요? | 상황 | 증상 | 영향 | |------|------|------|.

들어가며: “빌드가 30분 걸려요. 매번 커밋할 때마다 기다리기 힘들어요”

문제 시나리오: 대규모 C++ 프로젝트의 빌드 지옥

10만 줄 이상의 C++ 프로젝트에서 이런 경험을 해보셨나요?

상황증상영향
헤더 하나 수정전체 리빌드 20분+작은 수정에도 긴 대기
CI 파이프라인빌드 타임아웃, 병목배포 지연, 비용 증가
팀원 10명 동시 빌드빌드 서버 과부하개발 속도 저하
<iostream> 포함거의 모든 TU가 재컴파일불필요한 의존성 폭발

핵심 원인:

  • 헤더 의존성 폭발: #include가 전이적으로 퍼져 한 헤더 수정 시 수백 개 TU 재컴파일
  • 템플릿 인스턴화: 매 TU마다 동일한 템플릿이 반복 인스턴스화
  • 순차 빌드: 단일 코어만 활용
  • 캐시 미사용: 동일 코드를 매번 처음부터 컴파일

이 글의 해결책:

  1. C++20 모듈: 헤더 대신 모듈로 의존성 격리
  2. PCH(미리 컴파일된 헤더): 공통 헤더를 한 번만 컴파일
  3. ccache: 컴파일 결과 캐시로 재빌드 가속
  4. distcc: 분산 컴파일로 여러 머신 활용
  5. 병렬 빌드: -j로 멀티코어 활용

요구 환경: C++17 이상 (모듈은 C++20), CMake 3.16+, GCC 11+ 또는 Clang 14+, ccache, distcc(선택)

이 글을 읽으면:

  • 빌드 시간 병목의 원인을 파악할 수 있습니다.
  • 모듈·PCH·ccache·분산 빌드를 실전에서 적용할 수 있습니다.
  • CI/CD 파이프라인을 최적화할 수 있습니다.

목차

  1. 빌드 시간 병목 분석
  2. C++20 모듈로 의존성 격리
  3. PCH(미리 컴파일된 헤더)
  4. ccache로 재빌드 가속
  5. 병렬 빌드와 CMake 설정
  6. distcc 분산 컴파일
  7. 자주 하는 실수와 해결법
  8. 성능 최적화 팁
  9. 프로덕션 패턴
  10. 실전 예제: 종합 적용

1. 빌드 시간 병목 분석

빌드 파이프라인 이해

flowchart LR
    subgraph Source["소스 단계"]
        A[.cpp 파일]
        B[#include 처리]
    end

    subgraph Compile["컴파일 단계"]
        C[전처리]
        D[파싱]
        E[최적화]
        F[코드 생성]
    end

    subgraph Link["링크 단계"]
        G[오브젝트 병합]
        H[실행 파일]
    end

    A --> B --> C --> D --> E --> F --> G --> H

병목 지점:

  • 전처리·파싱: 헤더가 많을수록 급격히 증가 (대부분의 시간)
  • 최적화: -O2/-O3 사용 시 증가
  • 링크: 대형 바이너리에서 증가

헤더 의존성 폭발 시각화

flowchart TB
    subgraph Before["#include 체인 (main.cpp)"]
        M[main.cpp]
        H1[common.h]
        H2[utils.h]
        H3[config.h]
        H4[iostream]
        H5[string]
        H6[vector]
        H7[algorithm]
        M --> H1 --> H2 --> H3
        H1 --> H4 --> H5 --> H6 --> H7
    end

    subgraph Impact["config.h 수정 시"]
        I[재컴파일 대상]
        I1[main.cpp]
        I2[foo.cpp]
        I3[bar.cpp]
        I4[... 50+ TU]
    end

핵심: config.h 하나 수정 → 이를 직접·간접 포함하는 모든 TU 재컴파일.

빌드 시간 측정

# 시간 측정 (GNU time)
/usr/bin/time -v make -j8 2>&1 | grep -E "Elapsed|Maximum resident"

# CMake + Ninja 상세 타이밍
cmake --build build -- -j8 -v 2>&1 | tee build.log
# 컴파일만 측정 (링크 제외)
# GCC: -ftime-report
g++ -c -ftime-report -O2 main.cpp 2>&1 | tail -20

2. C++20 모듈로 의존성 격리

모듈 vs 헤더

항목#include 헤더C++20 모듈
파싱 횟수TU마다 반복한 번만 (BMI 캐시)
전이적 노출전체 노출export된 것만
템플릿 인스턴스TU마다 반복한 번만
빌드 순서순서 무관모듈 의존성 순서 필요

모듈 기본 예제

math.mpp (모듈 구현):

// math.mpp - C++20 모듈
module;

#include <cmath>  // 모듈 선언 전에만 전통적 #include

export module math;

export double sqrt_safe(double x) {
    return x >= 0 ? std::sqrt(x) : 0.0;
}

export template<typename T>
T clamp(T v, T lo, T hi) {
    return v < lo ? lo : (v > hi ? hi : v);
}

main.cpp (모듈 사용):

import math;  // #include 대신 import

#include <iostream>

int main() {
    std::cout << sqrt_safe(2.0) << '\n';
    std::cout << clamp(42, 0, 100) << '\n';
    return 0;
}

CMake에서 모듈 빌드

# CMakeLists.txt - C++20 모듈 지원
cmake_minimum_required(VERSION 3.28)
project(MyApp LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 모듈 파일을 먼저 컴파일 (의존성 순서)
add_library(math MODULE math.mpp)
target_compile_features(math PUBLIC cxx_std_20)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE math)

주의: GCC 14+, Clang 18+, MSVC 2022 17.5+에서 모듈 지원이 안정화됨. 이전 버전은 실험적.

모듈 전환 전략

flowchart LR
    A[기존 헤더] --> B{의존성 적음?}
    B -->|Yes| C[모듈로 전환]
    B -->|No| D[PCH 우선 적용]
    C --> E[점진적 확대]
    D --> E

권장 순서: 의존성이 적은 유틸리티·공통 타입부터 모듈로 전환.


3. PCH(미리 컴파일된 헤더)

PCH란?

자주 쓰는 헤더들을 한 번만 컴파일해 .gch(GCC) 또는 .pch(Clang/MSVC)로 저장하고, 각 TU에서 이 바이너리를 재사용합니다.

sequenceDiagram
    participant TU as Translation Unit
    participant PCH as PCH 캐시
    participant Compiler as 컴파일러

    Note over TU,Compiler: PCH 없을 때
    TU->>Compiler: stdafx.h 파싱 (매 TU마다)
    Compiler->>Compiler: 반복 작업

    Note over TU,Compiler: PCH 있을 때
    PCH->>Compiler: stdafx.h.gch 로드 (한 번만 생성)
    TU->>Compiler: 나머지 코드만 파싱

PCH 헤더 작성

pch.h (공통 포함 헤더):

// pch.h - 미리 컴파일할 헤더
#ifndef PCH_H
#define PCH_H

// 자주 쓰는 표준 라이브러리 (변경 거의 없음)
#include <iostream>
#include <string>
#include <vector>
#include <memory>
#include <algorithm>
#include <chrono>

// 프로젝트 공통 헤더
#include "config.h"
#include "types.h"

#endif

GCC에서 PCH 생성 및 사용

# 1. PCH 생성
g++ -x c++-header -std=c++20 -O2 -o pch.h.gch pch.h

# 2. 소스 컴파일 시 PCH 사용
g++ -std=c++20 -O2 -include pch.h -c main.cpp -o main.o

CMake에서 PCH 자동화

# CMakeLists.txt - PCH 지원
cmake_minimum_required(VERSION 3.16)
project(MyApp LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)

# PCH 타겟
add_library(pch_header INTERFACE)
target_precompile_headers(pch_header INTERFACE
    <iostream>
    <string>
    <vector>
    <memory>
    "config.h"
)

add_executable(app main.cpp foo.cpp bar.cpp)
target_link_libraries(app PRIVATE pch_header)
target_precompile_headers(app REUSE_FROM pch_header)

target_precompile_headers (CMake 3.16+): PCH를 자동 생성·연결합니다.

PCH 적용 시 주의사항

  • 컴파일 옵션 일치: PCH 생성 시 -O2, -DDEBUG 등이 TU와 완전히 동일해야 함
  • 헤더 순서: PCH 헤더는 파일 최상단에 포함 (그 전에 다른 #include 금지)
  • 변경 빈도: 자주 바뀌는 헤더는 PCH에 넣지 말 것 (PCH 재생성 비용)

4. ccache로 재빌드 가속

ccache 동작 원리

flowchart TB
    subgraph Input["입력"]
        S[소스 + 컴파일 옵션]
    end

    subgraph CCache["ccache"]
        H[해시 계산]
        L{캐시<br/>존재?}
        R[캐시에서 복원]
        C[실제 컴파일]
        S2[캐시 저장]
    end

    S --> H --> L
    L -->|Hit| R
    L -->|Miss| C --> S2
    R --> O[오브젝트 출력]
    C --> O

동작: 소스+옵션 해시로 캐시 조회 → Hit이면 컴파일 생략, Miss면 컴파일 후 저장.

ccache 설치 및 설정

# Ubuntu/Debian
sudo apt install ccache

# macOS
brew install ccache

# PATH에 ccache 우선 배치
export PATH="/usr/lib/ccache:$PATH"
# 또는
export CC="ccache gcc"
export CXX="ccache g++"

~/.ccache/ccache.conf (또는 환경변수):

# 캐시 크기 (기본 5GB)
max_size = 10G

# 압축 (디스크 절약, 약간의 CPU 비용)
compression = true
compression_level = 6

# 디버그 정보 무시 (다른 경로에서 빌드해도 Hit)
hash_dir = false

# 로그 (디버깅용)
log_file = /tmp/ccache.log

CMake + ccache

# CMakeLists.txt
find_program(CCACHE_PROGRAM ccache)
if(CCACHE_PROGRAM)
    set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
    set(CMAKE_C_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
endif()
# 또는 빌드 시
cmake -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -B build
cmake --build build -j8

ccache 통계 확인

ccache -s

# 출력 예:
# cache hit (direct)                 1234
# cache hit (preprocessed)             56
# cache miss                          89
# cache hit rate                      93.54 %

5. 병렬 빌드와 CMake 설정

병렬 빌드 기본

# make: -j N (N = 병렬 작업 수)
make -j$(nproc)

# Ninja (CMake 기본 생성기)
ninja -j8

# CMake
cmake --build build -- -j8

CPU 코어 수에 맞춰 자동 설정

# CMakeLists.txt
include(ProcessorCount)
ProcessorCount(N)
if(NOT N EQUAL 0)
    set(CMAKE_BUILD_PARALLEL_LEVEL ${N})
endif()
# 환경변수로 Ninja에 전달
export CMAKE_BUILD_PARALLEL_LEVEL=$(nproc)
cmake --build build

Unity Build (Jumbo Build)

여러 .cpp를 하나로 합쳐 컴파일 횟수를 줄입니다. 링크 시간은 늘어날 수 있음.

# CMakeLists.txt
set(CMAKE_UNITY_BUILD ON)
add_executable(app main.cpp foo.cpp bar.cpp baz.cpp)
# → 내부적으로 하나의 unity_*.cpp로 합쳐서 컴파일

주의: static 변수 이름 충돌, 매크로 전역 오염 등에 유의.


6. distcc 분산 컴파일

distcc 개요

여러 머신에 컴파일 작업을 분산합니다. 로컬 전처리 + 원격 코드 생성.

flowchart TB
    subgraph Client["클라이언트 머신"]
        S[소스]
        P[전처리]
        D[distcc]
    end

    subgraph Workers["워커 머신들"]
        W1[Worker 1]
        W2[Worker 2]
        W3[Worker 3]
    end

    S --> P --> D
    D --> W1
    D --> W2
    D --> W3
    W1 --> O[오브젝트]
    W2 --> O
    W3 --> O

distcc 설정

서버 (워커):

# distccd 실행 (포트 3632)
distccd --daemon --allow 192.168.1.0/24 --listen 0.0.0.0

클라이언트:

# 사용할 호스트 목록
export DISTCC_HOSTS="localhost 192.168.1.10 192.168.1.11"
export CC="distcc gcc"
export CXX="distcc g++"

# CMake
cmake -DCMAKE_C_COMPILER_LAUNCHER="distcc" \
      -DCMAKE_CXX_COMPILER_LAUNCHER="distcc" \
      -B build
cmake --build build -j16

ccache + distcc 조합

export CC="ccache distcc gcc"
export CXX="ccache distcc g++"
# ccache → distcc → gcc 순서로 래핑

7. 자주 하는 실수와 해결법

문제 1: PCH 사용 시 “file not found” 또는 “invalid precompiled header”

원인: PCH 생성 옵션과 TU 컴파일 옵션이 다름.

# ❌ 잘못된 예: PCH는 -O0, TU는 -O2
g++ -x c++-header -O0 -o pch.h.gch pch.h
g++ -O2 -include pch.h -c main.cpp  # 에러!

해결:

# ✅ 옵션 일치
g++ -x c++-header -std=c++20 -O2 -DNDEBUG -o pch.h.gch pch.h
g++ -std=c++20 -O2 -DNDEBUG -include pch.h -c main.cpp

문제 2: PCH 헤더 앞에 다른 #include가 있음

원인: PCH는 반드시 첫 번째 포함되어야 함.

// ❌ 잘못된 예
#include "other.h"   // PCH보다 먼저!
#include "pch.h"

// ✅ 올바른 예
#include "pch.h"
#include "other.h"

문제 3: ccache Hit이 안 됨

원인: __FILE__, __DATE__, __TIME__ 등이 소스에 포함되어 매번 다른 입력으로 인식.

// ❌ ccache 무효화
std::cout << "Built at " << __DATE__ << " " << __TIME__;

해결: 빌드 타임스탬프는 링크 단계에서 주입하거나, 별도 생성 파일로 분리.

// ✅ 버전 정보를 별도 .cpp에서
// version.cpp - 빌드 스크립트가 생성
const char* build_time = "2026-04-05 12:00:00";

문제 4: 모듈 빌드 순서 오류

원인: 모듈 B가 모듈 A를 import하는데, A가 아직 빌드되지 않음.

# ✅ 의존성 순서 보장
add_library(mod_a MODULE a.mpp)
add_library(mod_b MODULE b.mpp)
target_link_libraries(mod_b PRIVATE mod_a)  # mod_a 먼저 빌드

문제 5: distcc “compiler not found”

원인: 워커에 동일한 컴파일러·버전이 없음.

해결: 워커에 같은 툴체인 설치, DISTCC_VERBOSE=1로 경로 확인.

# 워커에서
which g++
# 클라이언트와 동일한 경로/버전이어야 함

문제 6: Unity Build에서 ODR 위반

원인: 여러 cpp를 합치면서 static 변수나 익명 네임스페이스가 중복.

// ❌ foo.cpp
static int counter = 0;

// ❌ bar.cpp (Unity로 합쳐지면)
static int counter = 0;  // ODR 위반!

해결: static 대신 익명 네임스페이스, 또는 Unity Build 비활성화.

// ✅
namespace {
int counter = 0;
}

8. 성능 최적화 팁

팁 1: 헤더 정리

// ❌ 불필요한 포함
#include <iostream>  // cout만 쓰는데
#include <vector>   // 사용 안 함

// ✅ 필요한 것만
#include <ostream>  // std::ostream
#include "my_types.h"

forward declaration 활용:

// ✅ 헤더에서
class Bar;  // forward declaration
void use_bar(Bar& b);  // Bar 정의 불필요

// .cpp에서
#include "bar.h"
void use_bar(Bar& b) { /* ... */ }

팁 2: 템플릿 분리

// ❌ 헤더에 모든 구현
template<typename T>
void process(T x) {
    // 긴 구현...
}

// ✅ 선언만 헤더, 구현은 .tpp
// util.hpp
template<typename T> void process(T x);

// util.tpp
#include "util.hpp"
template<typename T>
void process(T x) { /* ... */ }

필요한 타입만 .cpp에서 명시적 인스턴스화:

// util_instances.cpp
#include "util.tpp"
template void process<int>(int);
template void process<double>(double);

팁 3: 디버그 빌드 최적화

# Debug에서도 PCH 사용
set(CMAKE_PCH_INSTANTIATE_TEMPLATES OFF)
target_precompile_headers(app PRIVATE pch.h)

팁 4: 증분 빌드 최대화

  • 빌드 디렉토리 고정: build/ 한 곳에 유지해 ccache Hit 극대화
  • 경로 정규화: hash_dir = false로 절대 경로 차이 무시
  • 타임스탬프 분리: 소스는 Git, 빌드 아티팩트는 별도 저장

팁 5: CI에서 ccache 활용

# GitHub Actions 예시
- uses: actions/cache@v4
  with:
    path: ~/.ccache
    key: ccache-${{ runner.os }}-${{ hashFiles('**/CMakeLists.txt', '**/*.cpp') }}
    restore-keys: ccache-${{ runner.os }}-

- run: |
    ccache -s  # 캐시 통계
    cmake -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -B build
    cmake --build build -j4

9. 프로덕션 패턴

패턴 1: 계층별 빌드 전략

flowchart TB
    subgraph Layer1["1층: 안정 라이브러리"]
        L1[표준 라이브러리]
        L2[서드파티]
    end

    subgraph Layer2["2층: 프로젝트 코어"]
        M1[모듈/PCH]
    end

    subgraph Layer3["3층: 애플리케이션"]
        A1[앱 코드]
    end

    L1 --> L2 --> M1 --> A1
  • 1층: PCH 또는 모듈로 한 번만 빌드
  • 2층: 변경 적은 코어를 모듈화
  • 3층: 자주 바뀌는 앱 코드는 ccache·병렬에 의존

패턴 2: 빌드 프로파일

# CMakePresets.cmake 또는 프로파일
set(CMAKE_BUILD_TYPE "Release")
set(USE_PCH ON)
set(USE_CCACHE ON)
set(CMAKE_CXX_COMPILER_LAUNCHER "ccache")

패턴 3: 모니터링

# 빌드 시간 추적
echo "$(date -Iseconds)" >> build_times.log
/usr/bin/time -a -o build_times.log -f "Elapsed: %E\n" cmake --build build -j8

패턴 4: 점진적 모듈 전환

  1. 1단계: 새 코드만 모듈로 작성
  2. 2단계: 의존성 적은 유틸을 모듈로 전환
  3. 3단계: 핵심 라이브러리 모듈화
  4. 4단계: 레거시 헤더는 PCH로 유지

10. 실전 예제: 종합 적용

프로젝트 구조

my_project/
├── CMakeLists.txt
├── src/
│   ├── pch.h
│   ├── main.cpp
│   ├── foo.cpp
│   └── bar.cpp
└── build/

CMakeLists.txt (종합)

cmake_minimum_required(VERSION 3.20)
project(MyProject LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)

# ccache
find_program(CCACHE_PROGRAM ccache)
if(CCACHE_PROGRAM)
    set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
endif()

# PCH
add_library(pch_header INTERFACE)
target_precompile_headers(pch_header INTERFACE
    <iostream>
    <string>
    <vector>
    "config.h"
)

# 병렬 빌드
include(ProcessorCount)
ProcessorCount(N)
if(NOT N EQUAL 0)
    set(CMAKE_BUILD_PARALLEL_LEVEL ${N})
endif()

# 실행 파일
add_executable(app
    src/main.cpp
    src/foo.cpp
    src/bar.cpp
)
target_link_libraries(app PRIVATE pch_header)
target_precompile_headers(app REUSE_FROM pch_header)

빌드 스크립트

#!/bin/bash
# build.sh
set -e

# ccache 활성화
export CCACHE_DIR="${CCACHE_DIR:-$HOME/.ccache}"
mkdir -p "$CCACHE_DIR"

# distcc (선택)
# export DISTCC_HOSTS="localhost 192.168.1.10"

cmake -B build \
    -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
    -DCMAKE_BUILD_TYPE=Release

cmake --build build

echo "=== ccache 통계 ==="
ccache -s

성능 비교 (예시)

설정클린 빌드증분 빌드 (1파일 수정)
기본 (-j1)180초45초
-j835초12초
-j8 + ccache35초 (첫)2초
-j8 + ccache + PCH28초2초
-j16 + distcc18초3초

구현 체크리스트

프로덕션 적용 시 확인할 항목:

  • -j$(nproc) 또는 CMAKE_BUILD_PARALLEL_LEVEL 설정
  • ccache 설치 및 CMAKE_CXX_COMPILER_LAUNCHER=ccache 적용
  • PCH에 자주 쓰는 표준·프로젝트 헤더만 포함 (변경 잦은 헤더 제외)
  • PCH 생성 옵션과 TU 컴파일 옵션 일치 확인
  • CI에서 ccache 캐시 디렉토리 유지 (actions/cache 등)
  • distcc 사용 시 워커에 동일 컴파일러·버전 설치
  • 모듈 도입 시 CMake 3.28+ 및 GCC 14+/Clang 18+ 확인
  • 빌드 시간 로깅으로 개선 효과 측정

컴파일러별 지원 현황

기능GCC 14Clang 18MSVC 2022 17.5
C++20 모듈
PCH✅ .gch✅ .pch✅ .pch
#pragma once
target_precompile_headers

추가 문제 시나리오

시나리오 A: CI에서 매번 클린 빌드

  • 증상: PR마다 15분 이상 빌드, 캐시 없음
  • 해결: ccache 캐시를 아티팩트로 저장·복원, CMakeLists.txt·소스 해시를 캐시 키로 사용

시나리오 B: 팀원마다 빌드 시간이 다름

  • 증상: 일부는 5분, 일부는 20분
  • 원인: SSD vs HDD, ccache 미설정, -j 값 차이
  • 해결: 공통 build.sh·CMake 프리셋으로 환경 통일

시나리오 C: 헤더 하나 수정에 전체 리빌드

  • 증상: config.h 수정 시 50개 이상 TU 재컴파일
  • 해결: config를 모듈로 분리하거나, PCH에서 제외하고 필요한 cpp에서만 포함

정리

기법적용 시점효과난이도
병렬 빌드 (-j)항상2~8배쉬움
ccache항상증분 5~20배쉬움
PCH헤더 많은 프로젝트1.2~2배중간
모듈C++20, 새/리팩터 코드1.5~3배어려움
distcc다중 머신 환경2~4배중간
Unity BuildTU 수가 매우 많을 때1.5~2배주의

핵심 원칙:

  1. 측정 먼저: 병목 구간을 프로파일링
  2. 쉬운 것부터: -j, ccache → PCH → 모듈
  3. CI 연동: ccache 캐시 유지, 병렬 빌드
  4. 점진적 적용: 한 번에 다 바꾸지 말고 단계별 도입

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. 대규모 프로젝트 빌드 시간 단축, CI/CD 파이프라인 최적화, 개발 생산성 향상 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.

한 줄 요약: 모듈·PCH·ccache·분산 빌드를 조합하면 빌드 시간을 크게 줄일 수 있습니다.


관련 글

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3