| // Copyright 2018 The Fuchsia Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include <iostream> | |
| #include <random> | |
| #include <vector> | |
| #include <fbl/intrusive_single_list.h> | |
| #include <fbl/ref_counted.h> | |
| #include <fbl/ref_ptr.h> | |
| #include <fbl/slab_allocator.h> | |
| #include <fbl/string_printf.h> | |
| #include <perftest/perftest.h> | |
| namespace { | |
| /*** Common definitions ***/ | |
| // The motivation for multiple sizes is to quantify any scaling behavior with | |
| // the size of the allocation. | |
| constexpr size_t kSmallBlockSizeBytes = 64; | |
| constexpr size_t kLargeBlockSizeBytes = 8192; | |
| // This value must accommodate the maximal value of |total_size_kbytes| in | |
| // RegisterRetainedMemTest(). | |
| constexpr size_t kLiveAllocLimitBytes = 32u * 1024 * 1024; | |
| template <size_t Size> | |
| class DataBuf { | |
| public: | |
| DataBuf(){ | |
| // We provide a user-defined default constructor that does nothing, to | |
| // avoid the cost of zeroing out |data|. | |
| // | |
| // Rationale: in the absence of a user-defined constructor, expressions | |
| // such as |DataBuf()| or |new DataBuf()| trigger value-initialization, | |
| // which zeros out |data|. For details, see | |
| // https://stackoverflow.com/a/2418195 | |
| }; | |
| private: | |
| char data[Size]; | |
| }; | |
| template <typename AllocatorTraits> | |
| class SlabDataBuf; | |
| template <typename AllocatorTraits> | |
| class SlabDataBuf : public fbl::SlabAllocated<AllocatorTraits>, | |
| public fbl::RefCounted<SlabDataBuf<AllocatorTraits>>, | |
| public fbl::SinglyLinkedListable<fbl::RefPtr<SlabDataBuf<AllocatorTraits>>> { | |
| public: | |
| SlabDataBuf(){ | |
| // As with DataBuf, we provide a user-defined default constructor that | |
| // does nothing, to avoid the cost of zeroing out |data|. | |
| // | |
| // CAUTION: For reasons that are unclear, inheriting DataBuf (rather than | |
| // declaring our own |data|) does not avoid the cost of zeroing out | |
| // DataBuf::data, even though DataBuf has a user-defined default | |
| // constructor. | |
| }; | |
| private: | |
| char data[AllocatorTraits::kUserBufSize]; | |
| }; | |
| /*** Static slab allocator definitions ***/ | |
| template <typename AllocatorTraits> | |
| class StaticSlabAllocator; | |
| template <size_t ObjSize, size_t SlabSize> | |
| struct StaticSlabAllocatorTraits | |
| : public fbl::StaticSlabAllocatorTraits< | |
| fbl::RefPtr<SlabDataBuf<StaticSlabAllocatorTraits<ObjSize, SlabSize>>>, SlabSize> { | |
| using AllocT = StaticSlabAllocator<StaticSlabAllocatorTraits<ObjSize, SlabSize>>; | |
| static constexpr char kName[] = "SlabStatic"; | |
| static constexpr size_t kUserBufSize = ObjSize; | |
| static constexpr size_t kSlabSizeKbytes = SlabSize / 1024; | |
| static auto GetConfigAsString() { | |
| return fbl::StringPrintf("%zubytes/%zuKbytes", kUserBufSize, kSlabSizeKbytes); | |
| } | |
| }; | |
| template <typename AllocatorTraits> | |
| class StaticSlabAllocator { | |
| public: | |
| static constexpr size_t kUserBufSize = AllocatorTraits::kUserBufSize; | |
| auto New() { return fbl::SlabAllocator<AllocatorTraits>::New(); } | |
| }; | |
| using StaticSmallBlockAllocatorTraits = | |
| StaticSlabAllocatorTraits<kSmallBlockSizeBytes, fbl::DEFAULT_SLAB_ALLOCATOR_SLAB_SIZE>; | |
| using StaticLargeBlockAllocatorTraits = | |
| StaticSlabAllocatorTraits<kLargeBlockSizeBytes, kLargeBlockSizeBytes * 205>; | |
| static_assert(fbl::SlabAllocator<StaticLargeBlockAllocatorTraits>::AllocsPerSlab == | |
| fbl::SlabAllocator<StaticSmallBlockAllocatorTraits>::AllocsPerSlab, | |
| "Please adjust the SLAB_SIZE parameter for " | |
| "StaticLargeBlockAllocatorTraits, so that the StaticLargeBlockAllocator " | |
| "amortizes malloc() calls over as many slab objects as the Static " | |
| "SmallBlockAllocator."); | |
| /*** Instanced slab allocator definitions ***/ | |
| template <typename AllocatorTraits> | |
| class InstancedSlabAllocator; | |
| template <size_t ObjSize, size_t SlabSize> | |
| struct InstancedSlabAllocatorTraits | |
| : public fbl::SlabAllocatorTraits< | |
| fbl::RefPtr<SlabDataBuf<InstancedSlabAllocatorTraits<ObjSize, SlabSize>>>, SlabSize> { | |
| using AllocT = InstancedSlabAllocator<InstancedSlabAllocatorTraits<ObjSize, SlabSize>>; | |
| static constexpr char kName[] = "SlabInstanced"; | |
| static constexpr size_t kUserBufSize = ObjSize; | |
| static constexpr size_t kSlabSizeKbytes = SlabSize / 1024; | |
| static auto GetConfigAsString() { | |
| return fbl::StringPrintf("%zubytes/%zuKbytes", kUserBufSize, kSlabSizeKbytes); | |
| } | |
| }; | |
| template <typename AllocatorTraits> | |
| class InstancedSlabAllocator { | |
| public: | |
| static constexpr size_t kUserBufSize = AllocatorTraits::kUserBufSize; | |
| InstancedSlabAllocator() : allocator_(kMaxSlabs) {} | |
| auto New() { return allocator_.New(); } | |
| private: | |
| static constexpr size_t kMaxSizeBytes = kLiveAllocLimitBytes; | |
| static constexpr size_t kMaxSlabs = | |
| (kMaxSizeBytes / (fbl::SlabAllocator<AllocatorTraits>::AllocsPerSlab * kUserBufSize)) + | |
| 1 /* Round up */; | |
| fbl::SlabAllocator<AllocatorTraits> allocator_; | |
| }; | |
| using InstancedSmallBlockAllocatorTraits = | |
| InstancedSlabAllocatorTraits<kSmallBlockSizeBytes, fbl::DEFAULT_SLAB_ALLOCATOR_SLAB_SIZE>; | |
| using InstancedLargeBlockAllocatorTraits = | |
| InstancedSlabAllocatorTraits<kLargeBlockSizeBytes, kLargeBlockSizeBytes * 187>; | |
| static_assert(fbl::SlabAllocator<InstancedLargeBlockAllocatorTraits>::AllocsPerSlab == | |
| fbl::SlabAllocator<InstancedSmallBlockAllocatorTraits>::AllocsPerSlab, | |
| "Please adjust the SLAB_SIZE parameter for " | |
| "InstancedLargeBlockAllocatorTraits, so that the " | |
| "InstancedLargeBlockAllocator amortizes malloc() calls over as many slab " | |
| "objects as the InstancedSmallBlockAllocator."); | |
| /*** Heap allocator definitions ***/ | |
| template <typename AllocatorTraits> | |
| class HeapAllocator; | |
| template <size_t ObjSize> | |
| struct HeapAllocatorTraits { | |
| using AllocT = HeapAllocator<HeapAllocatorTraits<ObjSize>>; | |
| static constexpr char kName[] = "Malloc"; | |
| static constexpr size_t kUserBufSize = ObjSize; | |
| static auto GetConfigAsString() { return fbl::StringPrintf("%zubytes", kUserBufSize); } | |
| }; | |
| template <typename AllocatorTraits> | |
| class HeapAllocator { | |
| public: | |
| static constexpr size_t kUserBufSize = AllocatorTraits::kUserBufSize; | |
| auto New() { return std::make_unique<DataBuf<kUserBufSize>>(); } | |
| }; | |
| using HeapSmallBlockAllocatorTraits = HeapAllocatorTraits<kSmallBlockSizeBytes>; | |
| using HeapLargeBlockAllocatorTraits = HeapAllocatorTraits<kLargeBlockSizeBytes>; | |
| /*** Benchmark code ***/ | |
| // Utility function called from RetainAndFreeOldest() and RetainAndFreeRandom(). | |
| template <typename AllocatorT> | |
| bool RetainAndFree(const std::vector<size_t>& replacement_sequence, perftest::RepeatState* state) { | |
| const size_t num_bufs_to_retain = replacement_sequence.size(); | |
| if (num_bufs_to_retain < 1) { | |
| std::cerr << "Must retain at least 1 buffer" << std::endl; | |
| return false; | |
| } | |
| // Populate an initial collection of buffers. | |
| AllocatorT allocator; | |
| std::vector<decltype(allocator.New())> retained_bufs(num_bufs_to_retain); | |
| std::generate(retained_bufs.begin(), retained_bufs.end(), [&] { return allocator.New(); }); | |
| for (size_t i = 0; i < retained_bufs.size(); ++i) { | |
| if (!retained_bufs[i]) { | |
| std::cerr << "Failed to allocate buf " << i << " before benchmark loop" << std::endl; | |
| return false; | |
| } | |
| } | |
| // The benchmark task: replace a random existing buffer with a new one. | |
| for (size_t i = 0; state->KeepRunning(); ++i) { | |
| const size_t old_buf_index = replacement_sequence[i % num_bufs_to_retain]; | |
| const auto& buf = (retained_bufs[old_buf_index] = allocator.New()); | |
| if (!buf) { | |
| std::cerr << "Failed to allocate " << allocator.kUserBufSize | |
| << " bytes at benchmark iteration " << i << std::endl; | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| // Measure the time taken to allocate and immediately free a block | |
| // from a slab allocator. The block is allocated from the pool | |
| // initialized by one of the DECLARE_STATIC_SLAB_ALLOCATOR_STORAGE | |
| // statements at the end of this file. This benchmark represents (presumed) | |
| // best-case behavior, as the memory pool should be unfragmented. | |
| template <typename AllocatorT> | |
| bool AllocAndFree(perftest::RepeatState* state) { | |
| AllocatorT allocator; | |
| while (state->KeepRunning()) { | |
| auto buf = allocator.New(); | |
| perftest::DoNotOptimize(buf); | |
| if (!buf) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| // Measure the time taken to free the oldest allocated block, and allocate a | |
| // new one, using the slab allocator. The block is allocated from the pool | |
| // initialized by one of the DECLARE_STATIC_SLAB_ALLOCATOR_STORAGE statements | |
| // at the end of this file. This benchmark abstracts a network copy workload, | |
| // when copying from a fast source, to a slow sink. | |
| template <typename AllocatorT> | |
| bool RetainAndFreeOldest(perftest::RepeatState* state, size_t num_bufs_to_retain) { | |
| // Generate the sequence of indexes of buffers to replace. | |
| std::vector<size_t> buf_to_free(num_bufs_to_retain); | |
| std::iota(buf_to_free.begin(), buf_to_free.end(), 0); | |
| return RetainAndFree<AllocatorT>(buf_to_free, state); | |
| } | |
| // Measure the time taken to free a random allocated block, and allocate a | |
| // new one, using the slab allocator. The block is allocated from the pool | |
| // initialized by one of the DECLARE_STATIC_SLAB_ALLOCATOR_STORAGE statements | |
| // at the end of this file. This benchmark attempts to quantify the effects of | |
| // memory fragmentation. | |
| template <typename AllocatorT> | |
| bool RetainAndFreeRandom(perftest::RepeatState* state, size_t num_bufs_to_retain) { | |
| // Generate the sequence of indexes of buffers to replace. | |
| std::vector<size_t> buf_to_free(num_bufs_to_retain); | |
| std::random_device rand_dev; | |
| std::iota(buf_to_free.begin(), buf_to_free.end(), 0); | |
| std::shuffle(buf_to_free.begin(), buf_to_free.end(), rand_dev); | |
| return RetainAndFree<AllocatorT>(buf_to_free, state); | |
| } | |
| /*** Linkage and instantiation ***/ | |
| using RetainedMemPerfTest = bool (*)(perftest::RepeatState* state, size_t num_bufs_to_retain); | |
| template <typename AllocatorTraits, RetainedMemPerfTest PerfTest> | |
| void RegisterTest(const char* bench_name) { | |
| // The maximum value of 32768KB below was chosen empirically, as the point at | |
| // which allocators started showing scaling behaviors on Eve. | |
| for (size_t total_size_kbytes : {8, 32, 128, 512, 2048, 8192, 32768}) { | |
| const size_t total_size_bytes = total_size_kbytes * 1024; | |
| perftest::RegisterTest( | |
| fbl::StringPrintf("MemAlloc/%s/%s/%s/%zuKbytes", AllocatorTraits::kName, bench_name, | |
| AllocatorTraits::GetConfigAsString().c_str(), total_size_kbytes) | |
| .c_str(), | |
| PerfTest, total_size_bytes / AllocatorTraits::kUserBufSize); | |
| } | |
| } | |
| using NoRetainedMemPerfTest = bool (*)(perftest::RepeatState* state); | |
| template <typename AllocatorTraits, NoRetainedMemPerfTest PerfTest> | |
| void RegisterTest(const char* bench_name) { | |
| perftest::RegisterTest(fbl::StringPrintf("MemAlloc/%s/%s/%s", AllocatorTraits::kName, bench_name, | |
| AllocatorTraits::GetConfigAsString().c_str()) | |
| .c_str(), | |
| PerfTest); | |
| } | |
| #define REGISTER_PERF_TEST_INSTANCE(ALLOCATION_PATTERN, ALLOCATOR_TRAITS) \ | |
| RegisterTest<ALLOCATOR_TRAITS, ALLOCATION_PATTERN<ALLOCATOR_TRAITS::AllocT>>(#ALLOCATION_PATTERN); | |
| #define REGISTER_PERF_TEST(ALLOCATION_PATTERN) \ | |
| do { \ | |
| REGISTER_PERF_TEST_INSTANCE(ALLOCATION_PATTERN, StaticSmallBlockAllocatorTraits); \ | |
| REGISTER_PERF_TEST_INSTANCE(ALLOCATION_PATTERN, StaticLargeBlockAllocatorTraits); \ | |
| REGISTER_PERF_TEST_INSTANCE(ALLOCATION_PATTERN, InstancedSmallBlockAllocatorTraits); \ | |
| REGISTER_PERF_TEST_INSTANCE(ALLOCATION_PATTERN, InstancedLargeBlockAllocatorTraits); \ | |
| REGISTER_PERF_TEST_INSTANCE(ALLOCATION_PATTERN, HeapSmallBlockAllocatorTraits); \ | |
| REGISTER_PERF_TEST_INSTANCE(ALLOCATION_PATTERN, HeapLargeBlockAllocatorTraits); \ | |
| } while (0) | |
| void RegisterTests() { | |
| REGISTER_PERF_TEST(AllocAndFree); | |
| REGISTER_PERF_TEST(RetainAndFreeOldest); | |
| REGISTER_PERF_TEST(RetainAndFreeRandom); | |
| } | |
| PERFTEST_CTOR(RegisterTests); | |
| } // namespace | |
| #define DECLARE_STATIC_STORAGE(SLAB_TRAITS) \ | |
| DECLARE_STATIC_SLAB_ALLOCATOR_STORAGE( \ | |
| SLAB_TRAITS, (kLiveAllocLimitBytes / (fbl::SlabAllocator<SLAB_TRAITS>::AllocsPerSlab * \ | |
| SLAB_TRAITS::kUserBufSize)) + \ | |
| 1 /* Round up */); | |
| DECLARE_STATIC_STORAGE(StaticSmallBlockAllocatorTraits); | |
| DECLARE_STATIC_STORAGE(StaticLargeBlockAllocatorTraits); |