Line | Count | Source |
1 | | // Licensed to the Apache Software Foundation (ASF) under one |
2 | | // or more contributor license agreements. See the NOTICE file |
3 | | // distributed with this work for additional information |
4 | | // regarding copyright ownership. The ASF licenses this file |
5 | | // to you under the Apache License, Version 2.0 (the |
6 | | // "License"); you may not use this file except in compliance |
7 | | // with the License. You may obtain a copy of the License at |
8 | | // |
9 | | // http://www.apache.org/licenses/LICENSE-2.0 |
10 | | // |
11 | | // Unless required by applicable law or agreed to in writing, |
12 | | // software distributed under the License is distributed on an |
13 | | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
14 | | // KIND, either express or implied. See the License for the |
15 | | // specific language governing permissions and limitations |
16 | | // under the License. |
17 | | // This file is copied from |
18 | | // https://github.com/ClickHouse/ClickHouse/blob/master/src/Common/Arena.h |
19 | | // and modified by Doris |
20 | | |
21 | | #pragma once |
22 | | |
23 | | #include <sanitizer/asan_interface.h> |
24 | | #include <string.h> |
25 | | |
26 | | #include <boost/noncopyable.hpp> |
27 | | #include <cstdint> |
28 | | #include <limits> |
29 | | #include <memory> |
30 | | #include <vector> |
31 | | |
32 | | #include "common/compiler_util.h" |
33 | | #include "core/allocator.h" |
34 | | #include "core/allocator_fwd.h" |
35 | | |
36 | | namespace doris { |
37 | | |
38 | | /** Memory pool to append something. For example, short strings. |
39 | | * Usage scenario: |
40 | | * - put lot of strings inside pool, keep their addresses; |
41 | | * - addresses remain valid during lifetime of pool; |
42 | | * - at destruction of pool, all memory is freed; |
43 | | * - memory is allocated and freed by large chunks; |
44 | | * - freeing parts of data is not possible; |
45 | | */ |
46 | | class Arena : private boost::noncopyable { |
47 | | private: |
48 | | /// Padding allows to use 'memcpy_small_allow_read_write_overflow15' instead of 'memcpy'. |
49 | | static constexpr size_t pad_right = 15; |
50 | | |
51 | | /// Contiguous chunk of memory and pointer to free space inside it. Member of single-linked list. |
52 | | struct alignas(16) Chunk : private Allocator<false> /// empty base optimization |
53 | | { |
54 | | char* begin = nullptr; |
55 | | char* pos = nullptr; |
56 | | char* end = nullptr; /// does not include padding. |
57 | | |
58 | | Chunk* prev = nullptr; |
59 | | |
60 | 641k | Chunk(size_t size_, Chunk* prev_) { |
61 | 641k | begin = reinterpret_cast<char*>(Allocator<false>::alloc(size_)); |
62 | 641k | pos = begin; |
63 | 641k | end = begin + size_ - pad_right; |
64 | 641k | prev = prev_; |
65 | | |
66 | 641k | ASAN_POISON_MEMORY_REGION(begin, size_); |
67 | 641k | } |
68 | | |
69 | 639k | ~Chunk() { |
70 | | /// We must unpoison the memory before returning to the allocator, |
71 | | /// because the allocator might not have asan integration, and the |
72 | | /// memory would stay poisoned forever. If the allocator supports |
73 | | /// asan, it will correctly poison the memory by itself. |
74 | 639k | ASAN_UNPOISON_MEMORY_REGION(begin, size()); |
75 | | |
76 | 639k | Allocator<false>::free(begin, size()); |
77 | | |
78 | 639k | if (prev) delete prev; |
79 | 639k | } |
80 | | |
81 | 2.12M | size_t size() const { return end + pad_right - begin; } |
82 | | size_t remaining() const { return end - pos; } |
83 | 119k | size_t used() const { return pos - begin; } |
84 | | }; |
85 | | |
86 | | size_t growth_factor = 2; |
87 | | size_t linear_growth_threshold = 128 * 1024 * 1024; |
88 | | |
89 | | /// Last contiguous chunk of memory. |
90 | | Chunk* head = nullptr; |
91 | | size_t size_in_bytes = 0; |
92 | | size_t _initial_size = 4096; |
93 | | // The memory used by all chunks, excluding head. |
94 | | size_t _used_size_no_head = 0; |
95 | | |
96 | 88.5k | static size_t round_up_to_page_size(size_t s) { return (s + 4096 - 1) / 4096 * 4096; } |
97 | | |
98 | | /// If chunks size is less than 'linear_growth_threshold', then use exponential growth, otherwise - linear growth |
99 | | /// (to not allocate too much excessive memory). |
100 | 88.5k | size_t next_size(size_t min_next_size) { |
101 | 88.5k | DCHECK(head != nullptr); |
102 | 88.5k | size_t size_after_grow = 0; |
103 | | |
104 | 88.5k | if (head->size() < linear_growth_threshold) { |
105 | 88.5k | size_after_grow = std::max(min_next_size, head->size() * growth_factor); |
106 | 88.5k | } else { |
107 | | // alloc_continue() combined with linear growth results in quadratic |
108 | | // behavior: we append the data by small amounts, and when it |
109 | | // doesn't fit, we create a new chunk and copy all the previous data |
110 | | // into it. The number of times we do this is directly proportional |
111 | | // to the total size of data that is going to be serialized. To make |
112 | | // the copying happen less often, round the next size up to the |
113 | | // linear_growth_threshold. |
114 | 65 | size_after_grow = |
115 | 65 | ((min_next_size + linear_growth_threshold - 1) / linear_growth_threshold) * |
116 | 65 | linear_growth_threshold; |
117 | 65 | } |
118 | | |
119 | 88.5k | assert(size_after_grow >= min_next_size); |
120 | 88.5k | return round_up_to_page_size(size_after_grow); |
121 | 88.5k | } |
122 | | |
123 | | /// Add next contiguous chunk of memory with size not less than specified. |
124 | 88.5k | void NO_INLINE _add_chunk(size_t min_size) { |
125 | 88.5k | DCHECK(head != nullptr); |
126 | 88.5k | _used_size_no_head += head->used(); |
127 | 88.5k | head = new Chunk(next_size(min_size + pad_right), head); |
128 | 88.5k | size_in_bytes += head->size(); |
129 | 88.5k | } |
130 | | |
131 | 29.9M | void _init_head_if_needed() { |
132 | 29.9M | if (UNLIKELY(head == nullptr)) { |
133 | 552k | head = new Chunk(_initial_size, nullptr); |
134 | 552k | size_in_bytes += head->size(); |
135 | 552k | } |
136 | 29.9M | } |
137 | | |
138 | | public: |
139 | | Arena(size_t initial_size_ = 4096, size_t growth_factor_ = 2, |
140 | | size_t linear_growth_threshold_ = 128 * 1024 * 1024) |
141 | 11.6M | : growth_factor(growth_factor_), |
142 | 11.6M | linear_growth_threshold(linear_growth_threshold_), |
143 | 11.6M | _initial_size(initial_size_), |
144 | 11.6M | _used_size_no_head(0) {} |
145 | | |
146 | 11.6M | ~Arena() { delete head; } |
147 | | |
148 | | /// Get piece of memory, without alignment. |
149 | 29.7M | char* alloc(size_t size) { |
150 | 29.7M | _init_head_if_needed(); |
151 | | |
152 | 29.7M | if (UNLIKELY(head->pos + size > head->end)) { |
153 | 88.3k | _add_chunk(size); |
154 | 88.3k | } |
155 | | |
156 | 29.7M | char* res = head->pos; |
157 | 29.7M | head->pos += size; |
158 | 29.7M | ASAN_UNPOISON_MEMORY_REGION(res, size + pad_right); |
159 | 29.7M | return res; |
160 | 29.7M | } |
161 | | |
162 | | /// Get piece of memory with alignment |
163 | 211k | char* aligned_alloc(size_t size, size_t alignment) { |
164 | 211k | _init_head_if_needed(); |
165 | | |
166 | 211k | do { |
167 | 211k | void* head_pos = head->pos; |
168 | 211k | size_t space = head->end - head->pos; |
169 | | |
170 | 211k | auto res = static_cast<char*>(std::align(alignment, size, head_pos, space)); |
171 | 211k | if (res) { |
172 | 211k | head->pos = static_cast<char*>(head_pos); |
173 | 211k | head->pos += size; |
174 | 211k | ASAN_UNPOISON_MEMORY_REGION(res, size + pad_right); |
175 | 211k | return res; |
176 | 211k | } |
177 | | |
178 | 18.4E | _add_chunk(size + alignment); |
179 | 18.4E | } while (true); |
180 | 211k | } |
181 | | |
182 | | template <typename T> |
183 | 41.2k | T* alloc() { |
184 | 41.2k | return reinterpret_cast<T*>(aligned_alloc(sizeof(T), alignof(T))); |
185 | 41.2k | } |
186 | | |
187 | | /** Rollback just performed allocation. |
188 | | * Must pass size not more that was just allocated. |
189 | | * Return the resulting head pointer, so that the caller can assert that |
190 | | * the allocation it intended to roll back was indeed the last one. |
191 | | */ |
192 | 11 | void* rollback(size_t size) { |
193 | 11 | DCHECK(head != nullptr); |
194 | | |
195 | 11 | head->pos -= size; |
196 | 11 | ASAN_POISON_MEMORY_REGION(head->pos, size + pad_right); |
197 | 11 | return head->pos; |
198 | 11 | } |
199 | | |
200 | | /** Begin or expand a contiguous range of memory. |
201 | | * 'range_start' is the start of range. If nullptr, a new range is |
202 | | * allocated. |
203 | | * If there is no space in the current chunk to expand the range, |
204 | | * the entire range is copied to a new, bigger memory chunk, and the value |
205 | | * of 'range_start' is updated. |
206 | | * If the optional 'start_alignment' is specified, the start of range is |
207 | | * kept aligned to this value. |
208 | | * |
209 | | * NOTE This method is usable only for the last allocation made on this |
210 | | * Arena. For earlier allocations, see 'realloc' method. |
211 | | */ |
212 | | [[nodiscard]] char* alloc_continue(size_t additional_bytes, char const*& range_start, |
213 | 33.4k | size_t start_alignment = 0) { |
214 | 33.4k | if (!range_start) { |
215 | | // Start a new memory range. |
216 | 8.43k | char* result = start_alignment ? aligned_alloc(additional_bytes, start_alignment) |
217 | 8.43k | : alloc(additional_bytes); |
218 | | |
219 | 8.43k | range_start = result; |
220 | 8.43k | return result; |
221 | 8.43k | } |
222 | | |
223 | 33.4k | DCHECK(head != nullptr); |
224 | | |
225 | | // Extend an existing memory range with 'additional_bytes'. |
226 | | |
227 | | // This method only works for extending the last allocation. For lack of |
228 | | // original size, check a weaker condition: that 'begin' is at least in |
229 | | // the current Chunk. |
230 | 24.9k | assert(range_start >= head->begin && range_start < head->end); |
231 | | |
232 | 24.9k | if (head->pos + additional_bytes <= head->end) { |
233 | | // The new size fits into the last chunk, so just alloc the |
234 | | // additional size. We can alloc without alignment here, because it |
235 | | // only applies to the start of the range, and we don't change it. |
236 | 24.9k | return alloc(additional_bytes); |
237 | 24.9k | } |
238 | | |
239 | | // New range doesn't fit into this chunk, will copy to a new one. |
240 | | // |
241 | | // Note: among other things, this method is used to provide a hack-ish |
242 | | // implementation of realloc over Arenas in ArenaAllocators. It wastes a |
243 | | // lot of memory -- quadratically so when we reach the linear allocation |
244 | | // threshold. This deficiency is intentionally left as is, and should be |
245 | | // solved not by complicating this method, but by rethinking the |
246 | | // approach to memory management for aggregate function states, so that |
247 | | // we can provide a proper realloc(). |
248 | 44 | const size_t existing_bytes = head->pos - range_start; |
249 | 44 | const size_t new_bytes = existing_bytes + additional_bytes; |
250 | 44 | const char* old_range = range_start; |
251 | | |
252 | 44 | char* new_range = |
253 | 44 | start_alignment ? aligned_alloc(new_bytes, start_alignment) : alloc(new_bytes); |
254 | | |
255 | 44 | memcpy(new_range, old_range, existing_bytes); |
256 | | |
257 | 44 | range_start = new_range; |
258 | 44 | return new_range + existing_bytes; |
259 | 24.9k | } |
260 | | |
261 | | /// NOTE Old memory region is wasted. |
262 | 0 | [[nodiscard]] char* realloc(const char* old_data, size_t old_size, size_t new_size) { |
263 | 0 | char* res = alloc(new_size); |
264 | 0 | if (old_data) { |
265 | 0 | memcpy(res, old_data, old_size); |
266 | 0 | ASAN_POISON_MEMORY_REGION(old_data, old_size); |
267 | 0 | } |
268 | 0 | return res; |
269 | 0 | } |
270 | | |
271 | | [[nodiscard]] char* aligned_realloc(const char* old_data, size_t old_size, size_t new_size, |
272 | 0 | size_t alignment) { |
273 | 0 | char* res = aligned_alloc(new_size, alignment); |
274 | 0 | if (old_data) { |
275 | 0 | memcpy(res, old_data, old_size); |
276 | 0 | ASAN_POISON_MEMORY_REGION(old_data, old_size); |
277 | 0 | } |
278 | 0 | return res; |
279 | 0 | } |
280 | | |
281 | | /// Insert string without alignment. |
282 | 3.09M | [[nodiscard]] const char* insert(const char* data, size_t size) { |
283 | 3.09M | char* res = alloc(size); |
284 | 3.09M | memcpy(res, data, size); |
285 | 3.09M | return res; |
286 | 3.09M | } |
287 | | |
288 | 0 | [[nodiscard]] const char* aligned_insert(const char* data, size_t size, size_t alignment) { |
289 | 0 | char* res = aligned_alloc(size, alignment); |
290 | 0 | memcpy(res, data, size); |
291 | 0 | return res; |
292 | 0 | } |
293 | | |
294 | | /** |
295 | | * Delete all the chunks before the head, usually the head is the largest chunk in the arena. |
296 | | * considering the scenario of memory reuse: |
297 | | * 1. first time, use arena alloc 64K memory, 4K each time, at this time, there are 4 chunks of 4k 8k 16k 32k in arena. |
298 | | * 2. then, clear arena, only one 32k chunk left in the arena. |
299 | | * 3. second time, same alloc 64K memory, there are 4 chunks of 4k 8k 16k 32k in arena. |
300 | | * 4. then, clear arena, only one 64k chunk left in the arena. |
301 | | * 5. third time, same alloc 64K memory, there is still only one 64K chunk in the arena, and the memory is fully reused. |
302 | | * |
303 | | * special case: if the chunk is larger than 128M, it will no longer be expanded by a multiple of 2. |
304 | | * If alloc 4G memory, 128M each time, then only one 128M chunk will be reserved after clearing, |
305 | | * and only 128M can be reused when you apply for 4G memory again. |
306 | | */ |
307 | 1.64M | void clear(bool delete_head = false) { |
308 | 1.64M | if (head == nullptr) { |
309 | 1.61M | return; |
310 | 1.61M | } |
311 | | |
312 | 28.3k | if (head->prev) { |
313 | 672 | delete head->prev; |
314 | 672 | head->prev = nullptr; |
315 | 672 | } |
316 | 28.3k | if (delete_head) { |
317 | 4.19k | delete head; |
318 | 4.19k | head = nullptr; |
319 | 4.19k | size_in_bytes = 0; |
320 | 24.1k | } else { |
321 | 24.1k | head->pos = head->begin; |
322 | 24.1k | size_in_bytes = head->size(); |
323 | 24.1k | } |
324 | 28.3k | _used_size_no_head = 0; |
325 | 28.3k | } |
326 | | |
327 | | /// Size of chunks in bytes. |
328 | 320k | size_t size() const { return size_in_bytes; } |
329 | | |
330 | 43.7k | size_t used_size() const { |
331 | 43.7k | if (head == nullptr) { |
332 | 13.0k | return _used_size_no_head; |
333 | 13.0k | } |
334 | | |
335 | 30.7k | return _used_size_no_head + head->used(); |
336 | 43.7k | } |
337 | | |
338 | 650 | bool contains(const void* data, size_t size) const { |
339 | 650 | const auto address = reinterpret_cast<uintptr_t>(data); |
340 | 650 | if (data == nullptr || size > std::numeric_limits<uintptr_t>::max() - address) { |
341 | 0 | return false; |
342 | 0 | } |
343 | 650 | const auto range_end = address + size; |
344 | 993 | for (const Chunk* chunk = head; chunk != nullptr; chunk = chunk->prev) { |
345 | 621 | const auto chunk_begin = reinterpret_cast<uintptr_t>(chunk->begin); |
346 | 621 | const auto chunk_used_end = reinterpret_cast<uintptr_t>(chunk->pos); |
347 | 621 | if (address >= chunk_begin && range_end <= chunk_used_end) { |
348 | 278 | return true; |
349 | 278 | } |
350 | 621 | } |
351 | 372 | return false; |
352 | 650 | } |
353 | | |
354 | | size_t remaining_space_in_current_chunk() const { |
355 | | if (head == nullptr) { |
356 | | return 0; |
357 | | } |
358 | | |
359 | | return head->remaining(); |
360 | | } |
361 | | }; |
362 | | |
363 | | using ArenaPtr = std::shared_ptr<Arena>; |
364 | | using Arenas = std::vector<ArenaPtr>; |
365 | | |
366 | | } // namespace doris |