Skip to content

Commit

Permalink
perf: optimize condition creation with ObjectPool (#3212)
Browse files Browse the repository at this point in the history
This introduces an ObjectPool for managing Condition objects, replacing frequent calls to std::make_shared with a lock-free pooling allocator. This optimization reduces memory allocation overhead, improves performance, and enhances thread safety in scenarios where Conditions are created and destroyed frequently, such as in onThink events or condition updates.

By reusing objects from the pool instead of allocating and deallocating memory repeatedly, this change significantly reduces the strain on the memory management system and improves runtime efficiency. The pool is designed to handle up to 1024 objects per Condition type and supports safe, high-performance multithreaded operations.
  • Loading branch information
dudantas authored Jan 5, 2025
1 parent 2ca896e commit 4778078
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 15 deletions.
33 changes: 18 additions & 15 deletions src/creatures/combat/condition.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include "creatures/creature.hpp"
#include "creatures/players/player.hpp"
#include "server/network/protocol/protocolgame.hpp"
#include "utils/object_pool.hpp"

/**
* Condition
Expand Down Expand Up @@ -215,41 +216,41 @@ std::shared_ptr<Condition> Condition::createCondition(ConditionId_t id, Conditio
case CONDITION_DAZZLED:
case CONDITION_CURSED:
case CONDITION_BLEEDING:
return std::make_shared<ConditionDamage>(id, type, buff, subId);
return ObjectPool<ConditionDamage, 1024>::allocateShared(id, type, buff, subId);

case CONDITION_HASTE:
case CONDITION_PARALYZE:
return std::make_shared<ConditionSpeed>(id, type, ticks, buff, subId, param);
return ObjectPool<ConditionSpeed, 1024>::allocateShared(id, type, ticks, buff, subId, param);

case CONDITION_INVISIBLE:
return std::make_shared<ConditionInvisible>(id, type, ticks, buff, subId);
return ObjectPool<ConditionInvisible, 1024>::allocateShared(id, type, ticks, buff, subId);

case CONDITION_OUTFIT:
return std::make_shared<ConditionOutfit>(id, type, ticks, buff, subId);
return ObjectPool<ConditionOutfit, 1024>::allocateShared(id, type, ticks, buff, subId);

case CONDITION_LIGHT:
return std::make_shared<ConditionLight>(id, type, ticks, buff, subId, param & 0xFF, (param & 0xFF00) >> 8);
return ObjectPool<ConditionLight, 1024>::allocateShared(id, type, ticks, buff, subId, param & 0xFF, (param & 0xFF00) >> 8);

case CONDITION_REGENERATION:
return std::make_shared<ConditionRegeneration>(id, type, ticks, buff, subId);
return ObjectPool<ConditionRegeneration, 1024>::allocateShared(id, type, ticks, buff, subId);

case CONDITION_SOUL:
return std::make_shared<ConditionSoul>(id, type, ticks, buff, subId);
return ObjectPool<ConditionSoul, 1024>::allocateShared(id, type, ticks, buff, subId);

case CONDITION_ATTRIBUTES:
return std::make_shared<ConditionAttributes>(id, type, ticks, buff, subId);
return ObjectPool<ConditionAttributes, 1024>::allocateShared(id, type, ticks, buff, subId);

case CONDITION_SPELLCOOLDOWN:
return std::make_shared<ConditionSpellCooldown>(id, type, ticks, buff, subId);
return ObjectPool<ConditionSpellCooldown, 1024>::allocateShared(id, type, ticks, buff, subId);

case CONDITION_SPELLGROUPCOOLDOWN:
return std::make_shared<ConditionSpellGroupCooldown>(id, type, ticks, buff, subId);
return ObjectPool<ConditionSpellGroupCooldown, 1024>::allocateShared(id, type, ticks, buff, subId);

case CONDITION_MANASHIELD:
return std::make_shared<ConditionManaShield>(id, type, ticks, buff, subId);
return ObjectPool<ConditionManaShield, 1024>::allocateShared(id, type, ticks, buff, subId);

case CONDITION_FEARED:
return std::make_shared<ConditionFeared>(id, type, ticks, buff, subId);
return ObjectPool<ConditionFeared, 1024>::allocateShared(id, type, ticks, buff, subId);

case CONDITION_ROOTED:
case CONDITION_INFIGHT:
Expand All @@ -261,11 +262,13 @@ std::shared_ptr<Condition> Condition::createCondition(ConditionId_t id, Conditio
case CONDITION_CHANNELMUTEDTICKS:
case CONDITION_YELLTICKS:
case CONDITION_PACIFIED:
return std::make_shared<ConditionGeneric>(id, type, ticks, buff, subId);
return ObjectPool<ConditionGeneric, 1024>::allocateShared(id, type, ticks, buff, subId);

case CONDITION_BAKRAGORE:
return std::make_shared<ConditionGeneric>(id, type, ticks, buff, subId, isPersistent);
return ObjectPool<ConditionGeneric, 1024>::allocateShared(id, type, ticks, buff, subId, isPersistent);

case CONDITION_GOSHNARTAINT:
return std::make_shared<ConditionGeneric>(id, type, ticks, buff, subId);
return ObjectPool<ConditionGeneric, 1024>::allocateShared(id, type, ticks, buff, subId);

default:
return nullptr;
Expand Down
84 changes: 84 additions & 0 deletions src/utils/object_pool.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* Canary - A free and open-source MMORPG server emulator
* Copyright (©) 2019-2024 OpenTibiaBR <[email protected]>
* Repository: https://github.com/opentibiabr/canary
* License: https://github.com/opentibiabr/canary/blob/main/LICENSE
* Contributors: https://github.com/opentibiabr/canary/graphs/contributors
* Website: https://docs.opentibiabr.com/
*/

#pragma once

#include "utils/lockfree.hpp"

/**
* @brief A lock-free object pool for efficient memory allocation and reuse.
*
* This class provides an efficient mechanism for managing the allocation
* and deallocation of objects, reducing the overhead associated with
* frequent memory operations. It uses a lock-free structure to ensure
* thread safety and high performance in multithreaded environments.
*
* @tparam T The type of objects managed by the pool.
* @tparam CAPACITY The maximum number of objects that can be held in the pool.
*/
template <typename T, size_t CAPACITY>
class ObjectPool {
public:
/**
* @brief The allocator type used for managing object memory.
*/
using Allocator = LockfreePoolingAllocator<T, CAPACITY>;

/**
* @brief Allocates an object from the pool and returns it as a `std::shared_ptr`.
*
* The object is constructed in place using the provided arguments.
* The `std::shared_ptr` includes a custom deleter that returns the object
* to the pool when it is no longer needed.
*
* @tparam Args The types of the arguments used to construct the object.* @param args The arguments forwarded to the constructor of the object.
* @return A `std::shared_ptr` managing the allocated object, or `nullptr` if the pool is empty.
*/
template <typename... Args>
static std::shared_ptr<T> allocateShared(Args &&... args) {
T* obj = allocator.allocate(1);
if (obj) {
// Construct the object in place
std::construct_at(obj, std::forward<Args>(args)...);

// Return a shared_ptr with a custom deleter
return std::shared_ptr<T>(obj, [](T* ptr) {
std::destroy_at(ptr); // Destroy the object
allocator.deallocate(ptr, 1); // Return to the pool
});
}
// Return nullptr if the pool is empty
return nullptr;
}

static void clear() {
allocator.clear();
}

/**
* @brief Preallocates a specified number of objects in the pool.
*
* This method allows you to populate the pool with preallocated objects
* to improve performance by reducing the need for dynamic allocations at runtime.
*
* @param count The number of objects to preallocate.
*/
static void preallocate(size_t count) {
LockfreeFreeList<T, CAPACITY>::preallocate(count);
}

private:
/**
* @brief The allocator instance used to manage object memory.
*/
static Allocator allocator;
};

template <typename T, size_t CAPACITY>
typename ObjectPool<T, CAPACITY>::Allocator ObjectPool<T, CAPACITY>::allocator;

0 comments on commit 4778078

Please sign in to comment.