Coverage Report

Created: 2026-08-01 13:18

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/core/arena.h
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
670k
        Chunk(size_t size_, Chunk* prev_) {
61
670k
            begin = reinterpret_cast<char*>(Allocator<false>::alloc(size_));
62
670k
            pos = begin;
63
670k
            end = begin + size_ - pad_right;
64
670k
            prev = prev_;
65
66
670k
            ASAN_POISON_MEMORY_REGION(begin, size_);
67
670k
        }
68
69
670k
        ~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
670k
            ASAN_UNPOISON_MEMORY_REGION(begin, size());
75
76
670k
            Allocator<false>::free(begin, size());
77
78
670k
            if (prev) delete prev;
79
670k
        }
80
81
2.21M
        size_t size() const { return end + pad_right - begin; }
82
        size_t remaining() const { return end - pos; }
83
120k
        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
89.4k
    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
89.4k
    size_t next_size(size_t min_next_size) {
101
89.4k
        DCHECK(head != nullptr);
102
89.4k
        size_t size_after_grow = 0;
103
104
89.4k
        if (head->size() < linear_growth_threshold) {
105
89.3k
            size_after_grow = std::max(min_next_size, head->size() * growth_factor);
106
89.3k
        } 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
66
            size_after_grow =
115
66
                    ((min_next_size + linear_growth_threshold - 1) / linear_growth_threshold) *
116
66
                    linear_growth_threshold;
117
66
        }
118
119
89.4k
        assert(size_after_grow >= min_next_size);
120
89.4k
        return round_up_to_page_size(size_after_grow);
121
89.4k
    }
122
123
    /// Add next contiguous chunk of memory with size not less than specified.
124
89.4k
    void NO_INLINE _add_chunk(size_t min_size) {
125
89.4k
        DCHECK(head != nullptr);
126
89.4k
        _used_size_no_head += head->used();
127
89.4k
        head = new Chunk(next_size(min_size + pad_right), head);
128
89.4k
        size_in_bytes += head->size();
129
89.4k
    }
130
131
29.8M
    void _init_head_if_needed() {
132
29.8M
        if (UNLIKELY(head == nullptr)) {
133
581k
            head = new Chunk(_initial_size, nullptr);
134
581k
            size_in_bytes += head->size();
135
581k
        }
136
29.8M
    }
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.3M
            : growth_factor(growth_factor_),
142
11.3M
              linear_growth_threshold(linear_growth_threshold_),
143
11.3M
              _initial_size(initial_size_),
144
11.3M
              _used_size_no_head(0) {}
145
146
11.3M
    ~Arena() { delete head; }
147
148
    /// Get piece of memory, without alignment.
149
29.5M
    char* alloc(size_t size) {
150
29.5M
        _init_head_if_needed();
151
152
29.5M
        if (UNLIKELY(head->pos + size > head->end)) {
153
89.2k
            _add_chunk(size);
154
89.2k
        }
155
156
29.5M
        char* res = head->pos;
157
29.5M
        head->pos += size;
158
29.5M
        ASAN_UNPOISON_MEMORY_REGION(res, size + pad_right);
159
29.5M
        return res;
160
29.5M
    }
161
162
    /// Get piece of memory with alignment
163
212k
    char* aligned_alloc(size_t size, size_t alignment) {
164
212k
        _init_head_if_needed();
165
166
212k
        do {
167
212k
            void* head_pos = head->pos;
168
212k
            size_t space = head->end - head->pos;
169
170
212k
            auto res = static_cast<char*>(std::align(alignment, size, head_pos, space));
171
213k
            if (res) {
172
213k
                head->pos = static_cast<char*>(head_pos);
173
213k
                head->pos += size;
174
213k
                ASAN_UNPOISON_MEMORY_REGION(res, size + pad_right);
175
213k
                return res;
176
213k
            }
177
178
18.4E
            _add_chunk(size + alignment);
179
18.4E
        } while (true);
180
212k
    }
181
182
    template <typename T>
183
40.9k
    T* alloc() {
184
40.9k
        return reinterpret_cast<T*>(aligned_alloc(sizeof(T), alignof(T)));
185
40.9k
    }
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.44k
            char* result = start_alignment ? aligned_alloc(additional_bytes, start_alignment)
217
8.44k
                                           : alloc(additional_bytes);
218
219
8.44k
            range_start = result;
220
8.44k
            return result;
221
8.44k
        }
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.49M
    [[nodiscard]] const char* insert(const char* data, size_t size) {
283
3.49M
        char* res = alloc(size);
284
3.49M
        memcpy(res, data, size);
285
3.49M
        return res;
286
3.49M
    }
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.71M
    void clear(bool delete_head = false) {
308
1.71M
        if (head == nullptr) {
309
1.69M
            return;
310
1.69M
        }
311
312
27.8k
        if (head->prev) {
313
639
            delete head->prev;
314
639
            head->prev = nullptr;
315
639
        }
316
27.8k
        if (delete_head) {
317
3.98k
            delete head;
318
3.98k
            head = nullptr;
319
3.98k
            size_in_bytes = 0;
320
23.8k
        } else {
321
23.8k
            head->pos = head->begin;
322
23.8k
            size_in_bytes = head->size();
323
23.8k
        }
324
27.8k
        _used_size_no_head = 0;
325
27.8k
    }
326
327
    /// Size of chunks in bytes.
328
306k
    size_t size() const { return size_in_bytes; }
329
330
43.0k
    size_t used_size() const {
331
43.0k
        if (head == nullptr) {
332
12.1k
            return _used_size_no_head;
333
12.1k
        }
334
335
30.9k
        return _used_size_no_head + head->used();
336
43.0k
    }
337
338
1.01k
    bool contains(const void* data, size_t size) const {
339
1.01k
        const auto address = reinterpret_cast<uintptr_t>(data);
340
1.01k
        if (data == nullptr || size > std::numeric_limits<uintptr_t>::max() - address) {
341
0
            return false;
342
0
        }
343
1.01k
        const auto range_end = address + size;
344
1.68k
        for (const Chunk* chunk = head; chunk != nullptr; chunk = chunk->prev) {
345
988
            const auto chunk_begin = reinterpret_cast<uintptr_t>(chunk->begin);
346
988
            const auto chunk_used_end = reinterpret_cast<uintptr_t>(chunk->pos);
347
988
            if (address >= chunk_begin && range_end <= chunk_used_end) {
348
320
                return true;
349
320
            }
350
988
        }
351
696
        return false;
352
1.01k
    }
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