Skip to content

Commit 08f8796

Browse files
laanwjNico Guiton
authored and
Nico Guiton
committed
Use best-fit strategy in Arena, now O(log(n)) instead O(n)
Summary: PR12048: 5fbf7c4 fix nits: variable naming, typos (Martin Ankerl) 1e0ee90 Use best-fit strategy in Arena, now O(log(n)) instead O(n) (Martin Ankerl) Pull request description: This replaces the first-fit algorithm used in the Arena with a best-fit. According to "Dynamic Storage Allocation: A Survey and Critical Review", Wilson et. al. 1995, http://www.scs.stanford.edu/14wi-cs140/sched/readings/wilson.pdf, both startegies work well in practice. The advantage of using best-fit is that we can switch the O(n) allocation to O(log(n)). Additionally, some previously O(log(n)) operations are now O(1) operations by using hash maps. The end effect is that the benchmark runs about 2.5 times faster on my machine: # Benchmark, evals, iterations, total, min, max, median old: BenchLockedPool, 5, 530, 5.25749, 0.00196938, 0.00199755, 0.00198172 new: BenchLockedPool, 5, 1300, 5.11313, 0.000781493, 0.000793314, 0.00078606 I've run all unit tests and benchmarks, and increased the number of iterations so that BenchLockedPool takes about 5 seconds again. Tree-SHA512: 6551e384671f93f10c60df530a29a1954bd265cc305411f665a8756525e5afe2873a8032c797d00b6e8c07e16d9827465d0b662875433147381474a44119ccce Also included PR16161 which fixes a bug introduced by PR12048: Changes in #12048 cause a compilation error in Arena::walk() when ARENA_DEBUG is defined. Specifically, Arena's chunks_free map was changed to have a different value type. Additionally, missing includes cause other compilation errors when ARENA_DEBUG is defined. Second commit fixes segfault in allocator_tests/arena_tests The test uses reinterpret_cast<void*> on unallocated memory. Using this memory in printchunk as char* causes a segfault, so have printchunk take void* instead. Reproduced with: make CPPFLAGS=-DARENA_DEBUG Backport of Core PR12048 and PR16161 bitcoin/bitcoin#12048 bitcoin/bitcoin#16161 Reviewer note: the last commit in PR16161 is TRAVIS related and was therefore modified to our code base. Test Plan: make check test_runner.py ./bitcoin-qt -> settings -> Encrypt Wallet -> set password and/or change password if already encrypted ./bench_bitcoin old: BenchLockedPool, 5, 530, 5.15069, 0.00192059, 0.00199956, 0.00193796 new: BenchLockedPool, 5, 1300, 4.44411, 0.00067821, 0.000698535, 0.000678916 ../configure CPPFLAGS=-DARENA_DEBUG make check test_runner.py ABC_BUILD_NAME=build-asan build-configurations.sh Reviewers: deadalnix, Fabien, jasonbcox, O1 Bitcoin ABC, #bitcoin_abc Reviewed By: deadalnix, Fabien, jasonbcox, O1 Bitcoin ABC, #bitcoin_abc Differential Revision: https://reviews.bitcoinabc.org/D3898
1 parent 4941397 commit 08f8796

File tree

4 files changed

+69
-39
lines changed

4 files changed

+69
-39
lines changed

contrib/teamcity/build-configurations.sh

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ case "$ABC_BUILD_NAME" in
6161
build-asan)
6262
# Build with the address sanitizer, then run unit tests and functional tests.
6363
CMAKE_FLAGS=(
64+
"-DCMAKE_CXX_FLAGS=-DARENA_DEBUG"
6465
"-DCMAKE_BUILD_TYPE=Debug"
6566
# ASAN does not support assembly code: https://github.com/google/sanitizers/issues/192
6667
# This will trigger a segfault if the SSE4 implementation is selected for SHA256.

src/bench/lockedpool.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,4 @@ static void BenchLockedPool(benchmark::State &state) {
4141
addr.clear();
4242
}
4343

44-
BENCHMARK(BenchLockedPool, 530);
44+
BENCHMARK(BenchLockedPool, 1300);

src/support/lockedpool.cpp

+52-32
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828

2929
#include <algorithm>
3030
#include <memory>
31+
#ifdef ARENA_DEBUG
32+
#include <iomanip>
33+
#include <iostream>
34+
#endif
3135

3236
LockedPoolManager *LockedPoolManager::_instance = nullptr;
3337
std::once_flag LockedPoolManager::init_flag;
@@ -47,7 +51,9 @@ Arena::Arena(void *base_in, size_t size_in, size_t alignment_in)
4751
: base(static_cast<char *>(base_in)),
4852
end(static_cast<char *>(base_in) + size_in), alignment(alignment_in) {
4953
// Start with one free chunk that covers the entire arena
50-
chunks_free.emplace(base, size_in);
54+
auto it = size_to_free_chunk.emplace(size_in, base);
55+
chunks_free.emplace(base, it);
56+
chunks_free_end.emplace(base + size_in, it);
5157
}
5258

5359
Arena::~Arena() {}
@@ -61,33 +67,36 @@ void *Arena::alloc(size_t size) {
6167
return nullptr;
6268
}
6369

64-
// Pick a large enough free-chunk
65-
auto it =
66-
std::find_if(chunks_free.begin(), chunks_free.end(),
67-
[=](const std::map<char *, size_t>::value_type &chunk) {
68-
return chunk.second >= size;
69-
});
70-
if (it == chunks_free.end()) {
70+
// Pick a large enough free-chunk. Returns an iterator pointing to the first
71+
// element that is not less than key. This allocation strategy is best-fit.
72+
// According to "Dynamic Storage Allocation: A Survey and Critical Review",
73+
// Wilson et. al. 1995,
74+
// http://www.scs.stanford.edu/14wi-cs140/sched/readings/wilson.pdf,
75+
// best-fit and first-fit policies seem to work well in practice.
76+
auto size_ptr_it = size_to_free_chunk.lower_bound(size);
77+
if (size_ptr_it == size_to_free_chunk.end()) {
7178
return nullptr;
7279
}
7380

7481
// Create the used-chunk, taking its space from the end of the free-chunk
82+
const size_t size_remaining = size_ptr_it->first - size;
7583
auto alloced =
76-
chunks_used.emplace(it->first + it->second - size, size).first;
77-
if (!(it->second -= size)) {
78-
chunks_free.erase(it);
84+
chunks_used.emplace(size_ptr_it->second + size_remaining, size).first;
85+
chunks_free_end.erase(size_ptr_it->second + size_ptr_it->first);
86+
if (size_ptr_it->first == size) {
87+
// whole chunk is used up
88+
chunks_free.erase(size_ptr_it->second);
89+
} else {
90+
// still some memory left in the chunk
91+
auto it_remaining =
92+
size_to_free_chunk.emplace(size_remaining, size_ptr_it->second);
93+
chunks_free[size_ptr_it->second] = it_remaining;
94+
chunks_free_end.emplace(size_ptr_it->second + size_remaining,
95+
it_remaining);
7996
}
80-
return reinterpret_cast<void *>(alloced->first);
81-
}
97+
size_to_free_chunk.erase(size_ptr_it);
8298

83-
/* extend the Iterator if other begins at its end */
84-
template <class Iterator, class Pair>
85-
bool extend(Iterator it, const Pair &other) {
86-
if (it->first + it->second == other.first) {
87-
it->second += other.second;
88-
return true;
89-
}
90-
return false;
99+
return reinterpret_cast<void *>(alloced->first);
91100
}
92101

93102
void Arena::free(void *ptr) {
@@ -101,19 +110,30 @@ void Arena::free(void *ptr) {
101110
if (i == chunks_used.end()) {
102111
throw std::runtime_error("Arena: invalid or double free");
103112
}
104-
auto freed = *i;
113+
std::pair<char *, size_t> freed = *i;
105114
chunks_used.erase(i);
106115

107-
// Add space to free map, coalescing contiguous chunks
108-
auto next = chunks_free.upper_bound(freed.first);
109-
auto prev =
110-
(next == chunks_free.begin()) ? chunks_free.end() : std::prev(next);
111-
if (prev == chunks_free.end() || !extend(prev, freed)) {
112-
prev = chunks_free.emplace_hint(next, freed);
116+
// coalesce freed with previous chunk
117+
auto prev = chunks_free_end.find(freed.first);
118+
if (prev != chunks_free_end.end()) {
119+
freed.first -= prev->second->first;
120+
freed.second += prev->second->first;
121+
size_to_free_chunk.erase(prev->second);
122+
chunks_free_end.erase(prev);
113123
}
114-
if (next != chunks_free.end() && extend(prev, *next)) {
124+
125+
// coalesce freed with chunk after freed
126+
auto next = chunks_free.find(freed.first + freed.second);
127+
if (next != chunks_free.end()) {
128+
freed.second += next->second->first;
129+
size_to_free_chunk.erase(next->second);
115130
chunks_free.erase(next);
116131
}
132+
133+
// Add/set space with coalesced free chunk
134+
auto it = size_to_free_chunk.emplace(freed.second, freed.first);
135+
chunks_free[freed.first] = it;
136+
chunks_free_end[freed.first + freed.second] = it;
117137
}
118138

119139
Arena::Stats Arena::stats() const {
@@ -122,14 +142,14 @@ Arena::Stats Arena::stats() const {
122142
r.used += chunk.second;
123143
}
124144
for (const auto &chunk : chunks_free) {
125-
r.free += chunk.second;
145+
r.free += chunk.second->first;
126146
}
127147
r.total = r.used + r.free;
128148
return r;
129149
}
130150

131151
#ifdef ARENA_DEBUG
132-
static void printchunk(char *base, size_t sz, bool used) {
152+
static void printchunk(void *base, size_t sz, bool used) {
133153
std::cout << "0x" << std::hex << std::setw(16) << std::setfill('0') << base
134154
<< " 0x" << std::hex << std::setw(16) << std::setfill('0') << sz
135155
<< " 0x" << used << std::endl;
@@ -140,7 +160,7 @@ void Arena::walk() const {
140160
}
141161
std::cout << std::endl;
142162
for (const auto &chunk : chunks_free) {
143-
printchunk(chunk.first, chunk.second, false);
163+
printchunk(chunk.first, chunk.second->first, false);
144164
}
145165
std::cout << std::endl;
146166
}

src/support/lockedpool.h

+15-6
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <map>
1111
#include <memory>
1212
#include <mutex>
13+
#include <unordered_map>
1314

1415
/**
1516
* OS-dependent allocation and deallocation of locked/pinned memory pages.
@@ -92,12 +93,20 @@ class Arena {
9293
bool addressInArena(void *ptr) const { return ptr >= base && ptr < end; }
9394

9495
private:
95-
/**
96-
* Map of chunk address to chunk information. This class makes use of the
97-
* sorted order to merge previous and next chunks during deallocation.
98-
*/
99-
std::map<char *, size_t> chunks_free;
100-
std::map<char *, size_t> chunks_used;
96+
typedef std::multimap<size_t, char *> SizeToChunkSortedMap;
97+
/** Map to enable O(log(n)) best-fit allocation, as it's sorted by size */
98+
SizeToChunkSortedMap size_to_free_chunk;
99+
100+
typedef std::unordered_map<char *, SizeToChunkSortedMap::const_iterator>
101+
ChunkToSizeMap;
102+
/** Map from begin of free chunk to its node in size_to_free_chunk */
103+
ChunkToSizeMap chunks_free;
104+
/** Map from end of free chunk to its node in size_to_free_chunk */
105+
ChunkToSizeMap chunks_free_end;
106+
107+
/** Map from begin of used chunk to its size */
108+
std::unordered_map<char *, size_t> chunks_used;
109+
101110
/** Base address of arena */
102111
char *base;
103112
/** End address of arena */

0 commit comments

Comments
 (0)