Skip to content
Snippets Groups Projects
Commit 66bc00e9 authored by o2-lysak's avatar o2-lysak
Browse files

Task 3 completed

parent 4dedb7cc
No related branches found
No related tags found
No related merge requests found
......@@ -21,6 +21,14 @@ The Bump Allocator is a simple memory allocation strategy implemented in C++. It
2. Allocation: Memory is allocated linearly from the start of the block. Each allocation moves the 'next' pointer forward.
3. Deallocation: The allocator does not support individual deallocation. Instead, it resets the 'next' pointer to the start once all allocations are freed.
## Running Task 1
```
clang++ -std=c++17 -o main task1/main.cpp
./main
```
## Breakdown of the Implementation
1. Creation of Heap Space on Allocator Initialisation:
- The constructor of `BumpAllocator` takes a `size` parameter and allocates a memory block of that size. This behavior meets the requirement of creating heap space when the allocator is instantiated.
......@@ -126,7 +134,103 @@ Unit tests are designed to ensure the Bump Allocator functions correctly under v
## Running the Tests
```
clang++ -std=c++17 -o test_BumpAllocator tests/test_BumpAllocator.cpp task1/BumpAllocator.tpp simpletest/simpletest.cpp
./test_BumpAllocator
```
![image](images/running-task2.png)
This indicates that the allocator is functioning as expected across a range of different scenarios, proving the allocator to be robust and reliable, handling various scenarios effectively.
## Task 3:
## Overview
This task focuses on the development and performance evaluation of two custom memory allocators: `UpwardBumpAllocator` and `DownwardBumpAllocator`. The goal is to understand the performance characteristics of each allocator under various allocation scenarios and to compare the efficiency of upward vs. downward allocation strategies.
## Allocators Overview
- `UpwardBumpAllocator`: Allocates memory in an upward direction, starting from the beginning of a pre-allocated memory block.
- `DownwardBumpAllocator`: Allocates memory in a downward direction, starting from the end of a pre-allocated memory block.
## Benchmarking Approach
The benchmarking process is designed to test the allocators under different conditions, including small, medium, large, and varied allocation sizes.
### Key Components
- Benchmark Utility: A flexible benchmarking function capable of handling both functions without arguments and those with varying argument types and numbers.
- Test Functions: Custom functions designed to test the allocators with different allocation patterns.
- Iteration Strategy: Each test is iterated 1000 times to average out transient system anomalies and provide consistent results.
## Test Scenarios
- Small Allocations: Tests the allocator's performance with numerous small-size allocations.
- Medium Allocations: Assesses performance with a moderate number of medium-size allocations.
- Large Allocations: Evaluates how the allocator handles fewer, but larger allocations.
- Varied Allocations: Mixes different allocation sizes to simulate more realistic usage patterns.
## Custom Tests
Custom allocation tests to demonstrate the extended capability of the benchmark suite where also implemented. These tests are tailored for specific use cases and provide insights into the allocator's performance under custom scenarios.
## Running the Tets:
```
clang++ -std=c++17 -o main task3/src/main.cpp
./main
```
## Results
![image](images/running-task3.png)
## Results Analysis
The benchmarking results for the `UpwardBumpAllocator` and `DownwardBumpAllocator` reveal some interesting patterns in their performance characteristics under different allocation scenarios. Here's a detailed analysis based on the provided data:
Small Allocations
- `UpwardBumpAllocator`: 0.00083412 ms
- `DownwardBumpAllocator`: 0.000909721 ms
In the case of small allocations, the `UpwardBumpAllocator` performs slightly better than the `DownwardBumpAllocator`. This could be attributed to the more straightforward incrementing pointer mechanism in upward allocation, which seems to be marginally more efficient for handling many small allocations.
Medium Allocations
- `UpwardBumpAllocator`: 0.000437511 ms
- `DownwardBumpAllocator`: 0.000474111 ms
For medium-sized allocations, a similar trend is observed with the `UpwardBumpAllocator` showing a slight performance advantage. This suggests that the upward allocation mechanism continues to maintain its efficiency even as the allocation size increases.
Large Allocations
- `UpwardBumpAllocator`: 0.000113601 ms
- `DownwardBumpAllocator`: 0.000116004 ms
The difference in performance becomes less pronounced with large allocations, indicating that both allocators handle large, contiguous memory allocations with similar efficiency. This is likely due to the reduced overhead of fewer allocation calls.
Varied Allocations
- `UpwardBumpAllocator`: 0.000526411 ms
- `DownwardBumpAllocator`: 0.000581314 ms
Under varied allocation patterns, the `UpwardBumpAllocator` again shows a slight edge. This indicates its consistent performance across different allocation sizes and patterns.
Custom Tests
- `UpwardBumpAllocator`: 0.0002 ms
- `DownwardBumpAllocator`: 0.0001 ms
Interestingly, in the custom test scenario, the `DownwardBumpAllocator` outperforms the `UpwardBumpAllocator`. This suggests that in certain specific use cases, the downward allocation strategy might have advantages, possibly due to more favorable memory access patterns or alignment handling.
## Conclusion
Overall, the `UpwardBumpAllocator` tends to have a slight performance advantage in most of the standard test scenarios, likely due to its simpler pointer arithmetic. However, the `DownwardBumpAllocator` demonstrates competitive performance, especially in scenarios tailored to its allocation strategy, as seen in the custom test results. These findings highlight the importance of choosing an allocation strategy based on specific application requirements and the nature of the memory allocation patterns.
images/running-task3.png

21.8 KiB

//main.cpp
#include "BumpAllocator.hpp" // Include the BumpAllocator header
#include <iostream> // Include the standard I/O header for console output
......
// benchmark.hpp
#ifndef BENCHMARK_HPP
#define BENCHMARK_HPP
#include <chrono>
#include <functional>
// Benchmark function for void function(void)
double benchmark(std::function<void()> function) {
auto start = std::chrono::high_resolution_clock::now();
function(); // Execute the function
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::milli> duration = end - start;
return duration.count(); // Return duration in milliseconds
}
// Benchmark function for any function type with any number and type of arguments
template<typename F, typename... Args>
double benchmark(F function, Args&&... args) {
auto start = std::chrono::high_resolution_clock::now();
function(std::forward<Args>(args)...); // Forward arguments to the function
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::milli> duration = end - start;
return duration.count(); // Return duration in milliseconds
}
#endif // BENCHMARK_HPP
// include/downward_allocator.hpp
#ifndef DOWNWARD_BUMP_ALLOCATOR_HPP // Start of the include guard
#define DOWNWARD_BUMP_ALLOCATOR_HPP
#include <iostream> // Include the standard I/O header for console output
#include <cstdint> // Include the header for fixed-width integer types
#include <algorithm> // Include the standard algorithms library
// Template declaration for the DownwardBumpAllocator class
template <typename T>
class DownwardBumpAllocator {
private:
T* start; // Pointer to the start of the allocated memory block
T* next; // Pointer to the next free space in the memory block
size_t capacity; // Total capacity of the allocator (number of objects it can hold)
size_t alloc_count; // Counter to keep track of the number of active allocations
// Helper method to align a given pointer 'ptr' to the specified 'alignment'
T* align(std::size_t alignment, T* ptr) {
auto p = reinterpret_cast<std::uintptr_t>(ptr); // Cast the pointer to an integer for manipulation
auto aligned = (p + alignment - 1) & ~(alignment - 1); // Perform alignment
return reinterpret_cast<T*>(aligned); // Cast the aligned integer back to a pointer and return
}
public:
DownwardBumpAllocator(size_t size); // Constructor declaration
~DownwardBumpAllocator(); // Destructor declaration
T* alloc(size_t n); // Method to allocate memory for 'n' objects
void dealloc(); // Method to deallocate memory
size_t getAllocCount() const; // Getter for the number of active allocations
size_t getNextPointerDifference() const; // Getter for the difference between 'next' and 'start'
size_t getRemainingCapacity() const; // Getter for the remaining capacity in the allocator
};
#include "downward_allocator.tpp" // Include the implementation of the template
#endif // BUMP_ALLOCATOR_HPP // End of the include guard
// downward_allocator.tpp
// Constructor for the UpwardBumpAllocator class
template <typename T>
DownwardBumpAllocator<T>::DownwardBumpAllocator(size_t size)
: capacity(size), alloc_count(0) { // Initialise capacity with the size and alloc_count with 0
start = new T[size]; // Allocate a memory block to hold 'size' number of T objects, assign it to 'start'
next = start + size; // Initialise 'next' to point to the end of the block
}
// Destructor for the DownwardBumpAllocator class
template <typename T>
DownwardBumpAllocator<T>::~DownwardBumpAllocator() {
delete[] start; // Deallocate the memory block pointed to by 'start'
}
// Method to allocate memory for 'n' objects of type T
template <typename T>
T* DownwardBumpAllocator<T>::alloc(size_t n) {
if (n == 0) {
return nullptr;
}
T* aligned_next = align(alignof(T), next - n); // Align for downward allocation
if (aligned_next >= start) { // Check if there is enough space left
next = aligned_next; // Move 'next' backward by 'n' object sizes
alloc_count++;
return next; // Return the pointer to the newly allocated memory
}
return nullptr; // Return nullptr if there isn't enough space
}
// Method to deallocate memory
template <typename T>
void DownwardBumpAllocator<T>::dealloc() {
if (alloc_count > 0) { // Check if there are any active allocations
alloc_count--; // Decrement the allocation count
if (alloc_count == 0) { // If all allocations have been deallocated
next = start; // Reset 'next' to point to the start of the memory block
}
}
}
// Method to get the current allocation count
template <typename T>
size_t DownwardBumpAllocator<T>::getAllocCount() const {
return alloc_count; // Return the current number of active allocations
}
// Method to get the difference between 'next' and 'start' pointers
template <typename T>
size_t DownwardBumpAllocator<T>::getNextPointerDifference() const {
return next - start; // Return the number of T objects between 'start' and 'next'
}
// Method to get the remaining capacity in terms of number of objects of type T
template <typename T>
size_t DownwardBumpAllocator<T>::getRemainingCapacity() const {
return start + capacity - next; // Return the difference between total capacity and used space
}
// include/upward_allocator.hpp
#ifndef UPWARD_BUMP_ALLOCATOR_HPP // Start of the include guard
#define UPWARD_BUMP_ALLOCATOR_HPP
#include <iostream> // Include the standard I/O header for console output
#include <cstdint> // Include the header for fixed-width integer types
#include <algorithm> // Include the standard algorithms library
// Template declaration for the UpwardBumpAllocator class
template <typename T>
class UpwardBumpAllocator {
private:
T* start; // Pointer to the start of the allocated memory block
T* next; // Pointer to the next free space in the memory block
size_t capacity; // Total capacity of the allocator (number of objects it can hold)
size_t alloc_count; // Counter to keep track of the number of active allocations
// Helper method to align a given pointer 'ptr' to the specified 'alignment'
T* align(std::size_t alignment, T* ptr) {
auto p = reinterpret_cast<std::uintptr_t>(ptr); // Cast the pointer to an integer for manipulation
auto aligned = (p + alignment - 1) & ~(alignment - 1); // Perform alignment
return reinterpret_cast<T*>(aligned); // Cast the aligned integer back to a pointer and return
}
public:
UpwardBumpAllocator(size_t size); // Constructor declaration
~UpwardBumpAllocator(); // Destructor declaration
T* alloc(size_t n); // Method to allocate memory for 'n' objects
void dealloc(); // Method to deallocate memory
size_t getAllocCount() const; // Getter for the number of active allocations
size_t getNextPointerDifference() const; // Getter for the difference between 'next' and 'start'
size_t getRemainingCapacity() const; // Getter for the remaining capacity in the allocator
};
#include "upward_allocator.tpp" // Include the implementation of the template
#endif // BUMP_ALLOCATOR_HPP // End of the include guard
// src/upward_allocator.cpp
// Constructor for the UpwardBumpAllocator class
template <typename T>
UpwardBumpAllocator<T>::UpwardBumpAllocator(size_t size)
: capacity(size), alloc_count(0) { // Initialise capacity with the size and alloc_count with 0
start = new T[size]; // Allocate a memory block to hold 'size' number of T objects, assign it to 'start'
next = start; // Initialise 'next' to point at the start of the block
}
// Destructor for the UpwardBumpAllocator class
template <typename T>
UpwardBumpAllocator<T>::~UpwardBumpAllocator() {
delete[] start; // Deallocate the memory block pointed to by 'start'
}
// Method to allocate memory for 'n' objects of type T
template <typename T>
T* UpwardBumpAllocator<T>::alloc(size_t n) {
T* aligned_next = align(alignof(T), next); // Align 'next' pointer according to the alignment requirements of type T
if (aligned_next + n <= start + capacity) { // Check if there is enough space left to allocate 'n' objects
T* current = aligned_next; // Store the current position of 'aligned_next'
next = aligned_next + n; // Move 'next' forward by 'n' object sizes
alloc_count++; // Increment the allocation count
return current; // Return the pointer to the newly allocated memory
}
return nullptr; // Return nullptr if there isn't enough space for the requested allocation
}
// Method to deallocate memory
template <typename T>
void UpwardBumpAllocator<T>::dealloc() {
if (alloc_count > 0) { // Check if there are any active allocations
alloc_count--; // Decrement the allocation count
if (alloc_count == 0) { // If all allocations have been deallocated
next = start; // Reset 'next' to point to the start of the memory block
}
}
}
// Method to get the current allocation count
template <typename T>
size_t UpwardBumpAllocator<T>::getAllocCount() const {
return alloc_count; // Return the current number of active allocations
}
// Method to get the difference between 'next' and 'start' pointers
template <typename T>
size_t UpwardBumpAllocator<T>::getNextPointerDifference() const {
return next - start; // Return the number of T objects between 'start' and 'next'
}
// Method to get the remaining capacity in terms of number of objects of type T
template <typename T>
size_t UpwardBumpAllocator<T>::getRemainingCapacity() const {
return start + capacity - next; // Return the difference between total capacity and used space
}
#include "../include/upward_allocator.hpp" // Include the header for UpwardBumpAllocator
#include "../include/downward_allocator.hpp" // Include the header for DownwardBumpAllocator
#include "../include/benchmark.hpp" // Include the header for the benchmark utility
#include <iostream>
const size_t capacity = 1000; // Define the capacity for the allocator (number of elements it can handle)
const int iterations = 1000; // Define the number of iterations for the benchmark loop
// Template function to test a given number of allocations of a specified size
template<typename Allocator>
void testAllocations(Allocator& allocator, int numAllocations, size_t size) {
for (int i = 0; i < numAllocations; ++i) { // Loop over the number of allocations
allocator.alloc(size); // Allocate memory of the specified size
}
allocator.dealloc(); // Deallocate all allocated memory at once (specific to bump allocators)
}
// Template function to perform a custom number of allocations with a custom size
template<typename Allocator, typename SizeType>
void testCustomAllocations(Allocator& allocator, SizeType size, int count) {
for (int i = 0; i < count; ++i) { // Loop over the count for allocations
allocator.alloc(size); // Allocate memory of the specified size
}
allocator.dealloc(); // Deallocate all allocated memory at once
}
// Function to run the series of tests for a given allocator
template<typename Allocator>
void runTests(const std::string& allocatorName) {
double timeSmall = 0.0, timeMedium = 0.0, timeLarge = 0.0, timeVaried = 0.0; // Initialise variables to track time for each test
for (int i = 0; i < iterations; ++i) { // Loop over the defined number of iterations
{ // Start a new scope for small allocations test
Allocator allocator(capacity); // Create an instance of the allocator with the defined capacity
timeSmall += benchmark([&]() { testAllocations<Allocator>(allocator, 100, 1); }); // Benchmark small allocations and add the time to the total
} // End of scope - the allocator is destructed here
{ // Start a new scope for medium allocations test
Allocator allocator(capacity); // Create an instance of the allocator with the defined capacity
timeMedium += benchmark([&]() { testAllocations<Allocator>(allocator, 50, 10); }); // Benchmark medium allocations and add the time to the total
} // End of scope - the allocator is destructed here
{ // Start a new scope for large allocations test
Allocator allocator(capacity); // Create an instance of the allocator with the defined capacity
timeLarge += benchmark([&]() { testAllocations<Allocator>(allocator, 10, 100); }); // Benchmark large allocations and add the time to the total
} // End of scope - the allocator is destructed here
{ // Start a new scope for varied allocations test
Allocator allocator(capacity); // Create an instance of the allocator with the defined capacity
timeVaried += benchmark([&]() { testAllocations<Allocator>(allocator, 30, 1);
testAllocations<Allocator>(allocator, 20, 50);
testAllocations<Allocator>(allocator, 10, 100); }); // Benchmark varied allocations and add the time to the total
} // End of scope - the allocator is destructed here
}
// Output the average time for each type of allocation test
std::cout << allocatorName << " - Average Small Allocations Time: " << timeSmall / iterations << " ms\n";
std::cout << allocatorName << " - Average Medium Allocations Time: " << timeMedium / iterations << " ms\n";
std::cout << allocatorName << " - Average Large Allocations Time: " << timeLarge / iterations << " ms\n";
std::cout << allocatorName << " - Average Varied Allocations Time: " << timeVaried / iterations << " ms\n";
}
int main() {
runTests<UpwardBumpAllocator<int>>("UpwardBumpAllocator"); // Run tests for the UpwardBumpAllocator
runTests<DownwardBumpAllocator<int>>("DownwardBumpAllocator"); // Run tests for the DownwardBumpAllocator
// Run a custom test for the UpwardBumpAllocator
UpwardBumpAllocator<int> customAllocator(capacity); // Create an instance of UpwardBumpAllocator
double customTestDuration = benchmark(testCustomAllocations<UpwardBumpAllocator<int>, size_t>, customAllocator, 50, 5); // Benchmark the custom test
std::cout << "Custom Test Duration for UpwardBumpAllocator: " << customTestDuration << " ms\n"; // Output the result
// Run a custom test for the DownwardBumpAllocator
DownwardBumpAllocator<int> customAllocatorDown(capacity); // Create an instance of DownwardBumpAllocator
customTestDuration = benchmark(testCustomAllocations<DownwardBumpAllocator<int>, size_t>, customAllocatorDown, 50, 5); // Benchmark the custom test
std::cout << "Custom Test Duration for DownwardBumpAllocator: " << customTestDuration << " ms\n"; // Output the result
return 0; // End of main function
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment