diff --git a/README.md b/README.md
index 381360af3c9bfe7aeea0c178b577eecaa03b5454..7530229ec52fa3aead04db91a99d96144a45590b 100644
--- a/README.md
+++ b/README.md
@@ -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.
diff --git a/images/running-task3.png b/images/running-task3.png
new file mode 100644
index 0000000000000000000000000000000000000000..40d6253316b507ea40bb8c48b4476f6e4b176f13
Binary files /dev/null and b/images/running-task3.png differ
diff --git a/task1/main.cpp b/task1/main.cpp
index e15260e7135c6da820f80f8a78d42e948594ae41..8c3e5d4b0398ec7e4287d249d4a2f89a064635b6 100644
--- a/task1/main.cpp
+++ b/task1/main.cpp
@@ -1,3 +1,4 @@
+//main.cpp
 #include "BumpAllocator.hpp"  // Include the BumpAllocator header
 #include <iostream>  // Include the standard I/O header for console output
 
diff --git a/task3/include/benchmark.hpp b/task3/include/benchmark.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..74e3fa8fe896d7d269b57145038017c62a28d637
--- /dev/null
+++ b/task3/include/benchmark.hpp
@@ -0,0 +1,28 @@
+// 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
+
diff --git a/task3/include/downward_allocator.hpp b/task3/include/downward_allocator.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..3200febea0d35c677b4695a0870f241339823cfa
--- /dev/null
+++ b/task3/include/downward_allocator.hpp
@@ -0,0 +1,37 @@
+// 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
diff --git a/task3/include/downward_allocator.tpp b/task3/include/downward_allocator.tpp
new file mode 100644
index 0000000000000000000000000000000000000000..b300699ca280751ebba1981ab2459b9af673070a
--- /dev/null
+++ b/task3/include/downward_allocator.tpp
@@ -0,0 +1,61 @@
+// 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
+}
diff --git a/task3/include/upward_allocator.hpp b/task3/include/upward_allocator.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..15dcc67747a01cb6ffecac9b048b9ac1ac26d8f7
--- /dev/null
+++ b/task3/include/upward_allocator.hpp
@@ -0,0 +1,37 @@
+// 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
diff --git a/task3/include/upward_allocator.tpp b/task3/include/upward_allocator.tpp
new file mode 100644
index 0000000000000000000000000000000000000000..32cb29b1ded0029ddf3b8f899d5d9d74e46a81d8
--- /dev/null
+++ b/task3/include/upward_allocator.tpp
@@ -0,0 +1,57 @@
+// 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
+}
diff --git a/task3/src/main.cpp b/task3/src/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c0475ea1b20eb5a36490acb82739c4f083a2587f
--- /dev/null
+++ b/task3/src/main.cpp
@@ -0,0 +1,75 @@
+#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
+}