Skip to content

Commit 6dbf08a

Browse files
authored
Improve post-teardown performance (#810)
This change improves the performance of post-teardown operations. (See #809) The main changes are: * Reduce time spent in flushing a thread * Back-off performing flush if a thread is used a lot during teardown. * Disable fork protection by default * Improve branch prediction for the spin lock.
1 parent 7cd1ae9 commit 6dbf08a

8 files changed

Lines changed: 106 additions & 22 deletions

File tree

CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ option(SNMALLOC_IPO "Link with IPO/LTO support" OFF)
2828
option(SNMALLOC_BENCHMARK_INDIVIDUAL_MITIGATIONS "Build tests and ld_preload for individual mitigations" OFF)
2929
option(SNMALLOC_ENABLE_DYNAMIC_LOADING "Build such that snmalloc can be dynamically loaded. This is not required for LD_PRELOAD, and will harm performance if enabled." OFF)
3030
option(SNMALLOC_ENABLE_WAIT_ON_ADDRESS "Use wait on address backoff strategy if it is available" ON)
31+
option(SNMALLOC_PTHREAD_FORK_PROTECTION "Guard against forking while allocator locks are held using pthread_atfork hooks" OFF)
3132
option(SNMALLOC_ENABLE_FUZZING "Enable fuzzing instrumentation tests" OFF)
3233
option(SNMALLOC_USE_SELF_VENDORED_STL "Avoid using system STL" OFF)
3334
# Options that apply only if we're not building the header-only library
@@ -133,6 +134,9 @@ int main() {
133134
}
134135
" SNMALLOC_PTHREAD_ATFORK_WORKS)
135136

137+
if (SNMALLOC_PTHREAD_FORK_PROTECTION AND NOT SNMALLOC_PTHREAD_ATFORK_WORKS)
138+
message(FATAL_ERROR "SNMALLOC_PTHREAD_FORK_PROTECTION requires working pthread_atfork support")
139+
endif()
136140

137141
if (NOT MSVC AND NOT (SNMALLOC_CLEANUP STREQUAL CXX11_DESTRUCTORS))
138142
# If the target compiler doesn't support -nostdlib++ then we must enable C at
@@ -333,6 +337,7 @@ endfunction()
333337
add_as_define(SNMALLOC_QEMU_WORKAROUND)
334338
add_as_define(SNMALLOC_TRACING)
335339
add_as_define(SNMALLOC_CI_BUILD)
340+
add_as_define(SNMALLOC_PTHREAD_FORK_PROTECTION)
336341
add_as_define(SNMALLOC_PLATFORM_HAS_GETENTROPY)
337342
add_as_define(SNMALLOC_PTHREAD_ATFORK_WORKS)
338343
add_as_define(SNMALLOC_HAS_LINUX_RANDOM_H)

src/snmalloc/ds_aal/flaglock.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,15 +116,16 @@ namespace snmalloc
116116
public:
117117
FlagLock(FlagWord& lock) : lock(lock)
118118
{
119-
while (lock.flag.exchange(true, stl::memory_order_acquire))
119+
while (
120+
SNMALLOC_UNLIKELY(lock.flag.exchange(true, stl::memory_order_acquire)))
120121
{
121122
// assert_not_owned_by_current_thread is only called when the first
122123
// acquiring is failed; which means the lock is already held somewhere
123124
// else.
124125
lock.assert_not_owned_by_current_thread();
125126
// This loop is better for spin-waiting because it won't issue
126127
// expensive write operation (xchg for example).
127-
while (lock.flag.load(stl::memory_order_relaxed))
128+
while (SNMALLOC_UNLIKELY(lock.flag.load(stl::memory_order_relaxed)))
128129
{
129130
Aal::pause();
130131
}

src/snmalloc/ds_aal/prevent_fork.h

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
namespace snmalloc
1212
{
13+
14+
#ifdef SNMALLOC_PTHREAD_FORK_PROTECTION
1315
// This is a simple implementation of a class that can be
1416
// used to prevent a process from forking. Holding a lock
1517
// in the allocator while forking can lead to deadlocks.
@@ -43,15 +45,15 @@ namespace snmalloc
4345
// calls would be ignored.
4446
static void ensure_init()
4547
{
46-
#ifdef SNMALLOC_PTHREAD_ATFORK_WORKS
48+
# ifdef SNMALLOC_PTHREAD_ATFORK_WORKS
4749
static stl::Atomic<bool> initialised{false};
4850

4951
if (initialised.load(stl::memory_order_acquire))
5052
return;
5153

5254
pthread_atfork(prefork, postfork_parent, postfork_child);
5355
initialised.store(true, stl::memory_order_release);
54-
#endif
56+
# endif
5557
};
5658

5759
public:
@@ -158,4 +160,15 @@ namespace snmalloc
158160
threads_preventing_fork--;
159161
}
160162
};
163+
#else
164+
// The fork protection can cost a lot and it is generally not required.
165+
// This is a dummy implementation of the PreventFork class that does nothing.
166+
class PreventFork
167+
{
168+
public:
169+
PreventFork() {}
170+
171+
~PreventFork() {}
172+
};
173+
#endif
161174
} // namespace snmalloc

src/snmalloc/global/threadalloc.h

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ namespace snmalloc
9393
// we need to record if we are already in that state as we will not
9494
// receive another teardown call, so each operation needs to release
9595
// the underlying data structures after the call.
96-
static inline thread_local bool teardown_called{false};
96+
static inline thread_local size_t times_teardown_called{0};
9797

9898
public:
9999
/**
@@ -114,8 +114,9 @@ namespace snmalloc
114114
if (alloc == &default_alloc)
115115
return;
116116

117-
teardown_called = true;
118-
alloc->flush();
117+
times_teardown_called++;
118+
if (bits::is_pow2(times_teardown_called) || times_teardown_called < 128)
119+
alloc->flush();
119120
AllocPool<Config>::release(alloc);
120121
alloc = const_cast<Alloc*>(&default_alloc);
121122
}
@@ -131,7 +132,7 @@ namespace snmalloc
131132
template<typename Restart, typename... Args>
132133
SNMALLOC_SLOW_PATH static auto check_init_slow(Restart r, Args... args)
133134
{
134-
bool post_teardown = teardown_called;
135+
bool post_teardown = times_teardown_called > 0;
135136

136137
alloc = AllocPool<Config>::acquire();
137138

src/snmalloc/mem/corealloc.h

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,6 +1169,10 @@ namespace snmalloc
11691169
template<bool check_slabs = false>
11701170
SNMALLOC_SLOW_PATH void dealloc_local_slabs(smallsizeclass_t sizeclass)
11711171
{
1172+
if constexpr (!check_slabs)
1173+
if (alloc_classes[sizeclass].unused == 0)
1174+
return;
1175+
11721176
// Return unused slabs of sizeclass_t back to global allocator
11731177
alloc_classes[sizeclass].available.iterate([this, sizeclass](auto* meta) {
11741178
auto domesticate =
@@ -1420,18 +1424,20 @@ namespace snmalloc
14201424
for (smallsizeclass_t sizeclass = 0; sizeclass < NUM_SMALL_SIZECLASSES;
14211425
sizeclass++)
14221426
{
1423-
dealloc_local_slabs<true>(sizeclass);
1427+
dealloc_local_slabs<mitigations(freelist_teardown_validate)>(sizeclass);
14241428
}
14251429

1426-
laden.iterate(
1427-
[domesticate](BackendSlabMetadata* meta) SNMALLOC_FAST_PATH_LAMBDA {
1428-
if (!meta->is_large())
1429-
{
1430-
meta->free_queue.validate(
1431-
freelist::Object::key_root, meta->as_key_tweak(), domesticate);
1432-
}
1433-
});
1434-
1430+
if constexpr (mitigations(freelist_teardown_validate))
1431+
{
1432+
laden.iterate(
1433+
[domesticate](BackendSlabMetadata* meta) SNMALLOC_FAST_PATH_LAMBDA {
1434+
if (!meta->is_large())
1435+
{
1436+
meta->free_queue.validate(
1437+
freelist::Object::key_root, meta->as_key_tweak(), domesticate);
1438+
}
1439+
});
1440+
}
14351441
// Set the remote_dealloc_cache to immediately slow path.
14361442
remote_dealloc_cache.capacity = 0;
14371443

src/snmalloc/mem/pooled.h

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,21 @@ namespace snmalloc
5151
public:
5252
void set_in_use()
5353
{
54-
if (in_use.exchange(true))
54+
#ifndef NDEBUG
55+
if (in_use.exchange(true, stl::memory_order_acq_rel))
5556
error("Critical error: double use of Pooled Type!");
57+
#else
58+
in_use.store(true, stl::memory_order_relaxed);
59+
#endif
5660
}
5761

5862
void reset_in_use()
5963
{
60-
in_use.store(false);
64+
#ifndef NDEBUG
65+
in_use.store(false, stl::memory_order_release);
66+
#else
67+
in_use.store(false, stl::memory_order_relaxed);
68+
#endif
6169
}
6270

6371
bool debug_is_in_use()

src/test/func/protect_fork/protect_fork.cc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
#include <iostream>
2-
#include <snmalloc/snmalloc.h>
3-
42
#ifndef SNMALLOC_PTHREAD_ATFORK_WORKS
53
int main()
64
{
@@ -9,7 +7,9 @@ int main()
97
}
108
#else
119

10+
# define SNMALLOC_PTHREAD_FORK_PROTECTION
1211
# include <pthread.h>
12+
# include <snmalloc/snmalloc.h>
1313
# include <thread>
1414

1515
void simulate_allocation()
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#include <cstdlib>
2+
#include <snmalloc/snmalloc.h>
3+
#include <test/measuretime.h>
4+
#include <test/setup.h>
5+
#include <vector>
6+
7+
using namespace snmalloc;
8+
9+
void fill(std::vector<void*>& out, size_t count, size_t size)
10+
{
11+
out.reserve(count);
12+
for (size_t i = 0; i < count; i++)
13+
{
14+
out.push_back(snmalloc::alloc<Uninit>(size));
15+
}
16+
}
17+
18+
void drain(const char* label, std::vector<void*>& vec, size_t size)
19+
{
20+
MeasureTime m;
21+
m << label << " (" << vec.size() << " x " << size << " B)";
22+
for (void* p : vec)
23+
{
24+
snmalloc::dealloc(p, size);
25+
}
26+
vec.clear();
27+
}
28+
29+
int main(int, char**)
30+
{
31+
setup();
32+
// Issue #809: perf when many objects are freed after the allocator has
33+
// already been finalised (e.g. static/global teardown). Keep counts equal
34+
// for baseline and post-teardown to isolate the teardown cost.
35+
constexpr size_t alloc_count = 1 << 18;
36+
constexpr size_t obj_size = 64;
37+
38+
std::vector<void*> ptrs;
39+
fill(ptrs, alloc_count, obj_size);
40+
drain("Baseline dealloc before finalise", ptrs, obj_size);
41+
42+
// Simulate the allocator already being torn down before remaining frees
43+
// (post-main / static destruction path from #809).
44+
ThreadAlloc::teardown();
45+
46+
fill(ptrs, alloc_count, obj_size);
47+
drain("Immediate dealloc after teardown", ptrs, obj_size);
48+
49+
return 0;
50+
}

0 commit comments

Comments
 (0)