1 //===-- local_cache.h -------------------------------------------*- C++ -*-===//
2 //
3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4 // See https://llvm.org/LICENSE.txt for license information.
5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6 //
7 //===----------------------------------------------------------------------===//
8 
9 #ifndef SCUDO_LOCAL_CACHE_H_
10 #define SCUDO_LOCAL_CACHE_H_
11 
12 #include "internal_defs.h"
13 #include "list.h"
14 #include "platform.h"
15 #include "report.h"
16 #include "stats.h"
17 
18 namespace scudo {
19 
20 template <class SizeClassAllocator> struct SizeClassAllocatorLocalCache {
21   typedef typename SizeClassAllocator::SizeClassMap SizeClassMap;
22   typedef typename SizeClassAllocator::CompactPtrT CompactPtrT;
23 
24   struct TransferBatch {
25     static const u16 MaxNumCached = SizeClassMap::MaxNumCachedHint;
setFromArraySizeClassAllocatorLocalCache::TransferBatch26     void setFromArray(CompactPtrT *Array, u16 N) {
27       DCHECK_LE(N, MaxNumCached);
28       Count = N;
29       memcpy(Batch, Array, sizeof(Batch[0]) * Count);
30     }
appendFromArraySizeClassAllocatorLocalCache::TransferBatch31     void appendFromArray(CompactPtrT *Array, u16 N) {
32       DCHECK_LE(N, MaxNumCached - Count);
33       memcpy(Batch + Count, Array, sizeof(Batch[0]) * N);
34       // u16 will be promoted to int by arithmetic type conversion.
35       Count = static_cast<u16>(Count + N);
36     }
clearSizeClassAllocatorLocalCache::TransferBatch37     void clear() { Count = 0; }
addSizeClassAllocatorLocalCache::TransferBatch38     void add(CompactPtrT P) {
39       DCHECK_LT(Count, MaxNumCached);
40       Batch[Count++] = P;
41     }
copyToArraySizeClassAllocatorLocalCache::TransferBatch42     void copyToArray(CompactPtrT *Array) const {
43       memcpy(Array, Batch, sizeof(Batch[0]) * Count);
44     }
getCountSizeClassAllocatorLocalCache::TransferBatch45     u16 getCount() const { return Count; }
getSizeClassAllocatorLocalCache::TransferBatch46     CompactPtrT get(u16 I) const {
47       DCHECK_LE(I, Count);
48       return Batch[I];
49     }
getMaxCachedSizeClassAllocatorLocalCache::TransferBatch50     static u16 getMaxCached(uptr Size) {
51       return Min(MaxNumCached, SizeClassMap::getMaxCachedHint(Size));
52     }
53     TransferBatch *Next;
54 
55   private:
56     CompactPtrT Batch[MaxNumCached];
57     u16 Count;
58   };
59 
60   // A BatchGroup is used to collect blocks. Each group has a group id to
61   // identify the group kind of contained blocks.
62   struct BatchGroup {
63     // `Next` is used by IntrusiveList.
64     BatchGroup *Next;
65     // The identifier of each group
66     uptr GroupId;
67     // Cache value of TransferBatch::getMaxCached()
68     u16 MaxCachedPerBatch;
69     // Number of blocks pushed into this group. This is an increment-only
70     // counter.
71     uptr PushedBlocks;
72     // This is used to track how many blocks are pushed since last time we
73     // checked `PushedBlocks`. It's useful for page releasing to determine the
74     // usage of a BatchGroup.
75     uptr PushedBlocksAtLastCheckpoint;
76     // Blocks are managed by TransferBatch in a list.
77     SinglyLinkedList<TransferBatch> Batches;
78   };
79 
80   static_assert(sizeof(BatchGroup) <= sizeof(TransferBatch),
81                 "BatchGroup uses the same class size as TransferBatch");
82 
initSizeClassAllocatorLocalCache83   void init(GlobalStats *S, SizeClassAllocator *A) {
84     DCHECK(isEmpty());
85     Stats.init();
86     if (LIKELY(S))
87       S->link(&Stats);
88     Allocator = A;
89   }
90 
destroySizeClassAllocatorLocalCache91   void destroy(GlobalStats *S) {
92     drain();
93     if (LIKELY(S))
94       S->unlink(&Stats);
95   }
96 
allocateSizeClassAllocatorLocalCache97   void *allocate(uptr ClassId) {
98     DCHECK_LT(ClassId, NumClasses);
99     PerClass *C = &PerClassArray[ClassId];
100     if (C->Count == 0) {
101       if (UNLIKELY(!refill(C, ClassId)))
102         return nullptr;
103       DCHECK_GT(C->Count, 0);
104     }
105     // We read ClassSize first before accessing Chunks because it's adjacent to
106     // Count, while Chunks might be further off (depending on Count). That keeps
107     // the memory accesses in close quarters.
108     const uptr ClassSize = C->ClassSize;
109     CompactPtrT CompactP = C->Chunks[--C->Count];
110     Stats.add(StatAllocated, ClassSize);
111     Stats.sub(StatFree, ClassSize);
112     return Allocator->decompactPtr(ClassId, CompactP);
113   }
114 
deallocateSizeClassAllocatorLocalCache115   void deallocate(uptr ClassId, void *P) {
116     CHECK_LT(ClassId, NumClasses);
117     PerClass *C = &PerClassArray[ClassId];
118     // We still have to initialize the cache in the event that the first heap
119     // operation in a thread is a deallocation.
120     initCacheMaybe(C);
121     if (C->Count == C->MaxCount)
122       drain(C, ClassId);
123     // See comment in allocate() about memory accesses.
124     const uptr ClassSize = C->ClassSize;
125     C->Chunks[C->Count++] =
126         Allocator->compactPtr(ClassId, reinterpret_cast<uptr>(P));
127     Stats.sub(StatAllocated, ClassSize);
128     Stats.add(StatFree, ClassSize);
129   }
130 
isEmptySizeClassAllocatorLocalCache131   bool isEmpty() const {
132     for (uptr I = 0; I < NumClasses; ++I)
133       if (PerClassArray[I].Count)
134         return false;
135     return true;
136   }
137 
drainSizeClassAllocatorLocalCache138   void drain() {
139     // Drain BatchClassId last as createBatch can refill it.
140     for (uptr I = 0; I < NumClasses; ++I) {
141       if (I == BatchClassId)
142         continue;
143       while (PerClassArray[I].Count > 0)
144         drain(&PerClassArray[I], I);
145     }
146     while (PerClassArray[BatchClassId].Count > 0)
147       drain(&PerClassArray[BatchClassId], BatchClassId);
148     DCHECK(isEmpty());
149   }
150 
createBatchSizeClassAllocatorLocalCache151   TransferBatch *createBatch(uptr ClassId, void *B) {
152     if (ClassId != BatchClassId)
153       B = allocate(BatchClassId);
154     if (UNLIKELY(!B))
155       reportOutOfMemory(SizeClassAllocator::getSizeByClassId(BatchClassId));
156     return reinterpret_cast<TransferBatch *>(B);
157   }
158 
createGroupSizeClassAllocatorLocalCache159   BatchGroup *createGroup() {
160     void *Ptr = allocate(BatchClassId);
161     if (UNLIKELY(!Ptr))
162       reportOutOfMemory(SizeClassAllocator::getSizeByClassId(BatchClassId));
163     return reinterpret_cast<BatchGroup *>(Ptr);
164   }
165 
getStatsSizeClassAllocatorLocalCache166   LocalStats &getStats() { return Stats; }
167 
168 private:
169   static const uptr NumClasses = SizeClassMap::NumClasses;
170   static const uptr BatchClassId = SizeClassMap::BatchClassId;
alignasSizeClassAllocatorLocalCache171   struct alignas(SCUDO_CACHE_LINE_SIZE) PerClass {
172     u16 Count;
173     u16 MaxCount;
174     // Note: ClassSize is zero for the transfer batch.
175     uptr ClassSize;
176     CompactPtrT Chunks[2 * TransferBatch::MaxNumCached];
177   };
178   PerClass PerClassArray[NumClasses] = {};
179   LocalStats Stats;
180   SizeClassAllocator *Allocator = nullptr;
181 
initCacheMaybeSizeClassAllocatorLocalCache182   ALWAYS_INLINE void initCacheMaybe(PerClass *C) {
183     if (LIKELY(C->MaxCount))
184       return;
185     initCache();
186     DCHECK_NE(C->MaxCount, 0U);
187   }
188 
initCacheSizeClassAllocatorLocalCache189   NOINLINE void initCache() {
190     for (uptr I = 0; I < NumClasses; I++) {
191       PerClass *P = &PerClassArray[I];
192       const uptr Size = SizeClassAllocator::getSizeByClassId(I);
193       P->MaxCount = static_cast<u16>(2 * TransferBatch::getMaxCached(Size));
194       if (I != BatchClassId) {
195         P->ClassSize = Size;
196       } else {
197         // ClassSize in this struct is only used for malloc/free stats, which
198         // should only track user allocations, not internal movements.
199         P->ClassSize = 0;
200       }
201     }
202   }
203 
destroyBatchSizeClassAllocatorLocalCache204   void destroyBatch(uptr ClassId, void *B) {
205     if (ClassId != BatchClassId)
206       deallocate(BatchClassId, B);
207   }
208 
refillSizeClassAllocatorLocalCache209   NOINLINE bool refill(PerClass *C, uptr ClassId) {
210     initCacheMaybe(C);
211     TransferBatch *B = Allocator->popBatch(this, ClassId);
212     if (UNLIKELY(!B))
213       return false;
214     DCHECK_GT(B->getCount(), 0);
215     C->Count = B->getCount();
216     B->copyToArray(C->Chunks);
217     B->clear();
218     destroyBatch(ClassId, B);
219     return true;
220   }
221 
drainSizeClassAllocatorLocalCache222   NOINLINE void drain(PerClass *C, uptr ClassId) {
223     const u16 Count = Min(static_cast<u16>(C->MaxCount / 2), C->Count);
224     Allocator->pushBlocks(this, ClassId, &C->Chunks[0], Count);
225     // u16 will be promoted to int by arithmetic type conversion.
226     C->Count = static_cast<u16>(C->Count - Count);
227     for (u16 I = 0; I < C->Count; I++)
228       C->Chunks[I] = C->Chunks[I + Count];
229   }
230 };
231 
232 } // namespace scudo
233 
234 #endif // SCUDO_LOCAL_CACHE_H_
235