Skip to content

Commit d40ae14

Browse files
authored
Merge pull request #306 from LLNL/hotfix/object_cache_free
Bugfix in freeing object cache data
2 parents 8df4db7 + dec2272 commit d40ae14

File tree

5 files changed

+191
-59
lines changed

5 files changed

+191
-59
lines changed

example/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ add_metall_executable(allocator_aware_type allocator_aware_type.cpp)
3636

3737
add_metall_executable(logger logger.cpp)
3838

39+
add_metall_executable(concurrent concurrent.cpp)
40+
3941
if (BUILD_C)
4042
add_c_executable(c_api c_api.c)
4143
target_link_libraries(c_api PRIVATE metall_c)

example/concurrent.cpp

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright 2023 Lawrence Livermore National Security, LLC and other Metall
2+
// Project Developers. See the top-level COPYRIGHT file for details.
3+
//
4+
// SPDX-License-Identifier: (Apache-2.0 OR MIT)
5+
6+
/// \file concurrent.cpp
7+
/// \brief This example demonstrates Metall's concurrency support.
8+
/// Metall can be used in a multi-threaded environment.
9+
/// Please see the API documentation of the manager class to find out which
10+
/// functions are thread-safe.
11+
12+
#include <iostream>
13+
#include <thread>
14+
15+
#include <metall/metall.hpp>
16+
17+
void metall_alloc(metall::manager& manager, const int tid) {
18+
for (int i = 0; i < 10; ++i) {
19+
if (tid % 2 == 0) {
20+
manager.deallocate(manager.allocate(10));
21+
} else {
22+
manager.destroy_ptr(manager.construct<int>(metall::anonymous_instance)());
23+
}
24+
}
25+
}
26+
27+
int main() {
28+
{
29+
metall::manager manager(metall::create_only, "/tmp/datastore");
30+
31+
{
32+
std::thread t1(metall_alloc, std::ref(manager), 1);
33+
std::thread t2(metall_alloc, std::ref(manager), 2);
34+
std::thread t3(metall_alloc, std::ref(manager), 3);
35+
std::thread t4(metall_alloc, std::ref(manager), 4);
36+
37+
t1.join();
38+
t2.join();
39+
t3.join();
40+
t4.join();
41+
}
42+
assert(manager.check_sanity());
43+
assert(manager.all_memory_deallocated());
44+
}
45+
46+
return 0;
47+
}

example/concurrent_map.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
// Copyright 2023 Lawrence Livermore National Security, LLC and other Metall
2+
// Project Developers. See the top-level COPYRIGHT file for details.
13
//
2-
// Created by Iwabuchi, Keita on 9/25/20.
3-
//
4+
// SPDX-License-Identifier: (Apache-2.0 OR MIT)
5+
46

57
#include <iostream>
68
#include <thread>

include/metall/kernel/object_cache.hpp

Lines changed: 69 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -38,19 +38,17 @@ namespace mdtl = metall::mtlldetail;
3838

3939
namespace obcdetail {
4040

41-
/// A cache block is a unit of memory that contains cached objects
42-
/// (specifically, a cached object is difference_type).
43-
/// Cache blocks are members of two linked-lists.
44-
/// One is a linked-list of all cache blocks in the cache.
45-
/// The other is a linked-list of cache blocks in the same bin.
46-
/// The linked-lists are used to manage the order of cache blocks.
47-
/// The order of cache blocks is used to determine which cache block is
48-
/// evicted when the cache is full.
41+
/// A cache block contains offsets of cached objects of the same bin (object
42+
/// size). The maximum number of objects in a cache block is 'k_capacity'. Cache
43+
/// blocks compose two double-linked lists: 1) a linked list of cache blocks of
44+
/// the same bin. 2) a linked list of cache blocks of any bin.
4945
template <typename difference_type, typename bin_no_type>
5046
struct cache_block {
5147
static constexpr unsigned int k_capacity = 64;
5248

49+
// Disable them to avoid unexpected calls.
5350
cache_block() = delete;
51+
~cache_block() = delete;
5452

5553
inline void clear() {
5654
bin_no = std::numeric_limits<bin_no_type>::max();
@@ -96,10 +94,10 @@ struct cache_block {
9694
difference_type cache[k_capacity];
9795
};
9896

99-
/// A bin header is a unit of memory that contains information about a bin
100-
/// within a cache. Specifically, it contains the active block and the number of
101-
/// objects in the active block. The active block is the block that is currently
102-
/// used to cache objects. Non-active blocks are always full.
97+
/// A bin header contains a pointer to the active block of the corresponding bin
98+
/// and the number of objects in the active block.
99+
/// Inserting and removing objects are done only to the active block. Non-active
100+
/// blocks are always full.
103101
template <typename difference_type, typename bin_no_type>
104102
class bin_header {
105103
public:
@@ -112,7 +110,7 @@ class bin_header {
112110
m_active_block = nullptr;
113111
}
114112

115-
// Move the active block to the next block
113+
// Move the active block to the next (older) block
116114
inline void move_to_next_active_block() {
117115
if (!m_active_block) return;
118116
m_active_block = m_active_block->bin_older_block;
@@ -147,14 +145,12 @@ class bin_header {
147145
const cache_block_type *m_active_block{nullptr};
148146
};
149147

150-
/// A free blocks list is a linked-list of free blocks.
151-
/// It is used to manage free blocks.
152-
/// Cache blocks are located in a contiguous memory region.
153-
/// All cache blocks are uninitialized at the beginning ---
154-
/// thus, they do not consume physical memory.
155-
/// This free list is designed such that it does not touch uninitialized blocks
156-
/// until they are used. This design is crucial to reduce Metall manager
157-
/// construction time.
148+
/// A free blocks list contains a linked-list of free blocks.
149+
/// This class assumes that A) bocks are located in a contiguous memory region
150+
/// and B) all cache blocks are uninitialized at the beginning so that they do
151+
/// not consume physical memory. This free list is designed such that it does
152+
/// not touch uninitialized blocks until they are used. This design is crucial
153+
/// to reduce Metall manager construction time.
158154
template <typename difference_type, typename bin_no_type>
159155
class free_blocks_list {
160156
public:
@@ -207,14 +203,13 @@ class free_blocks_list {
207203
// Blocks that were used and became empty
208204
const cache_block_type *m_blocks;
209205
// The top block of the uninitialized blocks.
210-
// Uninitialized blocks are located in a contiguous memory region.
211206
const cache_block_type *m_uninit_top;
212207
const cache_block_type *m_last_block;
213208
};
214209

215-
/// A cache header is a unit of memory that contains information about a cache.
216-
/// Specifically, it contains the total size of objects in the cache,
217-
/// the oldest and newest active blocks, and a free blocks list.
210+
/// A cache header contains some metadata of a single cache.
211+
/// Specifically, it contains the total size (byte) of objects in the cache,
212+
/// the pointers to the oldest and the newest blocks, and a free blocks list.
218213
template <typename difference_type, typename bin_no_type>
219214
struct cache_header {
220215
private:
@@ -231,24 +226,24 @@ struct cache_header {
231226

232227
void clear() {
233228
m_total_size_byte = 0;
234-
m_oldest_active_block = nullptr;
235-
m_newest_active_block = nullptr;
229+
m_oldest_block = nullptr;
230+
m_newest_block = nullptr;
236231
m_free_blocks.clear();
237232
}
238233

239234
inline void unregister(const cache_block_type *const block) {
240-
if (block == m_newest_active_block) {
241-
m_newest_active_block = block->older_block;
235+
if (block == m_newest_block) {
236+
m_newest_block = block->older_block;
242237
}
243-
if (block == m_oldest_active_block) {
244-
m_oldest_active_block = block->newer_block;
238+
if (block == m_oldest_block) {
239+
m_oldest_block = block->newer_block;
245240
}
246241
}
247242

248243
inline void register_new_block(const cache_block_type *const block) {
249-
m_newest_active_block = block;
250-
if (!m_oldest_active_block) {
251-
m_oldest_active_block = block;
244+
m_newest_block = block;
245+
if (!m_oldest_block) {
246+
m_oldest_block = block;
252247
}
253248
}
254249

@@ -258,20 +253,20 @@ struct cache_header {
258253
return m_total_size_byte;
259254
}
260255

261-
inline cache_block_type *newest_active_block() noexcept {
262-
return const_cast<cache_block_type *>(m_newest_active_block);
256+
inline cache_block_type *newest_block() noexcept {
257+
return const_cast<cache_block_type *>(m_newest_block);
263258
}
264259

265-
inline const cache_block_type *newest_active_block() const noexcept {
266-
return m_newest_active_block;
260+
inline const cache_block_type *newest_block() const noexcept {
261+
return m_newest_block;
267262
}
268263

269-
inline cache_block_type *oldest_active_block() noexcept {
270-
return const_cast<cache_block_type *>(m_oldest_active_block);
264+
inline cache_block_type *oldest_block() noexcept {
265+
return const_cast<cache_block_type *>(m_oldest_block);
271266
}
272267

273-
inline const cache_block_type *oldest_active_block() const noexcept {
274-
return m_oldest_active_block;
268+
inline const cache_block_type *oldest_block() const noexcept {
269+
return m_oldest_block;
275270
}
276271

277272
inline free_blocks_list_type &free_blocks() noexcept { return m_free_blocks; }
@@ -282,20 +277,28 @@ struct cache_header {
282277

283278
private:
284279
std::size_t m_total_size_byte;
285-
const cache_block_type *m_oldest_active_block{nullptr};
286-
const cache_block_type *m_newest_active_block{nullptr};
280+
const cache_block_type *m_oldest_block{nullptr};
281+
const cache_block_type *m_newest_block{nullptr};
287282
free_blocks_list_type m_free_blocks;
288283
};
289284

290-
/// A cache container is a unit of memory that contains all data structures that
291-
/// constitute a cache.
285+
/// A cache container contains all data that constitute a cache.
286+
/// This cache container holds a cache header, bin headers, and cache blocks in
287+
/// a contiguous memory region.
292288
template <typename difference_type, typename bin_no_type,
293289
std::size_t max_bin_no, std::size_t num_blocks_per_cache>
294290
struct cache_container {
295291
using cache_heaer_type = cache_header<difference_type, bin_no_type>;
296292
using bin_header_type = bin_header<difference_type, bin_no_type>;
297293
using cacbe_block_type = cache_block<difference_type, bin_no_type>;
298294

295+
// Disable the default constructor to avoid unexpected initialization.
296+
cache_container() = delete;
297+
298+
// Disable the copy constructor to avoid unexpected destructor call.
299+
~cache_container() = delete;
300+
301+
// This data structure must be initialized first using this function.
299302
void init() {
300303
new (&header) cache_heaer_type(blocks, num_blocks_per_cache);
301304
// Memo: The in-place an array construction may not be supported by some
@@ -381,7 +384,12 @@ inline constexpr std::size_t comp_num_blocks_per_cache(
381384

382385
} // namespace obcdetail
383386

384-
/// \brief A cache for small objects.
387+
/// A cache for small objects.
388+
/// This class manages per-'CPU' (i.e., CPU-core rather than 'socket') caches
389+
/// internally. Actually stored data is the offsets of cached objects. This
390+
/// cache push and pop objects using a LIFO policy. When the cache is full
391+
/// (exceeds a pre-defined threshold), it deallocates some oldest objects first
392+
/// before caching new ones.
385393
template <typename _size_type, typename _difference_type,
386394
typename _bin_no_manager, typename _object_allocator_type>
387395
class object_cache {
@@ -469,7 +477,7 @@ class object_cache {
469477
object_cache &operator=(const object_cache &) = default;
470478
object_cache &operator=(object_cache &&) noexcept = default;
471479

472-
/// \brief Pop an object offset from the cache.
480+
/// Pop an object offset from the cache.
473481
/// If the cache is empty, allocate objects and cache them first.
474482
difference_type pop(const bin_no_type bin_no,
475483
object_allocator_type *const allocator_instance,
@@ -479,8 +487,8 @@ class object_cache {
479487
deallocator_function);
480488
}
481489

482-
/// \brief Cache an object.
483-
/// If the cache is full, deallocate some cached objects first.
490+
/// Cache an object.
491+
/// If the cache is full, deallocate some oldest cached objects first.
484492
/// Return false if an error occurs.
485493
bool push(const bin_no_type bin_no, const difference_type object_offset,
486494
object_allocator_type *const allocator_instance,
@@ -489,7 +497,7 @@ class object_cache {
489497
deallocator_function);
490498
}
491499

492-
/// \brief Clear all cached objects.
500+
/// Clear all cached objects.
493501
/// Cached objects are going to be deallocated.
494502
void clear(object_allocator_type *const allocator_instance,
495503
object_deallocate_func_type deallocator_function) {
@@ -544,6 +552,10 @@ class object_cache {
544552
}
545553

546554
private:
555+
struct free_deleter {
556+
void operator()(void *const p) const noexcept { std::free(p); }
557+
};
558+
547559
inline static unsigned int priv_get_num_cpus() {
548560
return mdtl::get_num_cpus();
549561
}
@@ -565,7 +577,7 @@ class object_cache {
565577
#endif
566578
}
567579

568-
/// \brief Get CPU number.
580+
/// Get CPU number.
569581
/// This function does not call the system call every time as it is slow.
570582
inline static size_type priv_get_cpu_no() {
571583
#ifdef METALL_DISABLE_CONCURRENCY
@@ -646,7 +658,7 @@ class object_cache {
646658
new_block->cache);
647659

648660
// Link the new block to the existing blocks
649-
new_block->link_to_older(cache_header.newest_active_block(),
661+
new_block->link_to_older(cache_header.newest_block(),
650662
bin_header.active_block());
651663

652664
// Update headers
@@ -697,7 +709,7 @@ class object_cache {
697709
assert(free_block);
698710
free_block->clear();
699711
free_block->bin_no = bin_no;
700-
free_block->link_to_older(cache_header.newest_active_block(),
712+
free_block->link_to_older(cache_header.newest_block(),
701713
bin_header.active_block());
702714
cache_header.register_new_block(free_block);
703715
bin_header.update_active_block(free_block, 0);
@@ -726,7 +738,7 @@ class object_cache {
726738
// Make sure that the cache has enough space to allocate objects.
727739
while (total_size + new_objects_size > k_max_per_cpu_cache_size ||
728740
free_blocks.empty()) {
729-
auto *const oldest_block = cache_header.oldest_active_block();
741+
auto *const oldest_block = cache_header.oldest_block();
730742
assert(oldest_block);
731743

732744
// Deallocate objects from the oldest block
@@ -755,10 +767,10 @@ class object_cache {
755767
#ifdef METALL_ENABLE_MUTEX_IN_OBJECT_CACHE
756768
std::vector<mutex_type> m_mutex;
757769
#endif
758-
std::unique_ptr<cache_storage_type[]> m_cache{nullptr};
770+
std::unique_ptr<cache_storage_type[], free_deleter> m_cache{nullptr};
759771
};
760772

761-
// const_bin_iterator
773+
/// An iterator to iterate over cached objects of the same bin.
762774
template <typename _size_type, typename _difference_type,
763775
typename _bin_no_manager, typename _object_allocator_type>
764776
class object_cache<_size_type, _difference_type, _bin_no_manager,

0 commit comments

Comments
 (0)