1*0b57cec5SDimitry Andric //===- MultiOnDiskHashTable.h - Merged set of hash tables -------*- C++ -*-===// 2*0b57cec5SDimitry Andric // 3*0b57cec5SDimitry Andric // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 4*0b57cec5SDimitry Andric // See https://llvm.org/LICENSE.txt for license information. 5*0b57cec5SDimitry Andric // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 6*0b57cec5SDimitry Andric // 7*0b57cec5SDimitry Andric //===----------------------------------------------------------------------===// 8*0b57cec5SDimitry Andric // 9*0b57cec5SDimitry Andric // This file provides a hash table data structure suitable for incremental and 10*0b57cec5SDimitry Andric // distributed storage across a set of files. 11*0b57cec5SDimitry Andric // 12*0b57cec5SDimitry Andric // Multiple hash tables from different files are implicitly merged to improve 13*0b57cec5SDimitry Andric // performance, and on reload the merged table will override those from other 14*0b57cec5SDimitry Andric // files. 15*0b57cec5SDimitry Andric // 16*0b57cec5SDimitry Andric //===----------------------------------------------------------------------===// 17*0b57cec5SDimitry Andric 18*0b57cec5SDimitry Andric #ifndef LLVM_CLANG_LIB_SERIALIZATION_MULTIONDISKHASHTABLE_H 19*0b57cec5SDimitry Andric #define LLVM_CLANG_LIB_SERIALIZATION_MULTIONDISKHASHTABLE_H 20*0b57cec5SDimitry Andric 21*0b57cec5SDimitry Andric #include "llvm/ADT/DenseMap.h" 22*0b57cec5SDimitry Andric #include "llvm/ADT/DenseSet.h" 23*0b57cec5SDimitry Andric #include "llvm/ADT/PointerUnion.h" 24*0b57cec5SDimitry Andric #include "llvm/ADT/STLExtras.h" 25*0b57cec5SDimitry Andric #include "llvm/ADT/SmallVector.h" 26*0b57cec5SDimitry Andric #include "llvm/ADT/TinyPtrVector.h" 27*0b57cec5SDimitry Andric #include "llvm/ADT/iterator_range.h" 28*0b57cec5SDimitry Andric #include "llvm/Support/Endian.h" 29*0b57cec5SDimitry Andric #include "llvm/Support/EndianStream.h" 30*0b57cec5SDimitry Andric #include "llvm/Support/OnDiskHashTable.h" 31*0b57cec5SDimitry Andric #include "llvm/Support/raw_ostream.h" 32*0b57cec5SDimitry Andric #include <algorithm> 33*0b57cec5SDimitry Andric #include <cstdint> 34*0b57cec5SDimitry Andric #include <vector> 35*0b57cec5SDimitry Andric 36*0b57cec5SDimitry Andric namespace clang { 37*0b57cec5SDimitry Andric namespace serialization { 38*0b57cec5SDimitry Andric 39*0b57cec5SDimitry Andric /// A collection of on-disk hash tables, merged when relevant for performance. 40*0b57cec5SDimitry Andric template<typename Info> class MultiOnDiskHashTable { 41*0b57cec5SDimitry Andric public: 42*0b57cec5SDimitry Andric /// A handle to a file, used when overriding tables. 43*0b57cec5SDimitry Andric using file_type = typename Info::file_type; 44*0b57cec5SDimitry Andric 45*0b57cec5SDimitry Andric /// A pointer to an on-disk representation of the hash table. 46*0b57cec5SDimitry Andric using storage_type = const unsigned char *; 47*0b57cec5SDimitry Andric 48*0b57cec5SDimitry Andric using external_key_type = typename Info::external_key_type; 49*0b57cec5SDimitry Andric using internal_key_type = typename Info::internal_key_type; 50*0b57cec5SDimitry Andric using data_type = typename Info::data_type; 51*0b57cec5SDimitry Andric using data_type_builder = typename Info::data_type_builder; 52*0b57cec5SDimitry Andric using hash_value_type = unsigned; 53*0b57cec5SDimitry Andric 54*0b57cec5SDimitry Andric private: 55*0b57cec5SDimitry Andric /// The generator is permitted to read our merged table. 56*0b57cec5SDimitry Andric template<typename ReaderInfo, typename WriterInfo> 57*0b57cec5SDimitry Andric friend class MultiOnDiskHashTableGenerator; 58*0b57cec5SDimitry Andric 59*0b57cec5SDimitry Andric /// A hash table stored on disk. 60*0b57cec5SDimitry Andric struct OnDiskTable { 61*0b57cec5SDimitry Andric using HashTable = llvm::OnDiskIterableChainedHashTable<Info>; 62*0b57cec5SDimitry Andric 63*0b57cec5SDimitry Andric file_type File; 64*0b57cec5SDimitry Andric HashTable Table; 65*0b57cec5SDimitry Andric 66*0b57cec5SDimitry Andric OnDiskTable(file_type File, unsigned NumBuckets, unsigned NumEntries, 67*0b57cec5SDimitry Andric storage_type Buckets, storage_type Payload, storage_type Base, 68*0b57cec5SDimitry Andric const Info &InfoObj) 69*0b57cec5SDimitry Andric : File(File), 70*0b57cec5SDimitry Andric Table(NumBuckets, NumEntries, Buckets, Payload, Base, InfoObj) {} 71*0b57cec5SDimitry Andric }; 72*0b57cec5SDimitry Andric 73*0b57cec5SDimitry Andric struct MergedTable { 74*0b57cec5SDimitry Andric std::vector<file_type> Files; 75*0b57cec5SDimitry Andric llvm::DenseMap<internal_key_type, data_type> Data; 76*0b57cec5SDimitry Andric }; 77*0b57cec5SDimitry Andric 78*0b57cec5SDimitry Andric using Table = llvm::PointerUnion<OnDiskTable *, MergedTable *>; 79*0b57cec5SDimitry Andric using TableVector = llvm::TinyPtrVector<void *>; 80*0b57cec5SDimitry Andric 81*0b57cec5SDimitry Andric /// The current set of on-disk and merged tables. 82*0b57cec5SDimitry Andric /// We manually store the opaque value of the Table because TinyPtrVector 83*0b57cec5SDimitry Andric /// can't cope with holding a PointerUnion directly. 84*0b57cec5SDimitry Andric /// There can be at most one MergedTable in this vector, and if present, 85*0b57cec5SDimitry Andric /// it is the first table. 86*0b57cec5SDimitry Andric TableVector Tables; 87*0b57cec5SDimitry Andric 88*0b57cec5SDimitry Andric /// Files corresponding to overridden tables that we've not yet 89*0b57cec5SDimitry Andric /// discarded. 90*0b57cec5SDimitry Andric llvm::TinyPtrVector<file_type> PendingOverrides; 91*0b57cec5SDimitry Andric 92*0b57cec5SDimitry Andric struct AsOnDiskTable { 93*0b57cec5SDimitry Andric using result_type = OnDiskTable *; 94*0b57cec5SDimitry Andric 95*0b57cec5SDimitry Andric result_type operator()(void *P) const { 96*0b57cec5SDimitry Andric return Table::getFromOpaqueValue(P).template get<OnDiskTable *>(); 97*0b57cec5SDimitry Andric } 98*0b57cec5SDimitry Andric }; 99*0b57cec5SDimitry Andric 100*0b57cec5SDimitry Andric using table_iterator = 101*0b57cec5SDimitry Andric llvm::mapped_iterator<TableVector::iterator, AsOnDiskTable>; 102*0b57cec5SDimitry Andric using table_range = llvm::iterator_range<table_iterator>; 103*0b57cec5SDimitry Andric 104*0b57cec5SDimitry Andric /// The current set of on-disk tables. 105*0b57cec5SDimitry Andric table_range tables() { 106*0b57cec5SDimitry Andric auto Begin = Tables.begin(), End = Tables.end(); 107*0b57cec5SDimitry Andric if (getMergedTable()) 108*0b57cec5SDimitry Andric ++Begin; 109*0b57cec5SDimitry Andric return llvm::make_range(llvm::map_iterator(Begin, AsOnDiskTable()), 110*0b57cec5SDimitry Andric llvm::map_iterator(End, AsOnDiskTable())); 111*0b57cec5SDimitry Andric } 112*0b57cec5SDimitry Andric 113*0b57cec5SDimitry Andric MergedTable *getMergedTable() const { 114*0b57cec5SDimitry Andric // If we already have a merged table, it's the first one. 115*0b57cec5SDimitry Andric return Tables.empty() ? nullptr : Table::getFromOpaqueValue(*Tables.begin()) 116*0b57cec5SDimitry Andric .template dyn_cast<MergedTable*>(); 117*0b57cec5SDimitry Andric } 118*0b57cec5SDimitry Andric 119*0b57cec5SDimitry Andric /// Delete all our current on-disk tables. 120*0b57cec5SDimitry Andric void clear() { 121*0b57cec5SDimitry Andric for (auto *T : tables()) 122*0b57cec5SDimitry Andric delete T; 123*0b57cec5SDimitry Andric if (auto *M = getMergedTable()) 124*0b57cec5SDimitry Andric delete M; 125*0b57cec5SDimitry Andric Tables.clear(); 126*0b57cec5SDimitry Andric } 127*0b57cec5SDimitry Andric 128*0b57cec5SDimitry Andric void removeOverriddenTables() { 129*0b57cec5SDimitry Andric llvm::DenseSet<file_type> Files; 130*0b57cec5SDimitry Andric Files.insert(PendingOverrides.begin(), PendingOverrides.end()); 131*0b57cec5SDimitry Andric // Explicitly capture Files to work around an MSVC 2015 rejects-valid bug. 132*0b57cec5SDimitry Andric auto ShouldRemove = [&Files](void *T) -> bool { 133*0b57cec5SDimitry Andric auto *ODT = Table::getFromOpaqueValue(T).template get<OnDiskTable *>(); 134*0b57cec5SDimitry Andric bool Remove = Files.count(ODT->File); 135*0b57cec5SDimitry Andric if (Remove) 136*0b57cec5SDimitry Andric delete ODT; 137*0b57cec5SDimitry Andric return Remove; 138*0b57cec5SDimitry Andric }; 139*0b57cec5SDimitry Andric Tables.erase(std::remove_if(tables().begin().getCurrent(), Tables.end(), 140*0b57cec5SDimitry Andric ShouldRemove), 141*0b57cec5SDimitry Andric Tables.end()); 142*0b57cec5SDimitry Andric PendingOverrides.clear(); 143*0b57cec5SDimitry Andric } 144*0b57cec5SDimitry Andric 145*0b57cec5SDimitry Andric void condense() { 146*0b57cec5SDimitry Andric MergedTable *Merged = getMergedTable(); 147*0b57cec5SDimitry Andric if (!Merged) 148*0b57cec5SDimitry Andric Merged = new MergedTable; 149*0b57cec5SDimitry Andric 150*0b57cec5SDimitry Andric // Read in all the tables and merge them together. 151*0b57cec5SDimitry Andric // FIXME: Be smarter about which tables we merge. 152*0b57cec5SDimitry Andric for (auto *ODT : tables()) { 153*0b57cec5SDimitry Andric auto &HT = ODT->Table; 154*0b57cec5SDimitry Andric Info &InfoObj = HT.getInfoObj(); 155*0b57cec5SDimitry Andric 156*0b57cec5SDimitry Andric for (auto I = HT.data_begin(), E = HT.data_end(); I != E; ++I) { 157*0b57cec5SDimitry Andric auto *LocalPtr = I.getItem(); 158*0b57cec5SDimitry Andric 159*0b57cec5SDimitry Andric // FIXME: Don't rely on the OnDiskHashTable format here. 160*0b57cec5SDimitry Andric auto L = InfoObj.ReadKeyDataLength(LocalPtr); 161*0b57cec5SDimitry Andric const internal_key_type &Key = InfoObj.ReadKey(LocalPtr, L.first); 162*0b57cec5SDimitry Andric data_type_builder ValueBuilder(Merged->Data[Key]); 163*0b57cec5SDimitry Andric InfoObj.ReadDataInto(Key, LocalPtr + L.first, L.second, 164*0b57cec5SDimitry Andric ValueBuilder); 165*0b57cec5SDimitry Andric } 166*0b57cec5SDimitry Andric 167*0b57cec5SDimitry Andric Merged->Files.push_back(ODT->File); 168*0b57cec5SDimitry Andric delete ODT; 169*0b57cec5SDimitry Andric } 170*0b57cec5SDimitry Andric 171*0b57cec5SDimitry Andric Tables.clear(); 172*0b57cec5SDimitry Andric Tables.push_back(Table(Merged).getOpaqueValue()); 173*0b57cec5SDimitry Andric } 174*0b57cec5SDimitry Andric 175*0b57cec5SDimitry Andric public: 176*0b57cec5SDimitry Andric MultiOnDiskHashTable() = default; 177*0b57cec5SDimitry Andric 178*0b57cec5SDimitry Andric MultiOnDiskHashTable(MultiOnDiskHashTable &&O) 179*0b57cec5SDimitry Andric : Tables(std::move(O.Tables)), 180*0b57cec5SDimitry Andric PendingOverrides(std::move(O.PendingOverrides)) { 181*0b57cec5SDimitry Andric O.Tables.clear(); 182*0b57cec5SDimitry Andric } 183*0b57cec5SDimitry Andric 184*0b57cec5SDimitry Andric MultiOnDiskHashTable &operator=(MultiOnDiskHashTable &&O) { 185*0b57cec5SDimitry Andric if (&O == this) 186*0b57cec5SDimitry Andric return *this; 187*0b57cec5SDimitry Andric clear(); 188*0b57cec5SDimitry Andric Tables = std::move(O.Tables); 189*0b57cec5SDimitry Andric O.Tables.clear(); 190*0b57cec5SDimitry Andric PendingOverrides = std::move(O.PendingOverrides); 191*0b57cec5SDimitry Andric return *this; 192*0b57cec5SDimitry Andric } 193*0b57cec5SDimitry Andric 194*0b57cec5SDimitry Andric ~MultiOnDiskHashTable() { clear(); } 195*0b57cec5SDimitry Andric 196*0b57cec5SDimitry Andric /// Add the table \p Data loaded from file \p File. 197*0b57cec5SDimitry Andric void add(file_type File, storage_type Data, Info InfoObj = Info()) { 198*0b57cec5SDimitry Andric using namespace llvm::support; 199*0b57cec5SDimitry Andric 200*0b57cec5SDimitry Andric storage_type Ptr = Data; 201*0b57cec5SDimitry Andric 202*0b57cec5SDimitry Andric uint32_t BucketOffset = endian::readNext<uint32_t, little, unaligned>(Ptr); 203*0b57cec5SDimitry Andric 204*0b57cec5SDimitry Andric // Read the list of overridden files. 205*0b57cec5SDimitry Andric uint32_t NumFiles = endian::readNext<uint32_t, little, unaligned>(Ptr); 206*0b57cec5SDimitry Andric // FIXME: Add a reserve() to TinyPtrVector so that we don't need to make 207*0b57cec5SDimitry Andric // an additional copy. 208*0b57cec5SDimitry Andric llvm::SmallVector<file_type, 16> OverriddenFiles; 209*0b57cec5SDimitry Andric OverriddenFiles.reserve(NumFiles); 210*0b57cec5SDimitry Andric for (/**/; NumFiles != 0; --NumFiles) 211*0b57cec5SDimitry Andric OverriddenFiles.push_back(InfoObj.ReadFileRef(Ptr)); 212*0b57cec5SDimitry Andric PendingOverrides.insert(PendingOverrides.end(), OverriddenFiles.begin(), 213*0b57cec5SDimitry Andric OverriddenFiles.end()); 214*0b57cec5SDimitry Andric 215*0b57cec5SDimitry Andric // Read the OnDiskChainedHashTable header. 216*0b57cec5SDimitry Andric storage_type Buckets = Data + BucketOffset; 217*0b57cec5SDimitry Andric auto NumBucketsAndEntries = 218*0b57cec5SDimitry Andric OnDiskTable::HashTable::readNumBucketsAndEntries(Buckets); 219*0b57cec5SDimitry Andric 220*0b57cec5SDimitry Andric // Register the table. 221*0b57cec5SDimitry Andric Table NewTable = new OnDiskTable(File, NumBucketsAndEntries.first, 222*0b57cec5SDimitry Andric NumBucketsAndEntries.second, 223*0b57cec5SDimitry Andric Buckets, Ptr, Data, std::move(InfoObj)); 224*0b57cec5SDimitry Andric Tables.push_back(NewTable.getOpaqueValue()); 225*0b57cec5SDimitry Andric } 226*0b57cec5SDimitry Andric 227*0b57cec5SDimitry Andric /// Find and read the lookup results for \p EKey. 228*0b57cec5SDimitry Andric data_type find(const external_key_type &EKey) { 229*0b57cec5SDimitry Andric data_type Result; 230*0b57cec5SDimitry Andric 231*0b57cec5SDimitry Andric if (!PendingOverrides.empty()) 232*0b57cec5SDimitry Andric removeOverriddenTables(); 233*0b57cec5SDimitry Andric 234*0b57cec5SDimitry Andric if (Tables.size() > static_cast<unsigned>(Info::MaxTables)) 235*0b57cec5SDimitry Andric condense(); 236*0b57cec5SDimitry Andric 237*0b57cec5SDimitry Andric internal_key_type Key = Info::GetInternalKey(EKey); 238*0b57cec5SDimitry Andric auto KeyHash = Info::ComputeHash(Key); 239*0b57cec5SDimitry Andric 240*0b57cec5SDimitry Andric if (MergedTable *M = getMergedTable()) { 241*0b57cec5SDimitry Andric auto It = M->Data.find(Key); 242*0b57cec5SDimitry Andric if (It != M->Data.end()) 243*0b57cec5SDimitry Andric Result = It->second; 244*0b57cec5SDimitry Andric } 245*0b57cec5SDimitry Andric 246*0b57cec5SDimitry Andric data_type_builder ResultBuilder(Result); 247*0b57cec5SDimitry Andric 248*0b57cec5SDimitry Andric for (auto *ODT : tables()) { 249*0b57cec5SDimitry Andric auto &HT = ODT->Table; 250*0b57cec5SDimitry Andric auto It = HT.find_hashed(Key, KeyHash); 251*0b57cec5SDimitry Andric if (It != HT.end()) 252*0b57cec5SDimitry Andric HT.getInfoObj().ReadDataInto(Key, It.getDataPtr(), It.getDataLen(), 253*0b57cec5SDimitry Andric ResultBuilder); 254*0b57cec5SDimitry Andric } 255*0b57cec5SDimitry Andric 256*0b57cec5SDimitry Andric return Result; 257*0b57cec5SDimitry Andric } 258*0b57cec5SDimitry Andric 259*0b57cec5SDimitry Andric /// Read all the lookup results into a single value. This only makes 260*0b57cec5SDimitry Andric /// sense if merging values across keys is meaningful. 261*0b57cec5SDimitry Andric data_type findAll() { 262*0b57cec5SDimitry Andric data_type Result; 263*0b57cec5SDimitry Andric data_type_builder ResultBuilder(Result); 264*0b57cec5SDimitry Andric 265*0b57cec5SDimitry Andric if (!PendingOverrides.empty()) 266*0b57cec5SDimitry Andric removeOverriddenTables(); 267*0b57cec5SDimitry Andric 268*0b57cec5SDimitry Andric if (MergedTable *M = getMergedTable()) { 269*0b57cec5SDimitry Andric for (auto &KV : M->Data) 270*0b57cec5SDimitry Andric Info::MergeDataInto(KV.second, ResultBuilder); 271*0b57cec5SDimitry Andric } 272*0b57cec5SDimitry Andric 273*0b57cec5SDimitry Andric for (auto *ODT : tables()) { 274*0b57cec5SDimitry Andric auto &HT = ODT->Table; 275*0b57cec5SDimitry Andric Info &InfoObj = HT.getInfoObj(); 276*0b57cec5SDimitry Andric for (auto I = HT.data_begin(), E = HT.data_end(); I != E; ++I) { 277*0b57cec5SDimitry Andric auto *LocalPtr = I.getItem(); 278*0b57cec5SDimitry Andric 279*0b57cec5SDimitry Andric // FIXME: Don't rely on the OnDiskHashTable format here. 280*0b57cec5SDimitry Andric auto L = InfoObj.ReadKeyDataLength(LocalPtr); 281*0b57cec5SDimitry Andric const internal_key_type &Key = InfoObj.ReadKey(LocalPtr, L.first); 282*0b57cec5SDimitry Andric InfoObj.ReadDataInto(Key, LocalPtr + L.first, L.second, ResultBuilder); 283*0b57cec5SDimitry Andric } 284*0b57cec5SDimitry Andric } 285*0b57cec5SDimitry Andric 286*0b57cec5SDimitry Andric return Result; 287*0b57cec5SDimitry Andric } 288*0b57cec5SDimitry Andric }; 289*0b57cec5SDimitry Andric 290*0b57cec5SDimitry Andric /// Writer for the on-disk hash table. 291*0b57cec5SDimitry Andric template<typename ReaderInfo, typename WriterInfo> 292*0b57cec5SDimitry Andric class MultiOnDiskHashTableGenerator { 293*0b57cec5SDimitry Andric using BaseTable = MultiOnDiskHashTable<ReaderInfo>; 294*0b57cec5SDimitry Andric using Generator = llvm::OnDiskChainedHashTableGenerator<WriterInfo>; 295*0b57cec5SDimitry Andric 296*0b57cec5SDimitry Andric Generator Gen; 297*0b57cec5SDimitry Andric 298*0b57cec5SDimitry Andric public: 299*0b57cec5SDimitry Andric MultiOnDiskHashTableGenerator() : Gen() {} 300*0b57cec5SDimitry Andric 301*0b57cec5SDimitry Andric void insert(typename WriterInfo::key_type_ref Key, 302*0b57cec5SDimitry Andric typename WriterInfo::data_type_ref Data, WriterInfo &Info) { 303*0b57cec5SDimitry Andric Gen.insert(Key, Data, Info); 304*0b57cec5SDimitry Andric } 305*0b57cec5SDimitry Andric 306*0b57cec5SDimitry Andric void emit(llvm::SmallVectorImpl<char> &Out, WriterInfo &Info, 307*0b57cec5SDimitry Andric const BaseTable *Base) { 308*0b57cec5SDimitry Andric using namespace llvm::support; 309*0b57cec5SDimitry Andric 310*0b57cec5SDimitry Andric llvm::raw_svector_ostream OutStream(Out); 311*0b57cec5SDimitry Andric 312*0b57cec5SDimitry Andric // Write our header information. 313*0b57cec5SDimitry Andric { 314*0b57cec5SDimitry Andric endian::Writer Writer(OutStream, little); 315*0b57cec5SDimitry Andric 316*0b57cec5SDimitry Andric // Reserve four bytes for the bucket offset. 317*0b57cec5SDimitry Andric Writer.write<uint32_t>(0); 318*0b57cec5SDimitry Andric 319*0b57cec5SDimitry Andric if (auto *Merged = Base ? Base->getMergedTable() : nullptr) { 320*0b57cec5SDimitry Andric // Write list of overridden files. 321*0b57cec5SDimitry Andric Writer.write<uint32_t>(Merged->Files.size()); 322*0b57cec5SDimitry Andric for (const auto &F : Merged->Files) 323*0b57cec5SDimitry Andric Info.EmitFileRef(OutStream, F); 324*0b57cec5SDimitry Andric 325*0b57cec5SDimitry Andric // Add all merged entries from Base to the generator. 326*0b57cec5SDimitry Andric for (auto &KV : Merged->Data) { 327*0b57cec5SDimitry Andric if (!Gen.contains(KV.first, Info)) 328*0b57cec5SDimitry Andric Gen.insert(KV.first, Info.ImportData(KV.second), Info); 329*0b57cec5SDimitry Andric } 330*0b57cec5SDimitry Andric } else { 331*0b57cec5SDimitry Andric Writer.write<uint32_t>(0); 332*0b57cec5SDimitry Andric } 333*0b57cec5SDimitry Andric } 334*0b57cec5SDimitry Andric 335*0b57cec5SDimitry Andric // Write the table itself. 336*0b57cec5SDimitry Andric uint32_t BucketOffset = Gen.Emit(OutStream, Info); 337*0b57cec5SDimitry Andric 338*0b57cec5SDimitry Andric // Replace the first four bytes with the bucket offset. 339*0b57cec5SDimitry Andric endian::write32le(Out.data(), BucketOffset); 340*0b57cec5SDimitry Andric } 341*0b57cec5SDimitry Andric }; 342*0b57cec5SDimitry Andric 343*0b57cec5SDimitry Andric } // namespace serialization 344*0b57cec5SDimitry Andric } // namespace clang 345*0b57cec5SDimitry Andric 346*0b57cec5SDimitry Andric #endif // LLVM_CLANG_LIB_SERIALIZATION_MULTIONDISKHASHTABLE_H 347