본문으로 건너뛰기
Folly Code Review · 39/89

folly::EvictingCacheMap — LRU 구현 분석

· Hawk · 3분 읽기

#한 줄 요약

folly::EvictingCacheMap<K, V>는 fixed max size + LRU eviction의 hash map. access마다 가장 최근 위치로 옮기고, max size 초과 시 LRU 항목을 자동 제거. single-thread 전용.

#동기

cache의 두 핵심 요구.

  1. 최대 메모리 한도를 보장.
  2. 자주 쓰는 항목은 유지, 안 쓰는 건 제거.

std::unordered_map은 두 가지 모두 없다. 직접 구현하면 흔히 다음 둘을 같이 들고 다닌다.

  • std::unordered_map<K, list_iterator>: O(1) lookup.
  • std::list<pair<K, V>>: LRU ordering.

이 짝을 매번 작성하는 게 귀찮아 표준화한 것이 EvictingCacheMap. boost의 multi_index_container 또는 LinkedHashMap의 C++ 대응.

folly::EvictingCacheMap<int, std::string> cache(1000); // max 1000
cache.set(1, "a");
cache.get(1); // hit → LRU 앞으로
auto* p = cache.get_or_null(2); // miss → nullptr
// 1001번째 set 시 LRU 항목 자동 제거

#API & 사용법

#include <folly/container/EvictingCacheMap.h>
// 1. 생성 — max size, 옵션 nclear
folly::EvictingCacheMap<int, std::string> c(1024);
// 2. set — insert/update + LRU 앞으로
c.set(1, "a");
// 3. get — hit이면 LRU 앞으로
const std::string& v = c.get(1); // throws if miss
// 4. get_or_null — miss 시 nullptr
const std::string* p = c.get_or_null(2);
// 5. peek — LRU 갱신 안 함
const std::string* p2 = c.peek(1);
// 6. erase / clear
c.erase(1);
c.clear();
// 7. size / capacity
c.size(); c.getMaxSize();
// 8. 통계 — hit / miss / eviction
c.getStats();
// 9. eviction callback
c.setPruneHook([](const auto& k, auto&& v) {
LOG(INFO) << "evicted: " << k;
});

get은 LRU 위치를 옮긴다. peek은 옮기지 않음 (debugging/snapshot 용도).

#내부 구현

// 약식
template <typename K, typename V>
class EvictingCacheMap {
struct Node {
K key;
V value;
Node* prev;
Node* next;
};
folly::F14FastMap<K, Node*> index_; // key → node 빠른 lookup
Node* head_; // LRU front (most recent)
Node* tail_; // LRU back (least recent)
size_t size_;
size_t maxSize_;
std::function<void(K&&, V&&)> pruneHook_;
};

intrusive doubly linked list + hash map. 둘 다 O(1).

#Set

// 약식
void set(K k, V v) {
if (auto it = index_.find(k); it != index_.end()) {
// 이미 있음 — value 갱신 + LRU 앞으로
Node* n = it->second;
n->value = std::move(v);
moveToFront(n);
return;
}
// 새 entry
if (size_ >= maxSize_) evictOne(); // 한 개 제거
Node* n = newNode(std::move(k), std::move(v));
pushFront(n);
index_.emplace(n->key, n);
++size_;
}
void evictOne() {
Node* victim = tail_;
unlink(victim);
index_.erase(victim->key);
if (pruneHook_) pruneHook_(std::move(victim->key), std::move(victim->value));
delete victim;
--size_;
}

eviction은 tail 한 개씩. capacity 초과 폭이 크면 여러 번 호출.

#Get

const V& get(const K& k) {
auto it = index_.find(k);
if (it == index_.end()) throw std::out_of_range{};
Node* n = it->second;
moveToFront(n);
++stats_.hits;
return n->value;
}

매 get은 linked list pointer 3-4개 수정. 가벼우나 write임에 주의const get이라도 LRU 상태 변경.

#std/abseil 비교

표준에는 LRU cache가 없다. abseil에도 직접 대응 없음.

흔히 쓰는 대안:

  • boost::multi_index — full flexibility, 비용 큼.
  • 직접 구현 — std::list + std::unordered_map.
  • 3rd party (e.g. lru_cache11, hashlru) — header-only library.

folly::EvictingCacheMap은 fbcode 내부 표준 선택. 다른 folly type(F14, fbstring)과 잘 통합.

항목std/직접 구현boost::multi_indexfolly::EvictingCacheMap
Set/GetO(1)O(log n)O(1)
Iterator 안정rehash 시 invalid안정iterator 인터페이스 제한적
Prune callback직접 구현직접setPruneHook
Multi-threadXXX (single-thread 전용)
Custom policy직접가능LRU 전용

#사용 패턴

#1. URL → 처리 결과 cache

folly::EvictingCacheMap<std::string, Response> http_cache(10'000);
auto* cached = http_cache.get_or_null(url);
if (cached) return *cached;
auto resp = FetchAndProcess(url);
http_cache.set(url, std::move(resp));
return http_cache.get(url);

#2. Compiled regex cache

thread_local folly::EvictingCacheMap<std::string, std::regex>
regex_cache(128);
const std::regex& compile(const std::string& pattern) {
if (auto* r = regex_cache.get_or_null(pattern)) return *r;
regex_cache.set(pattern, std::regex{pattern});
return regex_cache.get(pattern);
}

thread_local로 multi-thread 회피.

#3. Stats callback

folly::EvictingCacheMap<int, BigObject> c(1024);
c.setPruneHook([](int k, BigObject&& v) {
metrics_.evicted.increment();
v.OnEvict(); // resource 해제 hook
});

#코드 리뷰 포인트

// Bad — multi-thread 사용
folly::EvictingCacheMap<int, std::string> shared_cache(1024);
// thread 여럿이 get/set → race
// Good — thread_local 또는 외부 mutex
thread_local folly::EvictingCacheMap<int, std::string> tls_cache(1024);
// 또는 folly::Synchronized<EvictingCacheMap<...>>

EvictingCacheMap은 single-thread 전용. concurrent용은 folly::ConcurrentHashMap + size limit을 직접 또는 외부 wrapper.

// 위험 — get으로 LRU 갱신, 의도 외 eviction 변경
const auto& v = cache.get(k); // LRU 앞으로
// 의도가 단순 확인이면 peek
// Good
if (auto* p = cache.peek(k)) Inspect(*p); // LRU 안 건드림

debugging/inspection은 peek. 진짜 hit으로 보고 싶으면 get.

// 주의 — max size를 너무 작게
folly::EvictingCacheMap<K, V> tiny(10);
// thrashing → hit rate 낮음 → cache 무용

cache는 working set 크기에 맞춰야 hit rate 의미. metric으로 모니터.

#안티패턴

  • multi-thread 공유: lock 없음 → race. thread_local 또는 외부 lock.
  • get의 LRU 부수 효과 무시: const-correctness 깨짐. get은 logical 쓰기.
  • pruneHook에서 heavy work: hook은 set/get 안에서 호출. blocking 작업은 별도 thread로.

#정리

  • EvictingCacheMap은 fixed size + LRU eviction의 single-thread cache.
  • intrusive list + F14 hash map으로 O(1) set/get/evict.
  • setPruneHook으로 eviction 시 콜백.
  • multi-thread는 thread_local 또는 외부 lock.
  • working set 크기에 맞춘 max size가 hit rate 결정.

#다음 편

Part 9에서 동기화 프리미티브를 본다. 시작은 folly::Synchronized — 데이터와 lock을 묶는 wrapper.

#관련 항목

Folly Code Review · 40 of 89

  1. 1 Folly Code Review — Meta의 production-grade C++ 라이브러리 코드 분석
  2. 2 Folly 개요 — Meta가 production에서 검증한 utility 모음 분석
  3. 3 Folly vs Abseil 철학 비교 — performance-first vs std-compatible
  4. 4 Folly 빌드와 fbcode 환경 — monorepo의 그림자
  5. 5 Folly API stability 정책 — 어떤 보장도 없다는 솔직함
  6. 6 Folly production validation 문화 — peta-scale에서 단련된 코드
  7. 7 folly::Future 분석 — std::future의 한계를 넘는 composable async
  8. 8 folly::Promise·makeFuture — Future를 만드는 두 길
  9. 9 folly::SemiFuture vs Future — executor binding의 명시화
  10. 10 folly::Future thenValue·thenError·thenTry — continuation 체인 분석
  11. 11 folly::collect·collectAll·collectAny — fan-in 패턴 분석
  12. 12 folly::Future retry·window·via — 제어 흐름 조합자
  13. 13 folly::fibers 분석 — M:N stackful coroutine
  14. 14 folly::InlineExecutor — 호출자 thread에서 즉시 실행
  15. 15 folly::CPUThreadPoolExecutor — CPU-bound 작업의 표준 thread pool
  16. 16 folly::IOThreadPoolExecutor — libevent 기반 I/O pool
  17. 17 folly::ManualExecutor — 결정적 테스트를 위한 수동 진행
  18. 18 folly::EventBase 분석 — libevent 이벤트 루프의 핵심
  19. 19 folly::IOBuf 분석 — zero-copy buffer chain의 기본 단위
  20. 20 folly::IOBufQueue — chain의 push/pull 추상화
  21. 21 folly::io::Cursor·RWCursor — chain 위의 stream
  22. 22 folly Zero-copy 패턴 — IOBuf로 ScatterGather I/O 표현
  23. 23 folly::IOBuf shared semantics — clone·unshare·takeOwnership
  24. 24 folly::FBString 분석 — SSO + COW 구현
  25. 25 folly의 fmt::format 통합 — 모던 포맷팅 채택
  26. 26 folly::StringPiece — string_view 호환 분석
  27. 27 folly Join·Split utilities — 문자열 분해와 결합
  28. 28 folly::to·tryTo — text↔num 변환 분석
  29. 29 folly Conv Customization — 사용자 타입 지원
  30. 30 folly Conv 성능 비교 — sprintf·stringstream 대비
  31. 31 folly::F14ValueMap vs std::unordered_map
  32. 32 folly::F14NodeMap — stable pointer가 필요할 때
  33. 33 folly::F14VectorMap — cache-friendly iteration
  34. 34 folly::F14FastMap — auto-select 동작
  35. 35 folly F14 internals — SIMD probing 메커니즘
  36. 36 folly::small_vector — inline storage 분석
  37. 37 folly::FixedString — compile-time string
  38. 38 folly::AtomicHashMap — lock-free read 분석
  39. 39 folly::ConcurrentHashMap — sharded 동시 해시 맵
  40. 40 folly::EvictingCacheMap — LRU 구현 분석
  41. 41 folly::Synchronized — lock wrapper 패턴
  42. 42 folly::SharedMutex 분석
  43. 43 folly::Baton — one-shot wait 동기화
  44. 44 folly::RWSpinLock 분석
  45. 45 folly::PicoSpinLock — 1-byte spinlock
  46. 46 folly::ProducerConsumerQueue — SPSC 큐 분석
  47. 47 folly::MPMCQueue — multi-producer multi-consumer
  48. 48 folly::UnboundedQueue — 동적 크기 lock-free
  49. 49 folly::fibers::Channel — Go-like channel
  50. 50 folly::dynamic — JSON-like dynamic type 분석
  51. 51 folly JSON conversion — toJson·parseJson
  52. 52 folly dynamic ↔ struct — manual marshaling
  53. 53 folly dynamic Visitor pattern — type별 분기
  54. 54 folly::Singleton vs Meyers/static — 왜 Folly의 Singleton인가
  55. 55 folly::SingletonVault 분석 — 등록·소멸·의존성
  56. 56 folly::Singleton try_get·try_get_fast — TLS-cached 접근
  57. 57 folly::ExceptionWrapper — type-erased exception holder
  58. 58 folly::ScopeGuard·SCOPE_EXIT — RAII cleanup
  59. 59 folly::Optional vs std::optional
  60. 60 folly::Function vs std::function
  61. 61 folly::Lazy — 지연 초기화 wrapper
  62. 62 folly Meta 스타일 code review 패턴
  63. 63 folly anti-patterns — 잘못 쓰면 std보다 느림
  64. 64 folly vs std 선택 기준 분석
  65. 65 folly::coro 개요 — production C++20 코루틴 어댑터
  66. 66 folly::coro::Task — lazy single-shot 코루틴
  67. 67 folly::coro::AsyncGenerator — 비동기 스트림
  68. 68 folly coro blockingWait·collectAll — 동기 경계와 fan-in
  69. 69 folly::coro::Baton·Mutex — 코루틴-aware 동기화
  70. 70 folly::Expected — 결과 또는 오류
  71. 71 folly::Try — Future 결과 wrapper
  72. 72 folly::Try vs Expected 선택 기준
  73. 73 folly::Range — 일반 iterator pair
  74. 74 folly::Uri — URL 파서
  75. 75 folly Fingerprint64·128 — 분산 hash
  76. 76 folly SpookyHashV2 — fast non-crypto hash
  77. 77 folly::Init — main() 부트스트랩
  78. 78 folly::Indestructible — global lifetime 패턴
  79. 79 folly::MicroLock — 1-byte 락
  80. 80 folly::MicroSpinLock — 가장 좁은 spin lock
  81. 81 folly::format — legacy formatter 분석
  82. 82 folly::demangle — typeid 디망글링
  83. 83 folly::DynamicConverter — dynamic ↔ struct
  84. 84 folly::RecordIO — append-only 로그 파일 포맷
  85. 85 folly::io::Compression — zstd·lz4·snappy wrapper
  86. 86 folly::AsyncIO — io_uring·Linux AIO
  87. 87 folly::CancellationToken — 코루틴·Future 취소 전파
  88. 88 folly::observer — hot config의 atomic refresh
  89. 89 fbcode 패턴 모음 — folly 사용의 실전