Abseil Tip 132 Avoid Redundant Map Lookups

Matt Kulukundis (kfm@google.com) 작성
최초 게시일: 2017년 3월 30일
최종 업데이트: 2019년 11월 25일

빠른 링크: abseil.io/tips/132


“이곳이 바로 스나크가 있을 곳이야!” 선장이 외쳤다.
그는 조심스럽게 그의 승무원들을 상륙시키며,
조수의 꼭대기에 머물며 머리카락을 손가락으로 얽어 쥐고 있었다.

“이곳이 바로 스나크가 있을 곳이야! 나는 두 번 말했다.
그것만으로도 승무원들에게 용기를 줄 것이다.
이곳이 바로 스나크가 있을 곳이야! 나는 세 번 말했다.
내가 세 번 말한 것은 사실이다.”

– 루이스 캐럴, The Hunting of the Snark에서 발췌


C++에서 연관 컨테이너(associative containers)는 매우 자주 사용되는 추상화 도구입니다. 그러나 종종 불필요한 작업을 수행하게 되는 경우가 많습니다. 이번 글에서는 이러한 추가 비용을 피할 수 있는 몇 가지 요령을 소개합니다.


결과 누적하기

종종 map은 공통 키를 수집하고 정보를 누적하는 데 사용됩니다. 예를 들어:

// 좋지 않은 코드: 문자열 키가 두 map에 복사됨
absl::flat_hash_map<std::string, int> word_counts;
absl::flat_hash_map<std::string, std::vector<std::string>> word_origins;
for (const auto& [origin, words] : origin_to_words) {
  for (const std::string& w : words) {
    if (!word_counts.contains(w)) {      // 첫 번째 조회
      InsertOrDie(&word_counts, w, 0);   // 두 번째 조회; 추가 문자열 복사
    }
    ++word_counts[w];                    // 세 번째 조회

    if (!word_origins.contains(w)) {     // 네 번째 조회
      InsertOrDie(&word_origins, w, {}); // 다섯 번째 조회; 추가 문자열 복사
    }
    word_origins[w].push_back(origin);   // 여섯 번째 조회
  }
}

operator[]가 이미 존재하지 않는 키에 대해 값 초기화된 인스턴스를 삽입한다는 사실을 활용하면, 다음과 같이 간결하게 작성할 수 있습니다:

// 문자열 키는 두 map에 복사됨
absl::flat_hash_map<std::string, int> word_counts;
absl::flat_hash_map<std::string, std::vector<std::string>> word_origins;
for (const auto& [origin, words] : origin_to_words) {
  for (const std::string& w : words) {
    ++word_counts[w];                    // 첫 번째 조회
    word_origins[w].push_back(origin);   // 두 번째 조회
  }
}

더 나아가, 키 중복 조회와 저장을 피하기 위해 두 map을 하나의 구조체로 결합할 수도 있습니다.

struct WordInfo {
  int count = 0;
  std::vector<std::string> origins;
};

absl::flat_hash_map<std::string, WordInfo> words_to_info;
for (const auto& [origin, words] : origin_to_words) {
  for (const std::string& w : words) {
    auto& info = words_to_info[w];
    ++info.count;
    info.origins.push_back(origin);
  }
}

이 패턴은 기본값이 누적의 초기 상태와 일치하는 경우에 유용합니다. 또한 코드 가독성도 높아집니다.


초기화 작업 한 번에 처리하기

때로는 map이 복잡한 객체나 무거운 연산의 결과를 캐싱하기 위해 사용됩니다.

// 좋지 않은 코드
class CobblerCache {
 public:
  const CobblerInterface& GetCobbler(const std::string& key) {
    if (!cobblers_.contains(key)) {                          // 첫 번째 조회
      InsertOrDie(&cobblers_, key, FluevogMaker::Create());  // 두 번째 조회
    }
    return *FindOrDie(cobblers_, key);                       // 세 번째 조회
  }

 private:
  absl::flat_hash_map<std::string, std::unique_ptr<CobblerInterface>> cobblers_;
};

operator[]가 새로운 값을 삽입할 때 std::unique_ptr이 기본적으로 null로 초기화된다는 점을 활용할 수 있습니다.

class CobblerCache {
 public:
  const CobblerInterface& GetCobbler(const std::string& key) {
    auto& cobbler = cobblers_[key];
    if (cobbler == nullptr) {
      cobbler = FluevogMaker::Create();
    }
    return *cobbler;
  }

 private:
  absl::flat_hash_map<std::string, std::unique_ptr<CobblerInterface>> cobblers_;
};

cobbler는 map 내부 값을 참조하므로 operator[] 호출 이후 추가 작업 없이 값을 설정할 수 있습니다.


안전한 조회

때로는 map에서 항목을 찾고, 실패할 경우 안전하게 빠져나오고 싶을 때가 있습니다.

// 좋지 않은 코드
class CobblerCache {
 public:
  std::unique_ptr<Shoe> MaybeMakeShoe(const std::string& key,
                                      const ShoeSpec& spec) {
    if (!cobblers_.contains(key)) return nullptr;      // 첫 번째 조회
    return FindOrDie(cobblers_, key)->MakeShoe(spec);  // 두 번째 조회
  }

 private:
  absl::flat_hash_map<std::string, std::unique_ptr<CobblerInterface>> cobblers_;
};

다음과 같이 작성하는 것이 더 나은 방법입니다.

class CobblerCache {
 public:
  std::unique_ptr<Shoe> MaybeMakeShoe(const std::string& key,
                                      const ShoeSpec& spec) {
    auto it = cobblers_.find(key);
    if (it == cobblers_.end()) return nullptr;
    return it->second->MakeShoe(spec);
  }
};

중복 항목 세기

때로는 map에 없는 항목을 삽입하고, 그렇지 않은 경우 특정 작업을 수행하고 싶을 수 있습니다.

// 좋지 않은 코드
int duplicates = 0;
absl::flat_hash_set<std::string> seen;

for (const std::string& id : ids) {
  if (seen.contains(id)) {  // 첫 번째 조회
    ++duplicates;
  } else {
    seen.insert(id);        // 두 번째 조회
  }
}

absl::flat_hash_set::insert는 삽입된 요소의 반복자와 삽입 여부를 나타내는 bool 값을 반환하므로 이를 활용할 수 있습니다.

int duplicates = 0;
absl::flat_hash_set<std::string> seen;

for (const std::string& id : ids) {
  if (!seen.insert(id).second) {
    ++duplicates;
  }
}

최선의 사용법

연관 컨테이너를 효율적으로 사용하는 동시에 가독성을 희생하지 않는 방법이 종종 있습니다. 이러한 API와 사용법을 배우고 활용하세요. 컨테이너 유형은 매우 자주 사용되기 때문에 이러한 기법에 익숙하다고 가정해도 좋습니다.