Abseil ABSL_PREDICT_TRUE/FALSE — branch hint
한 줄 요약:
ABSL_PREDICT_TRUE/ABSL_PREDICT_FALSE는 컴파일러에게 분기 확률을 알려주는 힌트다. 현대 CPU의 분기 예측기는 보통 이를 무시해도 충분히 잘 동작하지만, 코드 레이아웃(hot path 응집)에 미치는 영향은 측정 가능한 만큼 남아 있다.
#어떤 문제를 푸는가
if-else를 만나면 컴파일러는 두 가지를 결정해야 한다.
- 명령어 순서 — 어느 분기를 fall-through로 둘 것인가.
- 코드 배치 — 어느 분기를 hot path에 가까이 둘 것인가.
런타임의 분기 예측기는 첫 번째 결정에 점점 덜 의존한다. 그러나 두 번째 결정 — code layout — 은 컴파일 시점에 굳어지고, 런타임이 어떻게 해줄 수 없다. ABSL_PREDICT_*는 layout을 위한 힌트다.
#정의
// absl/base/optimization.h에서 발췌#if ABSL_HAVE_BUILTIN(__builtin_expect) #define ABSL_PREDICT_FALSE(x) (__builtin_expect(false || (x), false)) #define ABSL_PREDICT_TRUE(x) (__builtin_expect(false || (x), true))#else #define ABSL_PREDICT_FALSE(x) (x) #define ABSL_PREDICT_TRUE(x) (x)#endif__builtin_expect는 GCC/Clang의 표준 builtin이다. MSVC는 같은 기능이 없어 매크로가 그냥 통과한다.
false ||로 시작하는 트릭은 인자를 bool로 강제 변환하기 위한 것이다. int 값을 직접 expect에 넣으면 0/1 이외의 값에 대해 동작이 미묘해진다.
#사용 패턴
가장 자주 보는 형태.
absl::Status Process(const Request& req) { if (ABSL_PREDICT_FALSE(req.IsInvalid())) { return absl::InvalidArgumentError("..."); } // hot path return DoActualWork(req);}에러 경로를 PREDICT_FALSE로 표시한다. 컴파일러는 hot path의 code layout을 cache-friendly하게 배치하고, 에러 경로는 cold section으로 옮긴다.
#컴파일러가 실제로 하는 일
clang으로 다음을 컴파일하면.
int Sum(int* data, int n) { if (ABSL_PREDICT_FALSE(n == 0)) { return 0; } int s = 0; for (int i = 0; i < n; ++i) s += data[i]; return s;}assembly 결과의 핵심은 두 가지다.
Sum: test %esi, %esi je .L_cold_path # rarely-taken jump xor %eax, %eax # hot path falls through ... (loop) retq.L_cold_path: xor %eax, %eax # cold section, placed at function end retqje는 거의 안 가는 .L_cold_path로 분기하고, hot path는 fall-through로 이어진다. cold section은 보통 함수 끝에 배치된다.
비교: PREDICT 힌트가 없을 때는 cold path가 fall-through일 수도 있다. 컴파일러의 PGO(profile-guided optimization)가 있으면 굳이 힌트가 없어도 같은 결정을 내릴 수 있다.
#ABSL_PREDICT_*의 진짜 효과
세 가지 효과가 있고, 중요도가 다르다.
#1. Code layout (가장 큼)
cold path가 별도 section으로 분리되면 instruction cache 효율이 올라간다. 큰 함수에서 의미 있다. 작은 함수에서는 효과가 거의 없다.
#2. 분기 예측 (작음)
현대 CPU는 한 번 본 분기를 거의 완벽하게 예측한다. PREDICT 힌트가 런타임 예측기에 직접 영향을 주지는 않는다. 다만 코드 레이아웃 결정이 간접적으로 분기 예측 패턴에 영향을 줄 수 있다.
#3. 인라인 결정 (간접)
PREDICT_FALSE로 표시된 경로는 컴파일러가 인라인을 덜 하려 한다. cold path 함수가 따로 호출되면 hot path의 코드 크기가 줄어든다.
#측정 — 어디까지 의미 있는가
작은 벤치마크로 본 차이.
// 시나리오: 1000000번 반복, error rate 0.1%for (int i = 0; i < 1000000; ++i) { if (rand() % 1000 == 0) { // 0.1% DoError(); } else { DoFast(); }}| 빌드 | 시간 |
|---|---|
| 힌트 없음, -O2 | 100% (baseline) |
PREDICT_FALSE(rand() % 1000 == 0), -O2 | 98~100% |
| PGO 적용 | 95~98% |
힌트의 효과는 크지 않다. PGO를 쓰면 힌트와 비슷하거나 더 잘한다. 큰 함수, 복잡한 분기 구조에서는 차이가 더 벌어지지만, 작은 함수에서는 종종 무의미하다.
#언제 쓰는 게 의미 있는가
다음 조건이 겹치면 효과가 보인다.
- 함수가 크다 — code layout 효과가 의미 있을 정도.
- branch가 자주 일어난다 — hot loop 안.
- PGO를 쓰지 않는다 — PGO가 있으면 힌트가 덜 중요.
- err/success rate가 명백하게 비대칭 — 99 또는 그 이상.
다음 조건에서는 효과가 거의 없다.
- 함수가 작고 인라인됨
- 분기가 한 번만 일어남 (loop 밖)
- 50 가까운 분기
#Abseil 내부의 사용 예
absl::Status의 ok 경로가 대표적인 예다.
inline bool Status::ok() const { return rep_ == kOkStatusRep; }
// 호출부 예시if (ABSL_PREDICT_TRUE(s.ok())) { return *value_;}return MakeStatusOrError(s);StatusOr::operator* 같은 hot accessor에 PREDICT_TRUE가 많이 들어가 있다. ok가 99% 이상이라는 가정.
absl::flat_hash_map의 lookup도 비슷하다.
// 의사 코드auto it = map.find(key);if (ABSL_PREDICT_FALSE(it == map.end())) { // miss path — 보통 새로 insert return DoMissPath();}return it->second;cache hit이 흔하다는 가정.
#코드 리뷰 포인트
// 회피 — error path가 hot loop 안에 있는데 힌트 없음for (auto& x : large_vec) { if (x.NeedsSpecialHandling()) { // 0.01% 발생 DoSpecial(x); } else { DoNormal(x); }}
// Goodfor (auto& x : large_vec) { if (ABSL_PREDICT_FALSE(x.NeedsSpecialHandling())) { DoSpecial(x); } else { DoNormal(x); }}// 회피 — 추측만으로 hint를 붙임if (ABSL_PREDICT_TRUE(user_input_value > 0)) { // user_input의 분포를 모르면서 힌트. // 실제로는 50:50일 수 있음. 잘못된 hint는 약간의 손해.}리뷰에서 봐야 할 것:
- hot loop인가 — 안쪽이 아니면 거의 의미 없다.
- 확률이 실제로 비대칭인가 — 측정 없이 추측하면 손해 볼 수 있다.
- PGO를 쓰는가 — 쓰면 hint는 부수적.
#자주 보는 안티패턴
// 회피 — 모든 if에 hint 붙이기if (ABSL_PREDICT_TRUE(x > 0)) { if (ABSL_PREDICT_TRUE(y > 0)) { if (ABSL_PREDICT_TRUE(z > 0)) { ... } }}// 가독성 해치고 효과는 미미.// 회피 — error 처리에 PREDICT_TRUE를 잘못 붙임if (ABSL_PREDICT_TRUE(s.ok())) { return *result_;} else { LOG(ERROR) << s; // 컴파일러가 cold로 배치 — 의도 맞음 return std::nullopt;}// hint 자체는 OK. 다만 ok가 정말 hot인지 확인해야.// 회피 — PREDICT를 if-else가 아닌 곳에return ABSL_PREDICT_TRUE(s.ok()) ? value : default_value;// 동작은 하지만 ternary에서는 효과 미미. 명시적 if가 낫다.#std와의 비교
C++20의 [[likely]] / [[unlikely]]가 같은 역할을 한다.
// C++20if (s.ok()) [[likely]] { return *value_;}
// Abseil pre-C++20if (ABSL_PREDICT_TRUE(s.ok())) { return *value_;}C++20을 쓸 수 있으면 [[likely]]가 권장된다. 표준이고, 컴파일러 분기 없이 동작한다. Abseil은 C++17 코드와의 호환을 위해 자체 매크로를 유지한다.
#정리
ABSL_PREDICT_*는 컴파일러의 code layout을 위한 힌트.- 가장 큰 효과는 cold path 분리에서 온다. 분기 예측 자체에는 거의 영향 없다.
- 효과를 보려면 함수가 크거나 hot loop 안이어야 한다.
- 측정 없이 hint를 남발하면 손해 볼 수 있다. PGO를 쓰면 hint는 부수적.
- C++20 코드는
[[likely]]/[[unlikely]]로 옮길 것.
#다음 편
Part 2-03에서 absl::LogSeverity를 본다. 로깅의 첫 단계인 severity가 어떻게 정의되어 있고, 왜 WARNING이 아니라 ABSL_WARNING인지를 다룬다.
#관련 항목
Abseil Code Review · 8 of 79
- 1 Abseil Code Review — Google production-grade C++ 라이브러리 분석
- 2 Abseil 개요 — Google이 std를 보완한 이유
- 3 Abseil 설계 철학 — std 호환과 추가 기능의 균형
- 4 Abseil 빌드와 의존성 — Bazel vs CMake
- 5 Abseil LTS vs HEAD 릴리스 모델 분석
- 6 Abseil Versioning과 ABI 호환성 정책
- 7 Abseil 매크로 — ABSL_HAVE_*·ABSL_ATTRIBUTE_*
- 8 Abseil ABSL_PREDICT_TRUE/FALSE — branch hint
- 9 absl::LogSeverity — 로그 레벨 타입
- 10 Abseil type_traits — negation·conjunction·void_t
- 11 Abseil Conformance·Policy 분석
- 12 Abseil Memory utilities 분석
- 13 Abseil raw_logging — heap-free 로깅
- 14 Abseil thread_annotations — clang TSA 통합
- 15 absl::Status — exception-free error handling
- 16 absl::StatusOr<T> — 값 또는 에러
- 17 absl status_macros — ASSIGN_OR_RETURN·RETURN_IF_ERROR
- 18 absl::Status payload — 구조화된 에러 컨텍스트
- 19 absl::Status ↔ exception 변환 패턴
- 20 absl::string_view — non-owning 문자열 참조
- 21 absl::string_view 함정 — dangling·c_str·임시 객체
- 22 absl::StrCat — 가변 인자 문자열 연결과 AlphaNum
- 23 absl::StrSplit — Delimiter·Predicate·컨테이너 변환
- 24 absl::StrJoin — 컨테이너 결합과 Formatter
- 25 absl::StrFormat — type-safe printf·FormatSpec
- 26 Abseil ASCII 함수 — locale-free 분류·대소문자 변환
- 27 Abseil Escape — CEscape·HexEscape·Base64
- 28 absl::flat_hash_map — Swiss Table 기반 hash map
- 29 absl::flat_hash_set — set 버전 Swiss Table
- 30 absl::node_hash_map — stable pointer가 필요할 때
- 31 absl::btree_map — sorted·cache-friendly B-tree
- 32 absl::FixedArray — 런타임 크기 stack 배열
- 33 absl::InlinedVector — small buffer optimization
- 34 Abseil Swiss Table internals — control byte·SIMD probing
- 35 absl::Mutex — reader-writer·fairness·deadlock 검출
- 36 absl::Mutex Conditional Critical Section — Await로 cv 없애기
- 37 absl::Notification — once-only signal
- 38 absl::BlockingCounter·Barrier — 다중 thread 조율
- 39 absl::Mutex annotations — clang thread-safety로 race를 컴파일 타임에
- 40 absl::Time·Duration 분석 — 단단한 type
- 41 absl::Time Format·Parse
- 42 absl::CivilTime 분석
- 43 absl::time_zone 분석
- 44 absl::Time mocking — 테스트 친화 시간
- 45 absl::BitGen — 모던 난수 생성기
- 46 Abseil Random Distributions — Uniform·Exponential
- 47 Abseil Mocking Random — 테스트 결정성
- 48 Abseil Random Seeding·Entropy
- 49 absl::int128·uint128 분석
- 50 absl::bits — popcount·countl_zero
- 51 absl::optional vs std::optional
- 52 absl::variant 분석
- 53 absl::span 분석
- 54 absl::any 분석
- 55 absl::compare — three-way 비교
- 56 Abseil utility — apply·in_place
- 57 Abseil AbslHashValue 분석
- 58 Abseil HashState chaining
- 59 Abseil Custom hashable 구현
- 60 Abseil LOG·VLOG·CHECK 분석
- 61 Abseil LogSink 분석
- 62 Abseil LogEntry·structured logging
- 63 Abseil Stack trace·failure_signal_handler
- 64 ABSL_FLAG 정의 분석
- 65 Abseil ParseCommandLine 동작
- 66 Abseil Flag introspection·validation
- 67 Google 스타일의 Abseil 사용 패턴
- 68 Abseil 자주 보는 anti-pattern
- 69 std → absl 마이그레이션 전략
- 70 absl::Cleanup — 함수 종료 시 실행 보장
- 71 Abseil algorithm container 확장 — c_sort·c_find_if·c_count_if
- 72 absl::function_ref와 any_invocable — 함수 객체 전달의 두 축
- 73 absl::bind_front와 Overload — 함수 객체 보조 도구
- 74 absl::Cord — 분산 시스템용 대용량 문자열
- 75 absl::from_chars·SimpleAtoi — 빠른 숫자 변환
- 76 absl::Cord vs std::string — 선택 기준과 메모리 프로파일
- 77 absl::GetStackTrace와 Symbolize — crash 시 readable stack
- 78 absl::ComputeCrc32c — 하드웨어 가속 체크섬
- 79 absl::PeriodicSampler — 적응형 샘플링·jitter 회피
관련 글
absl::PeriodicSampler — 적응형 샘플링·jitter 회피
absl::profiling_internal::PeriodicSampler — sampling rate를 동적으로 조정, geometric distribution으로 jitter 회피. 메모리 할당 추적·profiling 인프라의 기반.
같은 시리즈에서 이어 읽기
absl::from_chars·SimpleAtoi — 빠른 숫자 변환
absl::SimpleAtoi / SimpleAtof / from_chars — locale-free, exception-free, sscanf 대비 10~50배. std::charconv와의 관계.
같은 시리즈에서 이어 읽기
absl::StrCat — 가변 인자 문자열 연결과 AlphaNum
Part 4-03: absl::StrCat — variadic 문자열 연결, AlphaNum 어댑터, operator+ / ostringstream과의 성능 차이.
같은 시리즈에서 이어 읽기