Commit 2a63cb9d authored by Benoit Lize's avatar Benoit Lize Committed by Commit Bot

Reland "base/allocator: Improve PartitionAlloc perftests."

This reverts commit 113ebf6d.

Reason for reland: fixed the Android release builds.

Original change's description:
> base/allocator: Improve PartitionAlloc perftests.
>
> Add:
> - Comparison with the system allocator
> - Better concurrent workloads for competing threads (identical as the
>   test)
> - Reporting of the total allocation rate, and the worst thread
> - Varying number of competing threads
>
> This is useful to evaluate the performance of PartitionAlloc with
> various changes, such as changing its locking to base::Lock, or
> implementing a thread cache.
>
> Bug: 998048
> Change-Id: I4c5f837f9e1b5af19b2f80bd79f3d3181166d930
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2111160
> Reviewed-by: Kentaro Hara <haraken@chromium.org>
> Commit-Queue: Benoit L <lizeb@chromium.org>
> Cr-Commit-Position: refs/heads/master@{#752077}

Bug: 998048
Change-Id: Ife2cf7e33eb17c56d25f9f66ceeee87db617d3e9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2113973Reviewed-by: default avatarKentaro Hara <haraken@chromium.org>
Reviewed-by: default avatarBenoit L <lizeb@chromium.org>
Commit-Queue: Benoit L <lizeb@chromium.org>
Cr-Commit-Position: refs/heads/master@{#752406}
parent b1ef191b
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
#include <vector> #include <vector>
#include "base/allocator/partition_allocator/partition_alloc.h" #include "base/allocator/partition_allocator/partition_alloc.h"
#include "base/strings/stringprintf.h"
#include "base/threading/platform_thread.h" #include "base/threading/platform_thread.h"
#include "base/time/time.h" #include "base/time/time.h"
#include "base/timer/lap_timer.h" #include "base/timer/lap_timer.h"
...@@ -29,14 +30,9 @@ constexpr int kMultiBucketIncrement = 13; ...@@ -29,14 +30,9 @@ constexpr int kMultiBucketIncrement = 13;
// Final size is 24 + (13 * 22) = 310 bytes. // Final size is 24 + (13 * 22) = 310 bytes.
constexpr int kMultiBucketRounds = 22; constexpr int kMultiBucketRounds = 22;
constexpr char kMetricPrefixMemoryAllocation[] = "MemoryAllocation."; constexpr char kMetricPrefixMemoryAllocation[] = "MemoryAllocation";
constexpr char kMetricThroughput[] = "throughput"; constexpr char kMetricThroughput[] = "throughput";
constexpr char kMetricTimePerAllocation[] = "time_per_allocation"; constexpr char kMetricTimePerAllocation[] = "time_per_allocation";
constexpr char kStoryBaseSingleBucket[] = "single_bucket";
constexpr char kStoryBaseSingleBucketWithFree[] = "single_bucket_with_free";
constexpr char kStoryBaseMultiBucket[] = "multi_bucket";
constexpr char kStoryBaseMultiBucketWithFree[] = "multi_bucket_with_free";
constexpr char kStorySuffixWithCompetingThread[] = "_with_competing_thread";
perf_test::PerfResultReporter SetUpReporter(const std::string& story_name) { perf_test::PerfResultReporter SetUpReporter(const std::string& story_name) {
perf_test::PerfResultReporter reporter(kMetricPrefixMemoryAllocation, perf_test::PerfResultReporter reporter(kMetricPrefixMemoryAllocation,
...@@ -46,43 +42,60 @@ perf_test::PerfResultReporter SetUpReporter(const std::string& story_name) { ...@@ -46,43 +42,60 @@ perf_test::PerfResultReporter SetUpReporter(const std::string& story_name) {
return reporter; return reporter;
} }
std::string GetSuffix(bool competing_thread) { enum class AllocatorType { kSystem, kPartitionAlloc };
return competing_thread ? kStorySuffixWithCompetingThread : "";
} class Allocator {
public:
Allocator() = default;
virtual ~Allocator() = default;
virtual void Init() {}
virtual void* Alloc(size_t size) = 0;
virtual void Free(void* data) = 0;
};
class AllocatingThread : public PlatformThread::Delegate { class SystemAllocator : public Allocator {
public: public:
explicit AllocatingThread(PartitionAllocatorGeneric* allocator) SystemAllocator() = default;
: allocator_(allocator), should_stop_(false) { ~SystemAllocator() override = default;
PlatformThread::Create(0, this, &thread_handle_); void* Alloc(size_t size) override { return malloc(size); }
void Free(void* data) override { free(data); }
};
class PartitionAllocator : public Allocator {
public:
PartitionAllocator()
: alloc_(std::make_unique<PartitionAllocatorGeneric>()) {}
~PartitionAllocator() override = default;
void Init() override { alloc_->init(); }
void* Alloc(size_t size) override { return alloc_->root()->Alloc(size, ""); }
void Free(void* data) override { return alloc_->root()->Free(data); }
private:
std::unique_ptr<PartitionAllocatorGeneric> alloc_;
};
class TestLoopThread : public PlatformThread::Delegate {
public:
explicit TestLoopThread(OnceCallback<float()> test_fn)
: test_fn_(std::move(test_fn)) {
CHECK(PlatformThread::Create(0, this, &thread_handle_));
} }
~AllocatingThread() override { float Run() {
should_stop_ = true;
PlatformThread::Join(thread_handle_); PlatformThread::Join(thread_handle_);
return laps_per_second_;
} }
// Allocates and frees memory in a loop until |should_stop_| becomes true. void ThreadMain() override { laps_per_second_ = std::move(test_fn_).Run(); }
void ThreadMain() override {
uint64_t count = 0;
while (true) {
// Only check |should_stop_| every 2^15 iterations, as it is a
// sequentially consistent access, hence expensive.
if (count % (1 << 15) == 0 && should_stop_)
break;
void* data = allocator_->root()->Alloc(10, "");
allocator_->root()->Free(data);
count++;
}
}
PartitionAllocatorGeneric* allocator_; OnceCallback<float()> test_fn_;
std::atomic<bool> should_stop_;
PlatformThreadHandle thread_handle_; PlatformThreadHandle thread_handle_;
std::atomic<float> laps_per_second_;
}; };
void DisplayResults(const std::string& story_name, void DisplayResults(const std::string& story_name,
size_t iterations_per_second) { float iterations_per_second) {
auto reporter = SetUpReporter(story_name); auto reporter = SetUpReporter(story_name);
reporter.AddResult(kMetricThroughput, iterations_per_second); reporter.AddResult(kMetricThroughput, iterations_per_second);
reporter.AddResult(kMetricTimePerAllocation, reporter.AddResult(kMetricTimePerAllocation,
...@@ -93,12 +106,11 @@ class MemoryAllocationPerfNode { ...@@ -93,12 +106,11 @@ class MemoryAllocationPerfNode {
public: public:
MemoryAllocationPerfNode* GetNext() const { return next_; } MemoryAllocationPerfNode* GetNext() const { return next_; }
void SetNext(MemoryAllocationPerfNode* p) { next_ = p; } void SetNext(MemoryAllocationPerfNode* p) { next_ = p; }
static void FreeAll(MemoryAllocationPerfNode* first, static void FreeAll(MemoryAllocationPerfNode* first, Allocator* alloc) {
PartitionAllocatorGeneric& alloc) {
MemoryAllocationPerfNode* cur = first; MemoryAllocationPerfNode* cur = first;
while (cur != nullptr) { while (cur != nullptr) {
MemoryAllocationPerfNode* next = cur->GetNext(); MemoryAllocationPerfNode* next = cur->GetNext();
alloc.root()->Free(cur); alloc->Free(cur);
cur = next; cur = next;
} }
} }
...@@ -107,172 +119,177 @@ class MemoryAllocationPerfNode { ...@@ -107,172 +119,177 @@ class MemoryAllocationPerfNode {
MemoryAllocationPerfNode* next_ = nullptr; MemoryAllocationPerfNode* next_ = nullptr;
}; };
class MemoryAllocationPerfTest : public testing::Test { #if !defined(OS_ANDROID)
public: float SingleBucket(Allocator* allocator) {
MemoryAllocationPerfTest() auto* first =
: timer_(kWarmupRuns, kTimeLimit, kTimeCheckInterval) {} reinterpret_cast<MemoryAllocationPerfNode*>(allocator->Alloc(40));
void SetUp() override { alloc_.init(); }
void TearDown() override { LapTimer timer(kWarmupRuns, kTimeLimit, kTimeCheckInterval);
alloc_.root()->PurgeMemory(PartitionPurgeDecommitEmptyPages | MemoryAllocationPerfNode* cur = first;
PartitionPurgeDiscardUnusedSystemPages); do {
} auto* next =
reinterpret_cast<MemoryAllocationPerfNode*>(allocator->Alloc(40));
CHECK_NE(next, nullptr);
cur->SetNext(next);
cur = next;
timer.NextLap();
} while (!timer.HasTimeLimitExpired());
// next_ = nullptr only works if the class constructor is called (it's not
// called in this case because then we can allocate arbitrary-length
// payloads.)
cur->SetNext(nullptr);
MemoryAllocationPerfNode::FreeAll(first, allocator);
return timer.LapsPerSecond();
}
#endif // defined(OS_ANDROID)
float SingleBucketWithFree(Allocator* allocator) {
// Allocate an initial element to make sure the bucket stays set up.
void* elem = allocator->Alloc(40);
LapTimer timer(kWarmupRuns, kTimeLimit, kTimeCheckInterval);
do {
void* cur = allocator->Alloc(40);
CHECK_NE(cur, nullptr);
allocator->Free(cur);
timer.NextLap();
} while (!timer.HasTimeLimitExpired());
allocator->Free(elem);
return timer.LapsPerSecond();
}
protected: #if !defined(OS_ANDROID)
void TestSingleBucket(bool competing_thread) { float MultiBucket(Allocator* allocator) {
MemoryAllocationPerfNode* first = auto* first =
reinterpret_cast<MemoryAllocationPerfNode*>( reinterpret_cast<MemoryAllocationPerfNode*>(allocator->Alloc(40));
alloc_.root()->Alloc(40, "<testing>")); MemoryAllocationPerfNode* cur = first;
timer_.Reset(); LapTimer timer(kWarmupRuns, kTimeLimit, kTimeCheckInterval);
MemoryAllocationPerfNode* cur = first; do {
do { for (int i = 0; i < kMultiBucketRounds; i++) {
MemoryAllocationPerfNode* next = auto* next = reinterpret_cast<MemoryAllocationPerfNode*>(allocator->Alloc(
reinterpret_cast<MemoryAllocationPerfNode*>( kMultiBucketMinimumSize + (i * kMultiBucketIncrement)));
alloc_.root()->Alloc(40, "<testing>"));
CHECK_NE(next, nullptr); CHECK_NE(next, nullptr);
cur->SetNext(next); cur->SetNext(next);
cur = next; cur = next;
timer_.NextLap(); }
} while (!timer_.HasTimeLimitExpired()); timer.NextLap();
// next_ = nullptr only works if the class constructor is called (it's not } while (!timer.HasTimeLimitExpired());
// called in this case because then we can allocate arbitrary-length cur->SetNext(nullptr);
// payloads.)
cur->SetNext(nullptr);
MemoryAllocationPerfNode::FreeAll(first, alloc_);
DisplayResults(
std::string(kStoryBaseSingleBucket) + GetSuffix(competing_thread),
timer_.LapsPerSecond());
}
void TestSingleBucketWithFree(bool competing_thread) {
// Allocate an initial element to make sure the bucket stays set up.
void* elem = alloc_.root()->Alloc(40, "<testing>");
timer_.Reset(); MemoryAllocationPerfNode::FreeAll(first, allocator);
do {
void* cur = alloc_.root()->Alloc(40, "<testing>");
CHECK_NE(cur, nullptr);
alloc_.root()->Free(cur);
timer_.NextLap();
} while (!timer_.HasTimeLimitExpired());
alloc_.root()->Free(elem);
DisplayResults(std::string(kStoryBaseSingleBucketWithFree) +
GetSuffix(competing_thread),
timer_.LapsPerSecond());
}
void TestMultiBucket(bool competing_thread) { return timer.LapsPerSecond() * kMultiBucketRounds;
MemoryAllocationPerfNode* first = }
reinterpret_cast<MemoryAllocationPerfNode*>( #endif // defined(OS_ANDROID)
alloc_.root()->Alloc(40, "<testing>"));
MemoryAllocationPerfNode* cur = first; float MultiBucketWithFree(Allocator* allocator) {
std::vector<void*> elems;
timer_.Reset(); elems.reserve(kMultiBucketRounds);
do { // Do an initial round of allocation to make sure that the buckets stay in
for (int i = 0; i < kMultiBucketRounds; i++) { // use (and aren't accidentally released back to the OS).
MemoryAllocationPerfNode* next = for (int i = 0; i < kMultiBucketRounds; i++) {
reinterpret_cast<MemoryAllocationPerfNode*>(alloc_.root()->Alloc( void* cur =
kMultiBucketMinimumSize + (i * kMultiBucketIncrement), allocator->Alloc(kMultiBucketMinimumSize + (i * kMultiBucketIncrement));
"<testing>")); CHECK_NE(cur, nullptr);
CHECK_NE(next, nullptr); elems.push_back(cur);
cur->SetNext(next);
cur = next;
}
timer_.NextLap();
} while (!timer_.HasTimeLimitExpired());
cur->SetNext(nullptr);
MemoryAllocationPerfNode::FreeAll(first, alloc_);
DisplayResults(
std::string(kStoryBaseMultiBucket) + GetSuffix(competing_thread),
timer_.LapsPerSecond() * kMultiBucketRounds);
} }
void TestMultiBucketWithFree(bool competing_thread) { LapTimer timer(kWarmupRuns, kTimeLimit, kTimeCheckInterval);
std::vector<void*> elems; do {
elems.reserve(kMultiBucketRounds);
// Do an initial round of allocation to make sure that the buckets stay in
// use (and aren't accidentally released back to the OS).
for (int i = 0; i < kMultiBucketRounds; i++) { for (int i = 0; i < kMultiBucketRounds; i++) {
void* cur = alloc_.root()->Alloc( void* cur = allocator->Alloc(kMultiBucketMinimumSize +
kMultiBucketMinimumSize + (i * kMultiBucketIncrement), "<testing>"); (i * kMultiBucketIncrement));
CHECK_NE(cur, nullptr); CHECK_NE(cur, nullptr);
elems.push_back(cur); allocator->Free(cur);
} }
timer.NextLap();
timer_.Reset(); } while (!timer.HasTimeLimitExpired());
do {
for (int i = 0; i < kMultiBucketRounds; i++) {
void* cur = alloc_.root()->Alloc(
kMultiBucketMinimumSize + (i * kMultiBucketIncrement), "<testing>");
CHECK_NE(cur, nullptr);
alloc_.root()->Free(cur);
}
timer_.NextLap();
} while (!timer_.HasTimeLimitExpired());
for (void* ptr : elems) { for (void* ptr : elems) {
alloc_.root()->Free(ptr); allocator->Free(ptr);
} }
DisplayResults(std::string(kStoryBaseMultiBucketWithFree) + return timer.LapsPerSecond() * kMultiBucketRounds;
GetSuffix(competing_thread),
timer_.LapsPerSecond() * kMultiBucketRounds);
}
LapTimer timer_;
PartitionAllocatorGeneric alloc_;
};
TEST_F(MemoryAllocationPerfTest, SingleBucket) {
TestSingleBucket(false);
} }
TEST_F(MemoryAllocationPerfTest, SingleBucketWithCompetingThread) { std::unique_ptr<Allocator> CreateAllocator(AllocatorType type) {
AllocatingThread t(&alloc_); if (type == AllocatorType::kSystem)
TestSingleBucket(true); return std::make_unique<SystemAllocator>();
return std::make_unique<PartitionAllocator>();
} }
TEST_F(MemoryAllocationPerfTest, SingleBucketWithFree) { void RunTest(int thread_count,
TestSingleBucketWithFree(false); AllocatorType alloc_type,
} float (*test_fn)(Allocator*),
const char* story_base_name) {
auto alloc = CreateAllocator(alloc_type);
alloc->Init();
std::vector<std::unique_ptr<TestLoopThread>> threads;
for (int i = 0; i < thread_count; ++i) {
threads.push_back(std::make_unique<TestLoopThread>(
BindOnce(test_fn, Unretained(alloc.get()))));
}
uint64_t total_laps_per_second = 0;
uint64_t min_laps_per_second = std::numeric_limits<uint64_t>::max();
for (int i = 0; i < thread_count; ++i) {
uint64_t laps_per_second = threads[i]->Run();
min_laps_per_second = std::min(laps_per_second, min_laps_per_second);
total_laps_per_second += laps_per_second;
}
std::string name = base::StringPrintf(
"%s.%s_%s_%d", kMetricPrefixMemoryAllocation, story_base_name,
alloc_type == AllocatorType::kSystem ? "System" : "PartitionAlloc",
thread_count);
TEST_F(MemoryAllocationPerfTest, SingleBucketWithFreeWithCompetingThread) { DisplayResults(name + "_total", total_laps_per_second);
AllocatingThread t(&alloc_); DisplayResults(name + "_worst", min_laps_per_second);
TestSingleBucketWithFree(true);
} }
// Failing on Nexus5x: crbug.com/949838 class MemoryAllocationPerfTest
#if defined(OS_ANDROID) : public testing::TestWithParam<std::tuple<int, AllocatorType>> {};
#define MAYBE_MultiBucket DISABLED_MultiBucket
#define MAYBE_MultiBucketWithCompetingThread \ INSTANTIATE_TEST_SUITE_P(
DISABLED_MultiBucketWithCompetingThread ,
#else MemoryAllocationPerfTest,
#define MAYBE_MultiBucket MultiBucket ::testing::Combine(::testing::Values(1, 2, 3, 4),
#define MAYBE_MultiBucketWithCompetingThread MultiBucketWithCompetingThread ::testing::Values(AllocatorType::kSystem,
#endif AllocatorType::kPartitionAlloc)));
TEST_F(MemoryAllocationPerfTest, MAYBE_MultiBucket) {
TestMultiBucket(false); // This test (and the other one below) allocates a large amount of memory, which
// can cause issues on Android.
#if !defined(OS_ANDROID)
TEST_P(MemoryAllocationPerfTest, SingleBucket) {
auto params = GetParam();
RunTest(std::get<0>(params), std::get<1>(params), SingleBucket,
"SingleBucket");
} }
#endif
TEST_F(MemoryAllocationPerfTest, MAYBE_MultiBucketWithCompetingThread) { TEST_P(MemoryAllocationPerfTest, SingleBucketWithFree) {
AllocatingThread t(&alloc_); auto params = GetParam();
TestMultiBucket(true); RunTest(std::get<0>(params), std::get<1>(params), SingleBucketWithFree,
"SingleBucketWithFree");
} }
TEST_F(MemoryAllocationPerfTest, MultiBucketWithFree) { #if !defined(OS_ANDROID)
TestMultiBucketWithFree(false); TEST_P(MemoryAllocationPerfTest, MultiBucket) {
auto params = GetParam();
RunTest(std::get<0>(params), std::get<1>(params), MultiBucket, "MultiBucket");
} }
#endif
TEST_F(MemoryAllocationPerfTest, MultiBucketWithFreeWithCompetingThread) { TEST_P(MemoryAllocationPerfTest, MultiBucketWithFree) {
AllocatingThread t(&alloc_); auto params = GetParam();
TestMultiBucketWithFree(true); RunTest(std::get<0>(params), std::get<1>(params), MultiBucketWithFree,
"MultiBucketWithFree");
} }
} // anonymous namespace } // namespace
} // namespace base } // namespace base
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment