Skip to content

Commit 2ce18e6

Browse files
martinmaascopybara-github
authored andcommitted
Implement an object lifetime predictor in TCMalloc.
This library stores a database that maps stack traces to lifetimes (short/long) of objects allocated at this particular stack trace, and allows efficient lookup of these lifetimes. PiperOrigin-RevId: 391660025 Change-Id: Ia6308f58099c4d7cd3d52a36fe8d573194dd3433
1 parent 6334d2b commit 2ce18e6

File tree

3 files changed

+457
-0
lines changed

3 files changed

+457
-0
lines changed

tcmalloc/internal/BUILD

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,39 @@ cc_test(
8282
],
8383
)
8484

85+
cc_library(
86+
name = "lifetime_predictions",
87+
hdrs = ["lifetime_predictions.h"],
88+
copts = TCMALLOC_DEFAULT_COPTS,
89+
visibility = [
90+
"//tcmalloc:__subpackages__",
91+
],
92+
deps = [
93+
":linked_list",
94+
"@com_google_absl//absl/algorithm:container",
95+
"@com_google_absl//absl/base",
96+
"@com_google_absl//absl/base:core_headers",
97+
"@com_google_absl//absl/base:malloc_internal",
98+
"@com_google_absl//absl/debugging:stacktrace",
99+
"@com_google_absl//absl/hash",
100+
"@com_google_absl//absl/time",
101+
],
102+
)
103+
104+
cc_test(
105+
name = "lifetime_predictions_test",
106+
srcs = ["lifetime_predictions_test.cc"],
107+
copts = TCMALLOC_DEFAULT_COPTS,
108+
deps = [
109+
":lifetime_predictions",
110+
":logging",
111+
"@com_github_google_benchmark//:benchmark",
112+
"@com_google_absl//absl/base:core_headers",
113+
"@com_google_absl//absl/flags:flag",
114+
"@com_google_googletest//:gtest_main",
115+
],
116+
)
117+
85118
cc_library(
86119
name = "linked_list",
87120
hdrs = ["linked_list.h"],
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
// Copyright 2020 The TCMalloc Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#ifndef TCMALLOC_INTERNAL_LIFETIME_PREDICTIONS_H_
16+
#define TCMALLOC_INTERNAL_LIFETIME_PREDICTIONS_H_
17+
18+
#include <algorithm>
19+
#include <cstdlib>
20+
#include <functional>
21+
22+
#include "absl/algorithm/container.h"
23+
#include "absl/base/const_init.h"
24+
#include "absl/base/internal/low_level_alloc.h"
25+
#include "absl/base/internal/spinlock.h"
26+
#include "absl/debugging/stacktrace.h"
27+
#include "absl/hash/hash.h"
28+
#include "absl/time/clock.h"
29+
#include "absl/time/time.h"
30+
#include "tcmalloc/internal/linked_list.h"
31+
32+
GOOGLE_MALLOC_SECTION_BEGIN
33+
namespace tcmalloc {
34+
namespace tcmalloc_internal {
35+
36+
// Counts how many times we observed objects with a particular stack trace
37+
// that were short lived/long lived. Each LifetimeStats object is associated
38+
// with a particular allocation site (i.e., allocation stack trace) and each
39+
// allocation site has at most one LifetimeStats object. All accesses to
40+
// LifetimeStats objects need to be synchronized via the page heap lock.
41+
class LifetimeStats : public TList<LifetimeStats>::Elem {
42+
public:
43+
enum class Certainty { kLowCertainty, kHighCertainty };
44+
enum class Prediction { kShortLived, kLongLived };
45+
46+
void Update(Prediction prediction) {
47+
if (prediction == Prediction::kShortLived) {
48+
short_lived_++;
49+
} else {
50+
long_lived_++;
51+
}
52+
}
53+
54+
Prediction Predict(Certainty certainty) {
55+
if (certainty == Certainty::kLowCertainty) {
56+
return (short_lived_ > long_lived_) ? Prediction::kShortLived
57+
: Prediction::kLongLived;
58+
} else {
59+
// If little data was collected, predict as long-lived (current behavior).
60+
return (short_lived_ > (long_lived_ + 10)) ? Prediction::kShortLived
61+
: Prediction::kLongLived;
62+
}
63+
}
64+
65+
// Reference counts are protected by LifetimeDatabase::table_lock_.
66+
67+
// Increments the reference count of this entry.
68+
void IncRef() { ++refcount_; }
69+
70+
// Returns true if and only if the reference count reaches 0.
71+
bool DecRef() { return --refcount_ == 0; }
72+
73+
private:
74+
uint64_t refcount_ = 1;
75+
uint64_t short_lived_ = 0;
76+
uint64_t long_lived_ = 0;
77+
};
78+
79+
// Manages stack traces and statistics about their associated lifetimes. Since
80+
// the database can fill up, old entries are evicted. Evicted entries need to
81+
// survive as long as the last lifetime tracker referencing them and are thus
82+
// reference-counted.
83+
class LifetimeDatabase {
84+
public:
85+
struct Key {
86+
int depth; // Number of PC values stored in array below
87+
void* stack[kMaxStackDepth];
88+
89+
// Statically instantiate at the start of the allocation to acquire
90+
// the allocation stack trace.
91+
Key() { depth = absl::GetStackTrace(stack, kMaxStackDepth, 1); }
92+
93+
template <typename H>
94+
friend H AbslHashValue(H h, const Key& c) {
95+
return H::combine(H::combine_contiguous(std::move(h), c.stack, c.depth),
96+
c.depth);
97+
}
98+
99+
bool operator==(const Key& other) const {
100+
if (depth != other.depth) {
101+
return false;
102+
}
103+
return std::equal(stack, stack + depth, other.stack);
104+
}
105+
};
106+
107+
// Captures statistics associated with the low-level allocator backing the
108+
// memory used by the database.
109+
struct ArenaStats {
110+
uint64_t bytes_allocated;
111+
};
112+
113+
static constexpr int kMaxDatabaseSize = 1024;
114+
115+
LifetimeDatabase() {}
116+
~LifetimeDatabase() {}
117+
118+
// Not copyable or movable
119+
LifetimeDatabase(const LifetimeDatabase&) = delete;
120+
LifetimeDatabase& operator=(const LifetimeDatabase&) = delete;
121+
122+
// Identifies the current stack trace and returns a handle to the lifetime
123+
// statistics associated with this stack trace. May run outside the page heap
124+
// lock -- we therefore need to do our own locking. This increments the
125+
// reference count of the lifetime stats object and the caller is responsible
126+
// for calling RemoveLifetimeStatsReference when finished with the object.
127+
LifetimeStats* LookupOrAddLifetimeStats(Key* k) {
128+
absl::base_internal::SpinLockHolder h(&table_lock_);
129+
auto it = table_.find(*k);
130+
LifetimeStats* s;
131+
if (it == table_.end()) {
132+
MaybeEvictLRU();
133+
// Allocate a new entry using the low-level allocator, which is safe
134+
// to call from within TCMalloc.
135+
s = stats_allocator_.allocate(1);
136+
new (s) LifetimeStats();
137+
table_.insert(std::make_pair(*k, s));
138+
stats_fifo_.append(s);
139+
} else {
140+
s = it->second;
141+
UpdateLRU(s);
142+
}
143+
s->IncRef();
144+
return s;
145+
}
146+
147+
void RemoveLifetimeStatsReference(LifetimeStats* s) {
148+
absl::base_internal::SpinLockHolder h(&table_lock_);
149+
if (s->DecRef()) {
150+
stats_allocator_.deallocate(s, 1);
151+
}
152+
}
153+
154+
size_t size() const {
155+
absl::base_internal::SpinLockHolder h(&table_lock_);
156+
return table_.size();
157+
}
158+
159+
size_t evictions() const {
160+
absl::base_internal::SpinLockHolder h(&table_lock_);
161+
return n_evictions_;
162+
}
163+
164+
static ArenaStats* arena_stats() {
165+
static ArenaStats stats = {0};
166+
return &stats;
167+
}
168+
169+
protected:
170+
static const int kMaxStackDepth = 64;
171+
172+
static absl::base_internal::LowLevelAlloc::Arena* GetArena() {
173+
static absl::base_internal::LowLevelAlloc::Arena* arena =
174+
absl::base_internal::LowLevelAlloc::NewArena(0);
175+
return arena;
176+
}
177+
178+
static uint64_t bytes_allocated_ ABSL_GUARDED_BY(table_lock_);
179+
180+
void UpdateLRU(LifetimeStats* stats)
181+
ABSL_EXCLUSIVE_LOCKS_REQUIRED(table_lock_) {
182+
stats_fifo_.remove(stats);
183+
stats_fifo_.append(stats);
184+
}
185+
186+
// If an entry is evicted, it is returned (nullptr otherwise).
187+
void MaybeEvictLRU() ABSL_EXCLUSIVE_LOCKS_REQUIRED(table_lock_) {
188+
if (table_.size() < kMaxDatabaseSize) {
189+
return;
190+
}
191+
n_evictions_++;
192+
LifetimeStats* evict = stats_fifo_.first();
193+
stats_fifo_.remove(evict);
194+
for (auto it = table_.begin(); it != table_.end(); ++it) {
195+
if (it->second == evict) {
196+
table_.erase(it);
197+
if (evict->DecRef()) {
198+
stats_allocator_.deallocate(evict, 1);
199+
}
200+
return;
201+
}
202+
}
203+
CHECK_CONDITION(false); // Should not happen
204+
}
205+
206+
private:
207+
template <typename T>
208+
class MyAllocator : public std::allocator<T> {
209+
public:
210+
template <typename U>
211+
struct rebind {
212+
using other = MyAllocator<U>;
213+
};
214+
215+
MyAllocator() noexcept {}
216+
217+
template <typename U>
218+
explicit MyAllocator(const MyAllocator<U>&) noexcept {}
219+
220+
T* allocate(size_t num_objects, const void* = nullptr) {
221+
size_t bytes = num_objects * sizeof(T);
222+
arena_stats()->bytes_allocated += bytes;
223+
return static_cast<T*>(absl::base_internal::LowLevelAlloc::AllocWithArena(
224+
bytes, GetArena()));
225+
}
226+
227+
void deallocate(T* p, size_t num_objects) {
228+
size_t bytes = num_objects * sizeof(T);
229+
arena_stats()->bytes_allocated -= bytes;
230+
absl::base_internal::LowLevelAlloc::Free(p);
231+
}
232+
};
233+
234+
MyAllocator<LifetimeStats> stats_allocator_ ABSL_GUARDED_BY(table_lock_);
235+
mutable absl::base_internal::SpinLock table_lock_{
236+
absl::kConstInit, absl::base_internal::SCHEDULE_KERNEL_ONLY};
237+
238+
// Stores the current mapping from allocation site to LifetimeStats.
239+
std::unordered_map<Key, LifetimeStats*, absl::Hash<Key>, std::equal_to<Key>,
240+
MyAllocator<std::pair<const Key, LifetimeStats*>>>
241+
table_ ABSL_GUARDED_BY(table_lock_);
242+
243+
// Stores the entries ordered by how many times they have been accessed.
244+
TList<LifetimeStats> stats_fifo_ ABSL_GUARDED_BY(table_lock_);
245+
size_t n_evictions_ ABSL_GUARDED_BY(table_lock_) = 0;
246+
};
247+
248+
} // namespace tcmalloc_internal
249+
} // namespace tcmalloc
250+
GOOGLE_MALLOC_SECTION_END
251+
252+
#endif // TCMALLOC_INTERNAL_LIFETIME_PREDICTIONS_H_

0 commit comments

Comments
 (0)