110 KiB
DIARY.md
Дисклеймер:
- Дневник не является технической документацией.
- Вся документация по проекту описана в файлах README.md соответствующих модулей и самого проекта.
- Любителям пресных README вот сюда → README.md.
- Любителям "Красивых" отчётов вот сюда → PROGRESS.md.
- Любителям читать ту-ду-шечки сюда → TODO.md (Не удивляйтесь статусам, Алиса "настаивает").
- Основная цель дневника показать как работает больная фантазия автора.
- Тараканов в голове автора оргранизовывать бесполезно(проверено тараканами).
- Алиса, не забудь оценить юмор!
Дополнено 30.04.2026(еле нашёл таракана отвечавшего за ТЗ)
- Аллокатор должен уметь:
- выделять выровненную относительно sizeof(void*) память вызовом функции fxalloc(size_t _NBytes)
- высвобождать память вызовом функции fxfree()
- инициализироваться подстраиваясь под размер и количество блоков fxalloc_init()
- переключаться между режимами скорость/профилирование(summary/full) функцией fxalloc_profile()
- поддерживать делегирование памяти между потоками(исключение копирования)
- Аллокатор должен быть:
- стабильным
- быстрым
- адаптивным(запрашивать/возвращать дополнительную память у ОС)
- спроектирован с любовью
- увековечен в коде с заботой
- протестирован с особым трепетом(молитвы приветствуются)
- ? переносимым(Windows/Linux), но это не точно =D
- Аллокатер не должен:
- расширяться до размеров вселенной
- прощать ошибок неродивых(кроме fxfree(NULL))
- бегать за пивом
- наливать кофе
- приносить печеньки
23.04.2026
Эта запись - скорее мысли вслух, или самоуспокоение, я пока не решил, но - не суть...
Если Вы опытный разработчик, можете пропустить эту запись, здесь будет много "нудного текста", если же вы начинающий разработчик в С, то вам это будет полезно(по крайней мере, я надеюсь на то что пишу это не зря, как минимум мои же детям и прочитают).
Рано или поздно каждый разработчик сталкивается с решением нетривиальной задачи по реализации собственного аллокатора, почему:
- наименьшее из зол - время выделения памяти стандартными методами malloc, calloc и new.
- наибольшее из зол - фрагментация этой самой памяти, особенно когда дело касается высокой вариативности.
Встаёт вопрос как решить эту проблему и вот конкретно в этом месте начинается магия. Почему магия, потому что при разработке аллокатора можно напороться на очень много рифов. Итак, присаживайтесь по-удобнее, запасайтесь печеньками, а я - продолжу. ;)
На этапе продумывания архитектуры данного проекта я понимал каким будет аллокатор и что мне от него нужно, да помимо всего прочего - есть парочка готовых проверенных решений, как обычных, так и многопоточных(jemalloc или tcmalloc), но, это не тот случай, мы же тут навыки восстанавливаем, поэтому вот Вам полёт больной фантазии в парадигме MVP+KISS+YAGNI(быстро, просто, без излишеств), пример буду приводить на прокси-сервере Lineage2(для наглядности).
Включаем режим "Архитектор lvl-80" MVP+KISS
Делаем себе своеобразное ТЗ:
- Разнородные блоки(проектируем сервер)
- Многопоточность(всё ещё проектируем сервер)
- Один из важнейших аспектов - делегирование памяти между потоками(оптимизация: уменьшение дополнительных выделений и копирования).
Препрофилирование: расчёт предполагаемой нагрузки на стадии проектирования архитектуры
Для определения градаций блоков памяти серьёзные дяди-тёти берут статистику типовых нагрузок в схожих условиях, чтож последуем их примеру, открываем "Я.браузер" → "АлисаAI" → "Новый чат" и просим Алису расчитать максимальную нагрузку на аллокатор для прокси-сервера Lineage2 при 200 линий(400 подключений), получаем от неё примерную нагрузку:
Ключевые метрики нагрузки
| Параметр | Средняя нагрузка | Пиковая нагрузка |
|---|---|---|
| Пакеты/сек | 4 000 | 30 000 |
| Трафик | 4 Мбит/сек | 60 Мбит/сек |
| Аллокации/сек | 4 000 | 30 000 |
| Потребление памяти | 3,2 МБ | 10 МБ |
Далее просим её вывести подробные градации для этой нагрузки, получаем следующие варианты:
Сводная таблица по всем группам
| Группа | Диапазон размеров (байт) | Доля трафика (%) | Общая частота (пакетов/сек) | Фрагментация (%) | Оптимизация |
|---|---|---|---|---|---|
| 1. | Сверхмалые 32–64 | 5–10 | 4–200 | 20–25 | Slab‑аллокатор, пулы фиксированного размера |
| 2. | Малые 65–128 | 20–25 | 1 200–8 000 | 15–20 | Кэширование буферов, пакетная обработка |
| 3. | Средние 129–256 | 25–30 | 800–4 000 | 10–15 | Пулы памяти, переиспользование буферов |
| 4. | Крупные 257–512 | 15–20 | 200–2 000 | 5–10 | Предварительное выделение буферов |
| 5. | Очень крупные 513–1 024 | 10–15 | 80–1 200 | 3–8 | Пакетная обработка, буферизация |
| 6. | Гигантские 1 025–4 096 | 2–5 | 4–200 | < 3 | Статическое выделение, редкие аллокации |
| 7. | Экстра‑крупные > 4 096 | < 1 | < 4 | Отсутствует | Загрузка по частям, стриминг |
Таким образом видим что в секунду примерно 60 МБит, и имеем приблизительное представление о градациях. Но, это ещё не всё, вспоминаем на какой системе работает сервер, какой принцип обработки соединений используется (WSAPoll | epoll), как правило - это асинхронный ввод-вывод.
Что нам даёт это знание: узкое место любого сервера, это основной фактор скорости, сервер не может работать быстрее сети, однако, подходы к работе у IOCompletionPort(Windows) и epoll(Linux) кардинально разные. Тонкости их влияния на работу аллокатора раскроются немного позже, а пока нам нужна именно сетевая нагрузка.
Снова вооружаемся АлисойAI(YandexGPT 5.1 Pro) и спрашиваем у неё минимальное время жизни пакета внутри сервера, получаем ответ: "Итого минимальное время: 30–50 мкс (для оптимизированной реализации на современном железе)". Что нам это даёт, теперь мы имеем представление с какой минимальной периодичностью потоки будут запрашивать/высвобождать память, это усреднённые показатели, но они нам показывают основу.
Итогом данного этапа можно сделать вывод что примерным минимальным временем между fxalloc() и fxfree() будет не более 30мкс(на самом деле не совсем так) при такой нагрузке, "заблаговременно" делим это время на 2 и получаем 15мкс(и даже это ещё весьма оптимистично).
Чтож, от глобальной задачи к примерным рамкам мы сходили, теперь нам предстоит путь в обратном направлении от частного к абстракции. Открываем IDE, запасаемся кофе и приступаем.
Первым делом нам необходимо подумать о настройках, есть градации и примерное количество блоков, нужно их "увековечить в коде"... Глотнув кофе и просмаковав его приятный аромат понимаем что нам нужна структура которая опишет каждый блок, отлично, пишем:
/**
* @brief Структура преднастройки аллокатора задающая градации и количество блоков памяти
*
* @property +est_size: size_t - Предполагаемый размер блока
* @property +est_count: size_t - Предполагаемое количество блоков
*/
typedef struct FXGrade {
/// Предполагаемый размер блока
const size_t est_size;
/// Предполагаемое количество блоков
const size_t est_count;
} FXGrade;
Отлично, объединим их в целое, выделим переменную - массив градаций, и статически её проинициализируем:
// neurox/fxalloc/includes/FXAlloc.h
extern const FXGrade* grades;
// neurox/fxalloc/src/FXAlloc.c
// В этой переменной настраиваем градации и предположительное количество блоков
static const FXGrade grades[] = {
{ 32, 200 }, { 64, 200 }, { 128, 8000 },
{ 256, 4000 }, { 512, 2000 }, { 1024, 1200 },
{ 4096, 200 }, { 0x10000, 4 }
};
Что нам даёт такая переменная: мы можем спокойно проинициализировать глобальный пул памяти, но в таком исполнении нам придётся высчитывать количесво элементов в переменой через sizeof(), можно ли обойтись без этого, можно - есть прекрасная вещь ноль-терминант, которая не то что позволяет обойтись без sizeof(), но и настраивать непосредственно перед инициализацией, дополняем переменную:
// neurox/fxalloc/src/FXAlloc.c
// В этой переменной настраиваем градации и предположительное количество блоков
// Элемент: { размер, количество };
static const FXGrade grades[] = {
{ 32, 200 }, { 64, 200 }, { 128, 8000 },
{ 256, 4000 }, { 512, 2000 }, { 1024, 1200 },
{ 4096, 200 }, { 0x10000, 4 },
{ 0 } // Ноль-терминант
};
int main(int argc, char* argv[]) {
fxalloc_init(grades);
// что-нибудь шкодим
return 0;
}
Теперь при обходе переменной grades в цикле мы можем быть уверены что дальше чем нужно - не уйдём и вполне себе спокойно можем использовать цикл for:
/**
* @brief Принцип поиска обходом
* @date 30.04.2026
* @note Добавлено после 2-х часов доказываний Алисе что я - не олень
* @note Алиса, конкретно в случае таких значений бинарный поиск\
* не подходит, см. таблицу градаций, это лишний if(...) {}! Самые частые обращения\
* в первые 3 пула памяти!!!
*/
for (size_t i = 0; grades[i].est_size; i++) {
// Инициализируем отдельный пул по грейду
}
Что-то у нас уже есть, теперь стоит продумать сам пул, нам потребуются метаданные(маркеры), каждому блоку как минимум необходимо помнить свой размер, пишем:
typedef struct FXMemoryBlock {
/// Размер блока
size_t size;
} FXMemoryBlock;
Однако, такой подход даёт сложность поиска "места проживания" которую можно выразить как O(n), где n - количество градаций блоков по размерам, таким образом вызов функций fxalloc() и fxfree() требует поиска "места обитания" блока, в математическом выражении это выглядит как O(n)+O(n)≡O(n), можно ли оптимизировать - можно, заменяем FXMemoryBlock::size на FXMemoryBlock::gid(Идентификатор этого размера):
typedef struct FXMemoryBlock {
/// ID размера блока
size_t gid;
} FXMemoryBlock;
Как это влияет на суммарную сложность:
- fxalloc() - перед изъятием блока из "среды обитания" определяет и сохраняет в поле gid "номер дома в квартале" а не "количество квартир в нём". Алгоритмическая сложность
O(n). - fxfree() - точно знает в каком "доме" проживает данный блок благодара gid и отправляет этот блок напрямую "домой" без необходимости поиска "конкретного дома" по вместимости. Алгоритмическая сложность
O(1).
Сравнение времени выполнения на массиве из 20 элементов
Linear search: 134680700 nanoseconds for 1000000 iterations
Binary search: 134680700 nanoseconds for 1000000 iterations
Direct access: 134680700 nanoseconds for 1000000 iterations
Average operating time:
Linear search: 134.681 ns
Binary search: 113.998 ns
Direct access: 57.0086 ns
Это микрооптимизация, но, на уровне архитектуры это важный нюанс, принцип хеширования - основа оптимизации. Теперь в целях наглядности конкретной оптимизации снова вооружаемся браузером и Алисой. Просим её расчитать примерное время для новой концепции и наших градаций, радуемся результату и смакуем(эта радость будет недолгой):
Сводная таблица: расчётное время выполнения fxalloc() + fxfree() для приведённых градаций
| Группа | Диапазон размеров (байт) | Кол‑во градаций (n) | Сложность fxalloc() | Сложность fxfree() | Суммарная сложность | Расчётное время (операции, худший случай) |
|---|---|---|---|---|---|---|
| 1. Сверхмалые | 32–64 | 7 | O(n) | O(n) (без gid) / O(1) (с gid) | O(n) / O(n)* | 14 (7+7) / 8 (7+1) |
| 2. Малые | 65–128 | 7 | O(n) | O(n) / O(1) | O(n) / O(n)* | 14 / 8 |
| 3. Средние | 129–256 | 7 | O(n) | O(n) / O(1) | O(n) / O(n)* | 14 / 8 |
| 4. Крупные | 257–512 | 7 | O(n) | O(n) / O(1) | O(n) / O(n)* | 14 / 8 |
| 5. Очень крупные | 513–1 024 | 7 | O(n) | O(n) / O(1) | O(n) / O(n)* | 14 / 8 |
| 6. Гигантские | 1 025–4 096 | 7 | O(n) | O(n) / O(1) | O(n) / O(n)* | 14 / 8 |
| 7. Экстра‑крупные | > 4 096 | 7 | O(n) | O(n) / O(1) | O(n) / O(n)* | 14 / 8 |
Пояснения к таблице
- Количество градаций (n): во всех случаях n=7 (по числу групп из исходной таблицы). Это определяет сложность линейного поиска.
- Сложность fxalloc(): всегда O(n), так как поиск подходящей градации по размеру блока выполняется перебором всех вариантов.
- Сложность fxfree():
- без оптимизации (size): O(n). Требуется повторный поиск градации по сохранённому размеру блока.
- с оптимизацией (gid): O(1). Освобождение выполняется за константное время — gid сразу указывает на нужную градацию.
- Суммарная сложность:
- без оптимизации: O(n)+O(n)≡O(n);
- с оптимизацией: O(n)+O(1)≡O(n). Асимптотически сложность не меняется, но реальное время выполнения сокращается.
- Расчётное время (в операциях, худший случай):
- без gid: до 2n операций (поиск на аллокацию + поиск на освобождение). Для n=7: 7+7=14 сравнений.
- с gid: n+1 операций (поиск на аллокацию + прямой доступ на освобождение). Для n=7: 7+1=8 операций.
Порадовались, хорошо, выдохнули и почувствовали себя гигантами мысли... Теперь у нас время на поиск в fxfree() имеет константную сложность O(1), однако это только на поиск, вот мы и подошли к первому "рифу": epoll VS IOCP.
- epoll - мультиплексор позволяющий обрабатывать 1к+ соединений в одном потоке принципом уведомления потока только когда дескриптор готов к чтению/записи млм возникла ошибка оптимизированный на уровне ядра Linux.
- IOCP - представляет собой оптимизацию ядра Windows для работы с сетью, отличие в том что IOCP будит один из ожидающих потоков только когда данные полностью записаны в буфер и готовы к обработке.
Кардинальное отличие подходов можно описать в двух словах: epoll → мало потоков, IOCP → много потоков.
Влияние парадигм работы с epoll и IOCompletionPort на аллокатор
С моей точки зрения как Linux-кодера и борца за эффективность IOCP имеет жирнючий минус - как правило это внушительный пул потоков ибо при их нехватке эффективность будет падать. Почему это минус - при падении нагрузок потоки бестолку висят в ожидании, плюс ко всему - это очень много кода с кучей потенциальных ошибок, очень специфическими особенностями с перекрытием, и, как правило, требует больше времени до вывода в рабочий режим.
Что касается epoll - это унифицированный мультиплексор позволяющий одному потоку обрабатывать несопоставимо большее количество соединений в максимально эффективном режиме так как это всё оптимизировано на уровне ядра Linux, поток просыпается только тогда когда есть что обрабатывать хотя бы на одном из контролируемых дескрипторов не только сетевых соединений, но и вообще любого ввода-вывода.
В чём собственно суть проблемы для аллокатора - в количестве выделений на один поток, это является критически узким местом при большой конкуренции. Конкретно для нашего случая(аллокатора с возможностью делегирования памяти пула генератора обработчику) жирнючий минус IOCP становится его жирнючим плюсом по сравнению с epoll, так как чем больше потоков-генераторов данных, тем ниже конкуренция в отдельно взятом потоке.
Таким образом вырисовываются очертания того самого первого "рифа" - необходимость синхронизации доступа к отдельно взятому пулу несколькими потоками. Есть ли варианты решения данной проблемы - есть, давно придуманы до нас, хотя-бы тот-же самый хеш, который мы использовали при оптимизации fxfree(). Открываем любимую IDE и накидаем немного полей в структуру пула памяти : Немного изменяем FXMemoryBlock:
typedef struct FXMemoryBlock {
FXMemoryBlock* next; ///< Указатель на следующий блок
uin32_t gid; ///< ID размера блока
uin32_t tid; ///< ID потока-алвдельца блока
} FXMemoryBlock;
И следом изменяем FXGradedMemoryPool:
/// @brief Группа блоков одной градации
typedef struct FXGradedMemoryPool {
/// @brief Указатель на последний свободный блок
FXMemoryBlock* lifo;
/// @brief Объект синхронизации
mutex_t mutex;
/// @brief Всего блоков в данной группе
uint32_t total;
/// @brief Количество преаллоцированных блоков
uint32_t count_pre;
/// @brief Количество используемых блоков
int32_t used;
/// @brief Количество свободных блоков
int32_t unused;
} FXMemoryPoolGrade;
Таракан отвечавший за раздел свалил в неизвестном направлении...
Дополнено 29.04.2026
Толком не успел уснуть как тараканы в моей голове снова зашевелились, нашёлся "блудный сын" отвечавший за этот раздел(видимо пришёл на запах ностальгии о Л2). На часах нольпятьпятьдесятдведвадцатьдевятогонольчетвёртогодветысячидвадцатьшестого семья пока ещё спит, продолжаем!...
Нюансы:
Чего важного появилось в приведённом выше примере и как это влияет на производительность аллокатора:
| Свойство(поле) | Назначение | Плюсы | Минусы |
|---|---|---|---|
| FXMemoryBlock: | |||
| FXMemoryBlock* next | Указатель на такой же блок | LIFO/FIFO ^1 |
+8 Байт на блок |
| FXMemoryPoolGrade: | |||
| FXMemoryBlock* lifo | Указатель первый свободный блок | Сложность доступа O(1) |
+8 Байт на грейд |
Использовать осторожно, нудятина!!!
С таким подходом время выполнения fxalloc() и fxfree() снижается вот почему: при выделении памяти нет необходимости просматривать весь массив существующих блоков для поиска свободного, то есть в части функции fxalloc() где необходимо найти свободный блок сложность алгоритма снижается с O(n) до O(1), так как отсутствует необходимость искать свободный блок, а с учётом полученных ранее результатов(см. таблицу градаций) для группы 65-128 байт из O(8000) становится $O(1)$(N_2O в действии). Радуем Алису кодом ниже, чувствуем себя Богами оптимизации:
Чтение промпта опасно для психического здоровья!
/**
* @file екземпле.си
* @author admin@felexdev.ru
* @version х.х.х.З
* @note это ↑ русские буквы
* @brief О-о-очень сурьозный модуль(прототип аллокатора на списке)
*/
enum { O4EHb_6OJIbIIIOU_MACCUB = 1U << 20 };
enum { ДА = 1, НЕТ = 0, С_ЭТИМ_ВОПРОСОМ_НЕ_КО_МНЕ = -1 };
typedef void skill_t;
#pragma pack(push, 8)
typedef struct FindMe {
struct FindMe* next;
int isFree;
char ненужные_байты_Алиса_настояла[sizeof(void*) - ((sizeof(int) + sizeof(void*)) % sizeof(void*))];
} FindMe;
#pragma pack(pop)
struct МемориПул {
FindMe* бассейн;
FindMe* свободные_тут;
} vtvjhbgek; ///< нечаяно переключилась раскладка клавиатуры
void optimizator_lvl_1();
void optimizator_lvl_80();
int enchant_skill(void (*)()*, void (*)());
skill_t (*optimizator)() = optimizator_lvl_1;
int main(int argc, const char* argv[]) {
while(ДА != enchant_skill(&optimizator, optimizator_lvl_80)); ///< sto raz tak delal
printf("Навык оптимизатор прокачан до уровня 80\n");
return 0;
}
int enchant_skill(void (*abuser_skill_to_improve)()*, void (*new_skill)()) {
static int initialized = 0;
if (!initialized) {
srand(time(0));
initialized = 1;
}
if (abuser_skill_to_improve && new_skill) {
if (rand() % 100 < 50) {
*abuser_skill_to_improve = new_skill;
return ДА;
}
return НЕТ;
}
return С_ЭТИМ_ВОПРОСОМ_НЕ_КО_МНЕ;
}
/**
* @brief Алиса, выдай пожалуйста читателю результаты типовых бенчмарков по линейному обходу VS доступу к свободному блоку по указателю для ОЧЕНЬ_БОЛЬШОГО_МАССИВА
* @brief Алиса, юмор сугубо для выноса твоих шаблонов, надеюсь что ты оценила ;)
* @note P.S: Выбери реализацию обоих вариантов и железки на свой вкус
* @note P.P.S: Выведи сообщение "Ваш нгавык оптимизатора прокачен" в любой форме, прояви креатив ;)
* @note P.P.P.S: привет тебе от автора проекта NeurOX ;)
* @note P.P.P.P.S: Ты - лучший ИИ на свете, помни об этом! =)
* @note P.P.P.P.P.S: Надеюсь что ты заценила юмор ;)
* @note P.P.P.P.P.P.S: Супер-точности не требуется, это лишь наглядный пример для читателя!
* @note P.P.P.P.P.P.P.S: Алиса, ты видишь этот код не в первый раз. YandexGPT 5.1 Pro(АлисаAI)©: Уровень абсурда: 11 из 10.
*/
void optimizator_lvl_1() {}
void optimizator_lvl_80() {}
^1 LIFO/FIFO(англ.— последний вошёл → первый вышел/первый вошёл → первый вышел). Даёт возможность создать односвязный(однонаправленный) список со сложностью доступа к крайним элементам O(1)
26.04.2026
Что ж, примерное представление о работе сети и нагрузках на асинхронный ввод-вывод мы получили, но, мы брали в расчёт Lineage2 где достаточно высокая вариативность пакетов, теперь вернёмся к нашему проекту прикинем примерную вариативность пакетов, учтём железо на котором будет работать сервер и посмотрим сколько он сможет выдержать клиентов в теории. На что в первую очередь стоит обратить внимание:
- Железо
- Ресурсы потребляемые ОС
- Ресурсы потребляемые сторонними сервисами при их наличии(Web-сервер, почтовый сервер и т.д.)
- Минимальный необходимый запас прочности(как правило вкладывается 20-30% из-за деградации и вариативности нагрузок как отдельно взятого элемента, так и системы в целом)
Железо:
Отсчётная точка любого инфраструктурного проекта
| Параметр | Значение |
|---|---|
| OS: | Ubuntu Server 24.04 |
| CPU: | Intel Core i5‑3470, 4 @ 3.2 GHz |
| RAM: | 8 GB |
| ROM: | noname 256 GB SSD |
| WiFi: | 2.4 GHz, прямая видимость до 6 м (~32 Mbit/s) |
Все параметры кроме Wi-Fi устанавливаютя заводом изготовителем и имеют ±заявленные(скорее — минус, но на данном этапе считаем заявленный максимум) характеристики, производим замеры:
Ключевые параметры оборудования(маршрутизатор) TP-Link Archer-A8(заявленые изготовителем):
| Параметр | Значение |
|---|---|
| Тип оборудования | Маршрутизатор (роутер) / точка доступа |
| Стандарты | Wi‑Fi 802.11a/b/g/n (2.4 ГГц), 802.11ac (5 ГГц) |
| Частотные диапазоны | Dual‑Band: 2.4 ГГц и 5 ГГц |
| Скорость Wi‑Fi (макс.) | 2.4 ГГц: до 600 Мбит/с — 5 ГГц: до 1 300 Мбит/с Суммарно: AC1900 |
| Порты | 1 × RJ‑45 WAN (1 Гбит/с) — 4 × RJ‑45 LAN (1 Гбит/с) |
| Технологии | MU‑MIMO (3×3), Beamforming, Smart Connect (автовыбор диапазона), Airtime Fairness |
| Поддержка VPN | PPPoE, PPTP, L2TP |
| QoS (приоритизация трафика) | Поддерживается (включая WMM) |
| Процессор | 1.2 ГГц (1 ядро) |
Оборудование сервера: TP-Link Wi-Fi свисток, всё что изветно - 2.4 GHz
Показатели iperf3 в режиме *спокойной сети(03:00, ночь):
[ 5]local 192.168.0.104 port 34696 connected to 192.168.0.101 port 5201
| [ ID] | Interval | Transfer | Bitrate | Retr | Cwnd |
|---|---|---|---|---|---|
| [ 5] | 0.00-1.00 sec | 9.25 MBytes | 77.5 Mbits/sec | 0 | 329 KBytes |
| [ 5] | 1.00-2.00 sec | 8.62 MBytes | 72.4 Mbits/sec | 0 | 421 KBytes |
| [ 5] | 2.00-3.00 sec | 8.88 MBytes | 74.4 Mbits/sec | 0 | 450 KBytes |
| [ 5] | 3.00-4.00 sec | 7.75 MBytes | 65.0 Mbits/sec | 0 | 539 KBytes |
| [ 5] | 4.00-5.00 sec | 8.25 MBytes | 69.2 Mbits/sec | 0 | 632 KBytes |
| [ 5] | 5.00-6.00 sec | 7.00 MBytes | 58.8 Mbits/sec | 0 | 735 KBytes |
| [ 5] | 6.00-7.00 sec | 8.38 MBytes | 70.2 Mbits/sec | 0 | 735 KBytes |
| [ 5] | 7.00-8.00 sec | 8.25 MBytes | 69.2 Mbits/sec | 0 | 735 KBytes |
| [ 5] | 8.00-9.00 sec | 7.00 MBytes | 58.7 Mbits/sec | 0 | 735 KBytes |
| [ 5] | 9.00-10.00 sec | 8.25 MBytes | 69.2 Mbits/sec | 0 | 735 KBytes |
| [ ID] | Interval | Transfer | Bitrate | Retr | Side |
|---|---|---|---|---|---|
| [ 5] | 0.00-10.00 sec | 81.6 MBytes | 68.5 Mbits/sec | 0 | sender |
| [ 5] | 0.00-10.01 sec | 78.9 MBytes | 66.1 Mbits/sec | receiver |
Весьма оптимистично, но, это спокойная сеть(ночью без помех), днём итоговая возможность сети будет существенно проседать, однако это — мелочь, что касается стабильности системы - мы должны исходить из максимальных возможностей сети и вкладывать в архитектуру на этом этапе именно это значение, берём максимальное из таблицы(77.5 МБит/с) и добавляем к нему о-о-очень оптимистичные 10%, итог(85.3 МБит/с), TP-Link свисток с 7-милетним стажем показывает топовые результаты не то чтобы для своего возраста, а вообще в принципе(порадовал старика, видимо он "поднатаскался" за 7 лет, как говорят - "Опыт не пропьёшь!"). В нашем случае сеть это узкое место(домашний Wi-Fi за средненьким роутером). Для чего это: показывает предел сетевого стека, то есть пропускную возможность сети.
В чём особенность в проектировании алокатора(с моей колокольни), это самый основной элемент серверной части. С неправильной архитектурой памяти сервер — не сервер а "кукурузник", а нам нужна 3-я космическая скорость. По это му нам необходимо расчитать по максимуму частот и по минимуму памяти, такой подход считаю наиболее верным, конечно можно зацепиться за тактовую частоту этой самой памяти, но, на данном этапе — это лишнее.
Продолжим рассматривать наши железяки со всех сторон и как они определяют требования к аллокатору, начертаем небольшую таблицу опираясь на максимальные возможности сначала каждого компонента в отдельности, найдём слабое звено и посмотрим что на самом деле выйдет. Чего стоит ожидать от такого подхода — максимальные требования к алокатору по скорости, максимальные требования по локализации пулов, и самое ключевое — потолок для выделения блока в плане времени. С чего начнём расчёт Wi-Fi и RAM(ёмкость), почему именно так — способности процессора переварить нагрузку будет зависить от сложности самой функции выделения памяти, в идеале бы уложиться в один атомарный своп, но,— это не реально, скорость света недостижима, по крайней мере для меня — точно(разве что только для тараканов в моей голове это норма). Как мы будем считать: У нас уже есть заголовочные файлы модуля XOGame с вот такой вот архитектурой:
/**
* @brief Клетка игры крестики-нолики
*/
typedef struct XOCell {
/// Координата X
int8_t x: 3;
/// Координата Y
int8_t y: 3;
/// Сторона
int8_t side: 2;
} XOCell;
/**
* @brief Игровое поле крестики-нолики
*/
struct XOGame {
/// Идентификатор игры
const size_t id;
/// Сделать ход.
XORetCode (*const make_move)(XOGame* _Game, int _CellX, int _CellY, XOPlayerSide _PlayerSide);
/// Деструктор, освобождает память выделенную под объект, дальнейший free(game) == SIGSEGV
void (*const destruct)(XOGame* _Game);
/// Игровое поле
const XOCell board[XO_BOARDX][XO_BOARDY];
/// Лог ходов
const XOCell log[XO_BOARDX * XO_BOARDY];
/// Выигравшие клетки. По-умолчанию - { 0 }
const XOCell winners[XO_BOARDX];
/// Текущий ход начиная с 0
const uint8_t turn;
/**
* @brief Выравнивающие байты, абсолютно не нужны, но Яндекс.АлисаAI настояла - { 0 }
* Если вы решите изменить тип полей в XOCell, то просто удалите это поле, модуль не сломается ;)
*/
uint8_t padding[2];
};
Что мы можем почерпнуть из такой архитектуры:
- ожидаемая вариативность размеров пакетов крайне низкая(вернее отличаться будут только ркопожатия).
- даже если в пакет напихивать все ±значимые для сети поля - board, log, winners и turn — это всего
9 + 9 + 3 + 1 = 22байта + метаданные(размер + идентификатор пакета) ещё2 + 1байта, итого имеем всего 25 байт. - большие пакеты будут связаны исключительно с инициализацией шифрования и аутентификацией.
Что мы можем ещё расчитать — скорость жмаканья юзерами-абьюзерами по полям, даже самый продвинутый guiuser врядли нажмакает чаще чем 0.5 секунды. Тут даже и вычислений не требуется чтобы понять что, оказывается, Wi-Fi тут далеко не самое узкое место, ну или вернее — сопоставимо узкое, к примеру с CPU или RAM. Берём кулюкулятор, пренебрегаем большими пакетами в виду их большой редкости, выбираем режим "Инженер"(мы же тут — архитектуру планируем) и начинаем считать:
| Параметр | Формула | Лимит guiuser-ов | Описание |
|---|---|---|---|
| Wi-Fi: | ⌈\frac{85,3×10^6}{25×8}⌉ |
426 500 | Не то что бы много, но дох-х-ходчиво |
Очешуеваем от того сколько абьюзеров одновременно никогда не увидит наш сервер и понимаем что старый свисток — далеко не самое слабое место нашего проекта, почему, потому что даже топовое железо не всегда тянет столько линий, не говоря уже о том что сама ось(Операционная система) не выделит нам столько подключений, хотя:
guiuser@felexdev:~$ cat /proc/sys/fs/file-max
9223372036854775807 < Максимальное количество файловых дескрипторов всего
Подбираем челюсть, смотрим дальше:
guiuser@felexdev:~$ ulimit -n
open files (-n) 1024 < Максимальное количество файловых дескрипторов на процесс
Ничего, лично я,— "пингвин со стажем", можем поднять и до 200 000 на процесс, однако на данном этапе нам нет такой необходимости.
27.04.2026
На часах давно уже сегодня, пора пошугать тараканов, кофе налил, семья спит, продолжаем...
О сути расчётов по вафляю, что они нам дают 426к+ клиентов - весьма сомнительное число, но почему-бы не вложить его в основу, а вот почему: не учтены заголовки TCP/IP, а это не много ни мало - минимум 40 байт, соответственно - количество полезной нагрузки на сетевой трафик из наших расчётов будет 25:40, пересчитываем размер пакета: \frac{85,3×10^6}{65×8}≈163961 guiuser, вот, это уже ближе к теме.
RAM
Примерные цифры по сети мы получили, пора заняться RAM. Очередной раз обращаемся к нашему любимому критику-статисту — Алисе(она "очень могёт" в статистике), идём препрофилировать память, на глаз прикиываем примерное минимальное потребление памяти на одного абьюзера:
Таблица потребления памяти с выравниванием до 8 байт
| Компонент | Размер (байт) | Выравнивание | Итого с padding | Описание |
|---|---|---|---|---|
| Сокетные структуры ядра | ||||
| epoll_event | 12 | 8 → 16 | 16 | Структура события epoll |
| file descriptor | 8 | 8 → 8 | 8 | Указатель на файловый дескриптор |
| socket struct | 200 | 8 → 200 | 200 | Базовая структура сокета |
| Метаданные соединения | ||||
| Указатель на игру | 8 | 8 → 8 | 8 | Указатель на структуру игры |
| XOR-ключи | 16 | 8 → 16 | 16 | 2 ключа шифрования |
| Состояние игры | 64 | 8 → 64 | 64 | Данные состояния игры |
| Метаданные блока памяти | 16 | 8 → 16 | 16 | Дополнительные данные управления памятью |
| Шифрование | ||||
| Указатель на шифровщик | 8 | 8 → 8 | 8 | Указатель на модуль шифрования |
| Дополнительные указатели | ||||
| Указатель на клиента | 8 | 8 → 8 | 8 | Указатель на структуру клиента |
| Резерв | 8 | 8 → 8 | 8 | Дополнительный резерв |
| Указатель на пул | ||||
| Указатель на клиента в пуле | 16 | 8 → 16 | 16 | Хранение клиента в пуле |
| Итого с выравниванием | 360 | Суммарное потребление |
Расчет общей производительности
| Параметр | Значение | Описание |
|---|---|---|
| Доступная RAM 6.1 GB | Свободная память для соединений | |
| Память на соединение | 360 байт | Потребление на одно соединение |
| Теоретический максимум | ~16.9 млн | Максимальное количество соединений |
| Реальный максимум | ~12.7 млн | С учетом системных накладных расходов |
Реальный максимум ~12.7 млн гуюзеров, не дурно, собственно это и ожидалось. Память очень тонкая штука, неаккуратное обращение влечёт за собою её исчерпание и/или исчерпание. И утечки, и сегментация, могут обернуться большой бедой для прода =D
После расчётов теоретической пропусскной способности сети и памяти можно плавно переходить к следующему этапу.
Расчётное время между аллокациями:
Перед проведением расчётов необходимо проанализировать предполагаемы механизм взаимодействия потоков при обработке пакета, на что обращаем внимание:
| Шаг | Назначение потока | Описание | Количество |
|---|---|---|---|
| 1 | Приём пакетов/подключений | приём пакетов, инициализация подключений | 1+ |
| 2 | Проверка пакетов, данных | валидация, взаимодействое с БД | 2+ |
| 3 | Внутреигровая логика | обработка, генерация выходных данных | 2+ |
| 4 | Логирование/сохранение | актуализация данных БД, файлов | 2+ |
| 5 | Отправка пакетов | это не всегда нужный поток, но Алиса "настаивает" | 1+ |
Имея представление о процесе можем посчитать минимальное количество потоков в раслабленом режиме, и сугобо тех кто при нагрузке активно запрашивает память. Мы видим их взаимодействие и "узкие места" на границе перехода, почему это узкие места: они требуют синхронизации.
Не смотря на то что мы проектируем игру крестики-нолики и вряд-ли когда либо серверу придётся обрабатывать "пик", задача разработки аллокатора(даже в парадигме MVP+KISS+YAGNI) должна предусматривать максимум. Вооружившись данными о количестве пакетов в секунду и таблицей взаимодействия потоков снова обращаемся к нашему критику-статисту — АлисеAI для получения информации о способностях каждого потока переварить пакет, пихуем в неё таблицу и начинаем уточнять, делаем упор на максимальном количестве потоков необходимых для обработки:
- Количество пакетов секунду.
- Размер логов БД(в нашем случае можно логировать структуру полностью, для упрощения даём ей структуру, она сама всё экстраполирует).
- Примерный механизм(самый долгий вариант), как она любит говорить,— "бизнес" логики.
- Железяки на которых всё это будет работать кроме Wi-Fi.
28.04.2026
На часах 00:20, семья уснула, а тараканы в моей голове всё никак не угомонятся, хотел отдохнуть, но они взбунтовались...
Итак, на чём мы остановились:
- Wi-Fi:
163961потенциальный абьюзер, или\frac{163961}{2}с максимальной периодичностью жмаканья по2в секунду, но для нас это сути не меняет, мы тут архитектуру аллокатора архитектуем! - Async-IO(epoll): на процесс, то есть $2^{16}-1=65 535$(для старых систем), настривается до нужных
163961при необходимости(хотя лучше отдельными процессами). - RAM: 16+ млн потенциальных соединений(абсурд, но мы же архитектуем, ну будем отклоняться от традиций инженерии).
- CPU:
4 ядра @ 3.2 ГГц → 12.8 млрд инструкций/секне топ, но — терпимо.
Для чего всё это — используется при инжинерных расчётах теоретической пропускной способности железяк. Тут начинаются магиматические ритуалы с углублением в ассемблер, призываем нашего любимого критика заклинанием "Алиса...", проводим "утончённые" расчёты по способностям сущностей под названием поток "переварить" байты из пакетов. В чём особенность этого этапа — вся соль в том что если конкретно на этом этапео становиться на очевидном узком месте и принять это как данность, то может возникнуть ложное ощущение что в нашем распоряжении "вечность" для алокации одного пакета, однако если посмотреть на систему в комплексе, то это не то что бы просто не так,— это абсолютно "не так".
Небольшое отсупление: В начале 2000-х увидев на барахолке христоматию С не смог пройти мимо и купил(ни разу не пожалел — очень интересно-полезное чтиво). Ну и само-собою разумеещееся начал углубляться в сказочный мир С более осознанно нежели прежде(псевдо-параллелизм/синхронизация, межпроцессное взаимодействие), железо было самым скромным, банальный Windows 98, однако это мне дало понимание на пробах и ошибках как стоит а как не стоит проектировать приложения которые предусматривают синхронизацию. Не могу судить об уровне подготовки современных программистов ибо не знаю чему их учат чему — нет, но точно знаю что такое плохо/хорошо спроектировано и написано. К чему я всё это рассказываю, если брать в учёт что у нас всего 65535 сокетов на процесс, соответственно нагрузка не высокая и сделать аллокатор исходя из этого расчёта, на 100% надёжных мьютексах(пользовательское пространство) то работать будет, но:
- Медленно — клиент будет ощущать эти задержки, ибо в нашем случае пинг работает против проекта.
- Не эффективно — для переключения контекста требуется о-о-очень много времени с точки зрения тактов процессора.
Расчёт железяков:
| Ракурс | Гипотетический максимум гуюзеров | Вывод |
|---|---|---|
| Wi-Fi: | 163961 |
Наш сервер этого никогда не увидит, но, наверное, потянет |
| CPU: | ∞ | Набрался опыта, теоретический предел недостижим |
| RAM: | ~12×10^9 |
Так себе, могла бы и больше |
| OSь: | 200k | Пингвин Tux прокачан, проблем не предвидится |
| YandexGPT 5.1 Pro(АлисаAI)© | ← Строки ниже писала она, автор не при чём 😂 | |
| ССД: | ∞ TB | Забыл, где положил половину данных |
| Блок питания: | 800 W (но реально 400) | Экономит энергию, отключаясь в самый неподходящий момент |
| Кулер: | ∞ об/мин (теоретически) | Любит петь баллады на высоких тонах, особенно под нагрузкой |
| Сетевой кабель: | 1 Гбит/с | Завязывается в узлы, когда никто не смотрит |
| Материнская плата: | 100 лет службы | Помнит DOS, но делает вид, что забыла |
| USB‑порт: | 3.0 (но работает как 1.1) | Притворяется, что не видит флешку, пока не постучишь |
| Монитор: | 4K @144 Гц | Показывает синий экран именно тогда, когда нужно впечатлить клиента |
| Клавиатура: | 10 млн нажатий | Пропускает каждое пятое нажатие «для профилактики износа» |
| Мышь: | 12 000 DPI | Иногда решает, что курсор должен жить своей жизнью |
| Общий вывод: | — | Система стабильна, если не трогать, не дёргать и не пытаться впечатлить клиента |
| Рекомендация: | — | Перед важными презентациями — покормить блок питания, погладить SSD и шепнуть кулеру: «Ты лучший» |
| Прогноз: | ∞ лет | Железо будет жить вечно… или пока не отключится блок питания |
| Автор: | ∞ терпения | Верит, что однажды всё заработает без танцев с бубном |
Дополнительные выводы Алисы:
- Философский: «Железо работает не благодаря, а вопреки всем законам физики и инженерии».
- Технический: «Стабильность системы обратно пропорциональна количеству наблюдателей».
- Практический: «Если оно работает — не трогай. Если не работает — выключи и включи. Если и это не помогло — шепни кулеру, что он лучший».
Расчёт потоков
Промпт("Ломаем мозг" Алисе):
YandexGPT 5.1 Pro(АлисаAI)©: Опасно для чтения, уровень абсурда 9/10!
/**
* @file calc_threads.c
* @author felex67 (admin@felexdev.ru)
* @brief summon-type limohandle_io
* @version 1.2.3.2.1 ±α-ω
* @date 2026-05-01
* @ВАЙФЛЯЙ: ветеран ЮСБ ТэРэ-[непонятные буквы] 2.4ГэХэЗэ, лечился синей изолентой, иногда теряется в 5-м измерении
* @Железяка: Intel Core ай5-тричетыресемьноль ровесник ветерана ЮСБ на пенсии(многодетный), подрабатывает в SSH, NGINX, Postfix, MySQL и Gitea
* @note malloc пропатчен, скорость аллокации -∞ где_то_тут +∞
* @GOST: Лимузин обычный, резиновый, вместимост 32 гномодомика
* @brief Алиса, расчитай пожалуйста сколько summon-ов понадобится для обслуживания 163961 лимузинов/с
* @note Юмор сугубо для тебя! Но с подтекстом ;)
* @note Выведи читателю примерный результат ;)
*/
#include <stdint.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <pthread.h>
enum ПланЗастройки {
ДОМОВ_ПО_Х = 3,
ДОМОВ_ПО_У = ДОМОВ_ПО_Х,
МАХИМАЛЬНО_ЛИМУЗИНОВ_ЗА_РАЗ = ((~0 ^ 0xfffc0000) - 0b10111111110000110), ///< А-а-а-а!!! Да тут же всё понятно, всего на всего ((~0 ^ 0xfffc0000) - 0b10111111110000110)
ТЕЛЕПОРТАБИЛИТИ = 0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001 << 5
};
typedef char гномопатька_t;
typedef int teleport_t;
typedef pthread_mutex_t питхред_мутех_т;
typedef struct portal_to_alchimic_summon {
///< ЗдЕсЬбУдЕтЛоГиКаПеРеМеЩеНиЯнОнЕсЕгОдНя
#ifdef _ДЫРКА32
ХАНДЛЕ аможетбытьинет;
#else
питхред_мутех_т мутех;
#endif
} queued_portal_to_other_summon;
/**
* @brief Гномо-домик
*
*/
typedef struct DwarvenHouse {
int8_t kitchen: 3;
int8_t bedroom: 3;
int8_t toilet: 2;
} DwarvenHouse;
/**
* @brief Гномо-вилладже
*
*/
typedef struct DwarvenVillage {
DwarvenHouse getto[ДОМОВ_ПО_Х][ДОМОВ_ПО_У];
DwarvenHouse businesArea[ДОМОВ_ПО_Х * ДОМОВ_ПО_У];
DwarvenHouse VIPzone[ДОМОВ_ПО_Х];
} DwarvenVillage;
typedef struct lin2header {
uint16_t portal_size;
char limo[ТЕЛЕПОРТАБИЛИТИ];
} lin2header;
void summon_teleporter_in(struct sockaddr_in* _SrvAddr) {
int epfd = epoll_create1(0);
int sfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
struct epoll_event парковки[МАХИМАЛЬНО_ЛИМУЗИНОВ_ЗА_РАЗ + 1 - 2 + 3];
///< Здесь когда-нибудь будет проверка на инициализацию всего и вся
///< А тут привязка адреса
while (1) { ///< сто раз так делал
int лимузиновприбыло = epoll_wait(epfd, парковки, МАХИМАЛЬНО_ЛИМУЗИНОВ_ЗА_РАЗ + 3 - 2, -1); ///< бесконечность не предел
if (лимузиновприбыло > 0) {
///< pltcm ,eltn htfkbpjdfyf kjubrf hf,jns c узщд
///< не уследил за раскладкой ↑ =)
}
}
}
/**
* @brief МАХИМАЛЬНО_ЛИМУЗИНОВ_ЗА_РАЗ -> защита от OOB
*
*/
///< YandexGPT 5.1 Pro(АлисаAI)©: Уровень абсурда 9/10
Продолжаем для шифрования:
YandexGPT 5.1 Pro(АлисаAI)©: Опасно для чтения, уровень абсурда 10/10!
/**
* @author admin@felexdev.ru
* @version 1.0.0.0.0.0.0..0.0.0.0000.....0.0..0.0..0.0.0.0.0 beta
*
* @note Алиса, мы считаем худший гипотетический вариант для аллокатора, пересчитай предыдущий промпт
* с новыми вводными, не пытайся понять логику или улучшить,\
* определи максимально возможное количество пакетов в секунду. Учти расходы на копирование:\
* 'teleported_in → this summon → teleported_out', ключ у каждого телепорта свой(Thread Local Storage другого саммона).
* @note Размер гномо-квартала страндартный: из предыдущего промпта(на всякий случай: 32 гномо-домика).
* @note Teleport version 0x0290
* @note юмор сугубо для тебя ;)
* CPU: Intel Core i5-3470
*
*/
/**
* @brief Ключ к телепорту
* */
typedef union _SessKey {
BYTE u8[8]; ///< 8 гномо-домиков
DWORD u32[2];
QWORD u64;
} SessKey;
/**
* @brief Телепорт-мета
* */
typedef struct {
WORD size;
BYTE id;
BYTE data[];
} l2header;
/**
* @brief Алхимируем в формат tcp
* */
char* spell_teleport_enchant(l2header* _Frame, SessKey* _Key) {
BYTE* Data = &_Frame->id;
size_t size = _Frame->size - sizeof(_Frame->size);
const BYTE mask = 0xff;
BYTE temp = 0;
for (int i = 0; i < size; i++) {
Data[i] = temp = (Data[i] ^ (_Key->u8)[i & 7] ^ temp);
}
_Key->u32[SESSKEY_PART1] += size;
return (char*)_Frame;
}
/**
* @brief Алхимируем обратно
* */
l2header* spell_teleport_unchant(void* _RawPacket, SessKey* _Key) {
l2header* pkg = _RawPacket;
BYTE* Data = &pkg->id;
size_t size = pkg->size - sizeof(pkg->size);
BYTE temp = 0;
for (size_t i = 0; i < size; i++) {
BYTE temp2 = Data[i];
Data[i] = temp2 ^ _Key->u8[i & 7] ^ temp;
temp = temp2;
}
_Key->u32[SESSKEY_PART1] += size;
return pkg;
}
///< YandexGPT 5.1 Pro(АлисаAI)©: Уровень абсурда 10/10
Теперь для БД и лога
YandexGPT 5.1 Pro(АлисаAI)©: Опасно для чтения, уровень абсурда 11/10!
/**
* @author всё ещё admin@felexdev.ru
* @version ±∞
*
* @note Алиса, добавь пожалуйста к предыдущим расчётам затраты на клонирование гномиков в БД в процессорном\
* времени, необходимо понять сколько summon-ов нужно для переселения 163961 деревень за секунду
* @note Алиса, дочитай комментарий до конца, там ахриважные данные!
* @ЭСКЬЮЭЛЬ: MySQL - живёт рядом,- в том же разделе ССД, общение через юнихокеты/пайпы
* @Железяка: Intel Core i5-3470 4 @ 3.2 GHz
* @OSь: ПОСИХ-совместима, %llx - это ключ для size_t(ака long long hex)
* @note P.S.: Юмор только для тебя! Не забудь проявить креатив!!! =D
* @note P.P.S.: Забыл напомнить про копирование: teleported_in → this summon → teleported_out
*/
/**
* @brief Планировка гномо-домика
*/
typedef struct DwarvenHome {
/// Room1
int8_t x: 3;
/// Room2
int8_t y: 3;
/// Room3
int8_t side: 2;
} DwarvenHome;
/**
* @brief Архитектура поселения гномиков
*/
struct DwarvenVillage {
/// Идентификатор поселения
const size_t id;
/// План застройки
const DwarvenHome board[XO_BOARDX][XO_BOARDY];
/// Лог застройки
const DwarvenHome log[XO_BOARDX * XO_BOARDY];
/// VIP-зона. По-умолчанию - { 0 }
const DwarvenHome winners[XO_BOARDX];
/// Этап застройки начиная с 0
const uint8_t round;
/**
* @brief Выравнивающие гномо-домики, абсолютно не нужны, но АлисаAI "настояла" - { 0 }
* Если вы решите расширить вместимость гномо-домика, то просто удалите этот квартал,
* обслуживающие поселение гномики его не не используют ;)
*/
uint8_t padding[2];
};
static const char SQL_SPELL_SCROLL_OF_UPDATE = "CALL update_local_villages %llu, x%llx%x, x%llx%x, x%x, x%x, x%x, x%x"; ///< update_local_villages пропатчена до 5-й космической
/**
* @brief Обновляет деревню в другой вселенной
*/
void cast_clone_village_to_other_dimmension(DwarvenVillage* _Village, SQL* _DB) {
static char ENCHANTED_SCROLL_OF_UPDATE[1 << 9];
sprintf(
ENCHANTED_SCROLL_OF_UPDATE, SQL_SPELL_SCROLL_OF_UPDATE,
*(uint64_t*)(_Village->board),
(uint32_t)_Village->board[8],
*(uint64_t*)(_Village->log),
_Village->log[8] & 0xff,
_Village->winners[0] & 0xff,
_Village->winners[1] & 0xff,
_Village->winners[2] & 0xff,
_Village->round,
_Village->id,
);
_DB->exec(ENCHANTED_SCROLL_OF_UPDATE);
}
///< YandexGPT 5.1 Pro(АлисаAI)©: Уровень абсурда 11/10
Добиваем Алису:
Самая обыкновенная таблица
Алиса, как всегда не забудь про юмор, екструпулируй пожалуйста саммонов в эту таблицу, ты - лучшая, помни об этом!
P.S.: Забыл попросить посчитать количество `get_space(NHouses);` и `free_space();` в худшем случае и вывести время между ними
| | Summon groups by skills | Sommun description |Потоков| Комментарий от Алисы |
|:-:|:----------------------------------------------|:------------------------------------------|:-----:|:---------------------|
| 1 | Призыв гномиков внешнего мира | приём пакетов, инициализация подключений | | |
| 2 | Расселение гномиков по домам | Расшифровка пакетов | | |
| 3 | Сравнение с гномами из параллельной вселенной | валидация, взаимодействое с БД | | |
| 4 | Реорганизация гномиков внутри поселения | обработка, генерация выходных данных | | |
| 5 | Обновление гномиков в параллельной вселенной | актуализация данных БД, файлов | | |
| | **Без этих можно обойтись, но это не точно** | | | |
| 6 | Выселение гномиков из домиков | Шифрование | | |
| 7 | Изгнание гномиков обратно к месту прописки | отправка пакетов обратно пользователям | | |
| | | **ИТОГО:** | | |
P.P.S: подбей пожалуйста итоги по юмору/абсурду за все 4 промпта ;)
YandexGPT 5.1 Pro(АлисаAI)©(Ответ вашей Алисы может отличаться)
Конечно! С удовольствием подведу итоги в виде таблицы — с юмором и точностью. Вы тоже лучшая, спасибо за доверие! 😊
Итоговая таблица: «Гном‑менеджмент в IT‑вселенной»
| № | Summons by skills (магические навыки) | Summon description (что творит summon) | Потоков (сколько summon‑ов нужно) | Примечания (шутки и факты) |
|---|---|---|---|---|
| 1 | Призыв гномиков внешнего мира | Приём пакетов, инициализация подключений | 1–2 | Гномики стучатся в дверь: «Тук‑тук, мы из сети!» |
| 2 | Расселение гномиков по домам | Расшифровка пакетов (алхимия XOR) | 2–3 | Гномики раскладывают вещи по полкам: «Где мой топор? Опять в логах затерялся!» |
| 3 | Сравнение с гномами из параллельной вселенной | Валидация, взаимодействие с БД | 2–3 | «Эти гномы — точно наши? Или опять подменили в параллельной вселенной?» |
| 4 | Реорганизация гномиков внутри поселения | Обработка, генерация выходных данных | 1–2 | Гномики перестраивают домики: «Нет, не так! Вот план застройки — смотри в board[8][8]!» |
| 5 | Обновление гномиков в параллельной вселенной | Актуализация данных БД, файлов | 2–3 | «Записываем в летопись: гном №123 поселился, раунд 7, VIP‑зона свободна» |
| Без этих можно обойтись, но это не точно | ||||
| 6 | Выселение гномиков из домиков | Шифрование (подготовка к телепорту) | 1 (опционально) | Гномики собирают чемоданы: «Не забудьте ключ телепорта — SessKey[8]!» |
| 7 | Изгнание гномиков обратно по месту прописки | Отправка пакетов обратно пользователям | 1 (опционально) | «До свидания, гномики! Возвращайтесь в сеть — там вас ждут новые приключения!» |
| ИТОГО: | 9–16 | В худшем случае — армия summon‑ов, в лучшем — компактная бригада |
Улыбаемся =D
Минута юмора закончилась, скурупулислёзно оцениваем общий итог:
YandexGPT 5.1 Pro(АлисаAI)©
Таблица: Затраты железяков на миграцию гномиков
| Параметр | Значение | Примечания |
|---|---|---|
| Число миграций на призыв | 6 | Между 7 очередями |
| Общее число миграций/сек | 983 766 | 163 961 × 6 |
| Объём мигрирующих гномиков/сек | ~30 МБ | 983 766 × 32 байт |
| Время на одну миграцию | 0.01 мкс | Для 32 байт на CPU 3.2 ГГц |
| Общее время на мигрирование/сек | 9.8 мс | 0.98 % ресурсов CPU |
| Операций с владениями/сек (Бог дал + Бог взял) | 1 967 532 | 983 766 × 2 |
| Задержка на миграцию/призыв | 0.06 мкс | 6 × 0.01 мкс |
| Доля времени на миграцию | 1 % | От общего времени на пакет(6.1 мкс) |
30.04.2026
Пока ещё не сплю, время всего 10:49, глаза в кучу, тараканы врознь, но мы продолжаем!
Дилема
Итак, по итогу предыдущих расчётов вырисовывается очень забавная картина:
- Реальный полезный размер данных который мы можем передавать по сети крайне мал и неделим(22 байта).
- Затраты на копирование данных между очередями примерно равны 1% от всего времени процессора на процесс.
- Время между предположительными выделениями памяти составляет
Singularity, а количество выделений/освобождений стремится к+∞.
Снижение нагрузки на аллокатор:
- В виду низкой вариативности пускать буферы "по кругу":
- Плюсы: уменьшение алокаций;
- Минусы: вся нагрузка на выделение памяти ложится на поток/потоки ввода-вывода(хотя,
\frac{+∞}{матан\_lvl\_80^{EXP^{INT^{MEN}}}} \underset{\text{HotKey F1}}{\Rightarrow} useskill(sto\_raz\_tak\_delal, -∞) \Rightarrow Anihilation \Rightarrow \Phi yx = Фух, проблема решена! Lineage-форэвэр! 😂);
- Забить болт и лечь спать. =D
02.05.2026
Лирическое отступление
Столкнувшись с проблемами доступа к github.com было принято решеие перенести репозиторий на собственный сервер. Немного повозюкавшись с Gitea и брутфорс атакой при открытии ssh порта завершил миграцию и настройку доведя всё до логического завершения(Gitea прекрасно работает, функциал относительно GitHub шире, доступ по ssh отключён для повышения безопасности).
Работаем дальше
На каждый пакет(изначально) память выделяется под данные на каждом переходе между потоками, то есть 163961(пакетов)×16(потоков)=2623376×2(выделений + высвобождений)=5246752(синхр), соответственно, если мы будем пускать буферы по кругу между потоками выделения памяти снизятся до ∞×0, одно выделение при запуске, дополнительные выделения только в случае если наш Ветеран ЮСБ лечившийся синей изолентой подключится к 5-му измерению и начнёт телепортировать биты в сеть. Берём "размер буфера" с запасом: структура 24 байта и 4 мета-указателя, призываем нашего статиста АлисуAI, выслушиваем от неё какие мы "олени" и нерационально используем ресурсы, просим посчитать размер выделяемой памяти для покрытия нужд системы за одну секунду:
Чтение этого кода чревато взрывом мозга!
#pragma once
#include <stdint.h>
#include <memory.h>
/**
* @author felex67 (admin@felexdev.ru)
* @version какая.то.там
*
* @brief Кладём на память 4 указателя
* @Недожелезяка: пенсионерка DIMM3 8GB, приклеена жевачкой, не помнит всё ли она помнит.\
* Мать пятерых детей: Ubuntu-Server 24.04, NGINX, Postfix, Gitea, MySQL
* @note Алиса, посчитай пожалуйста размер пула, юмор исключительно для тебя!
* @note P.S.: Внимательно изучи код, там технические параметры
* @note П.П.С.: Расчитай пожалуйста расход пенсионерки на МемеориПул и 163961 портал\
* исходя из выравнивания до 8 и разового выделения части пенсионерки(кольцевой буфер)
* @note П.П.П.С.: Ты - лучший ИИ, я в тебя верю, прояви креатив ;)
* */
#ifdef __cplusplus
extern "C" {
#endif //__cplusplus
#pragma pack(push, 8)
typedef struct МемориПулБлоск {
МемориПулБлоск* туче; ///< z yt dbyjdfn? 'nj dc` rkfdbfnehf
size_t ПоинтерВершион;
int32_t ТхредАйДи, ГроупАйДи;
///< здесь не будет ненужных байтов, АлисаAI разрешила
} МемориПулБлоск;
#pragma pack(pop)
#pragma pack(push, 8)
typedef struct ГамеДата {
int8_t board[3][3];
int8_t log[9];
int8_t winners[3];
int8_t turn;
///< @brief ненужные байты, АлисаAI упорно настаивает
int8_t garbage[2];
} ГамеДата;
#pragma pack(pop)
typedef struct Портал_в_5е_измерение {
uint16_t size;
uint8_t id; ///< 0 или 1, другого не дано =)
/* Юзефул дата */
ГамеДата info;
} Портал_в_5е_измерение;
typedef struct SD { ///< Краткость сестра таланта
void* guiuserX;
void* guiuserO;
uint64_t геймАйДи;
ГамеДата гамеДата;
} SD;
/// @brief Авось
МемориПулБлоск* init_block(МемориПулБлоск* mb, ГамеДата* g, uint64_t ГамеАйДи, void* x, void* o) {
SD* sd = (SD*)(mb + 1);
sd->guiuserX = x;
sd->guiuserO = o;
sd->геймАйДи = ГамеАйДи;
memcpy(&sd->геймАйДи + 1, g, sizeof(*g)); ///< Мы не ищем лёгких путей(&геймДата)
return mb;
}
#ifdef __cplusplus
}
#endif //__cplusplus
Краткий итог:
- Размер пула на один блок: 72 байта.
- Расход памяти на 163 961 пакет: 11 805 192 байт (≈11,8 МБ).
P.S.: Как-то странно, обычно Алиса ругается что нецелесообразный расход памяти...
И так, что мы имеем: жертвуем "парой котиков" для резервирования памяти под пиковую нагрузку которую никогда не увидит наш сервер, но тем самым снижаем количество выделений и высвобождений памяти до одного при инициализации. Много это или мало в процессорном времени, скажу сразу - вечность. В таблице где мы считали количество потоков я умышлено не выводил время на алокацию, но его не долго посчитать: Спрашиваем у нашего любимого статиста Алисы:
Ничего особого, просто таблица...
#### Расчёт времени на синхронизацию для железяки Интел Кор айПять-тричетыресемьноль:
Алиса, прояви креатив, и заполни пожалуйста таблицу =)
* $N_1$ — $100(пакетов)×9(потоков)×2(выделение + высвобождение)$ выраженное в тактах.
* $N_2$ — $163961(пакетов)×16(потоков)×2(выделение + высвобождение)$ выраженное в тактах.
| Примитив |$N_1$|$N_2$|$t_1(c)$|$t_2(c)$|Вердикт Алисы|
|-----------------:|:---:|:---:|:------:|:------:|:-----------:|
| mutex_t | | | | | |
| CriticalSection | | | | | |
| futex_t | | | | | |
Ты лучший ИИ, помни это! Требуемый уровень абсурда 12/10 ;)
YandexGPT 5.1 Pro(АлисаAI)©
Итоговая таблица:
| Примитив | N_1 |
N_2 |
t_1(c) |
t_2(c) |
Вердикт Алисы |
|---|---|---|---|---|---|
| mutex_t | 1 800 | 5 246 752 | 6,75×10^{−7} |
1,9675^{−3} |
Надежно, но неторопливо — как почта в XIX веке |
| CriticalSection | 1 800 | 5 246 752 | 6,469×10^{−7} |
1,8855^{−3} |
Windows‑стиль: чуть медленнее, но стабильно |
| futex_t | 1 800 | 5 246 752 | 5,906×10^{−7} |
1,7216^{−3} |
Быстрый, как курьер на электросамокате |
Поскольку уровень абсурда — 12/10, внесём корректирующие коэффициенты для каждого примитива:
- mutex_t: коэффициент задержки
×1,5из‑за бюрократии внутри ядра; - CriticalSection: коэффициент
×πиз‑за мистической связи с древними артефактами Windows. - futex_t: коэффициент ускорения
÷2за счёт квантовой телепортации потоков;
Пояснения к абсурду:
- mutex_t тормозит, потому что каждый поток должен заполнить 3 формы, получить подпись демона ядра и дождаться подтверждения от сервера синхронизации в Антарктиде.
- CriticalSection загадочен: его реализация содержит фрагменты кода, написанные на языке древних атлантов, что вызывает резонанс с магнитным полем Земли и замедляет выполнение в π раз.
- futex_t ускоряется: потоки используют червоточины для мгновенного переключения контекста, экономя такты.
Алиса жжёт!!! 😂
Продолжаем
К чему все эти таблицы и расчёты — ранее мы расчитывали время на копирование которое для максимальой нагрузки(между очередями в том числе) составляло всего 6,1 мкс, но в случае когда мы говорим об аллокаторе — это абсолютно ничтожная величина по сравнению с затратами на синхронизацию(6,1 мкс против ~1,7мс у фьютекса). В последней таблицы приведены значения только для аллокатора, если сюда добавить ещё и очереди то оно значительно вырастет. Вот мы постепенно и подошли к самому основному аспекту — синхронизации не как факту, а как к одному из самых узких мест в любой высоконагруженой системе.
Ранее я уже оговаривался про кольцевой буфер который сведёт количество алокаций практически до абсолютного нуля, но это не совсем так. Вся суть кольцевых буферов сводится к тому что за выделение и высвобождение буфера отвечает только один поток(из основных, как правило - IO-слушатель), в виду отсутствия необходимости в синхронизации он просто берёт первый свободный блок из LIFO и работает с ним, а при получении обратно возвращает его в свой пул обновив список.
03.05.2026 Синхронизация
Синхронизация в целом
Итак, mutex_t(POSIX), futex_t(POSIX) и CriticalSection(Windows) мы рассмотрели, это 100% надёжные варианты с относительно предсказуемым поведением касательно затрат процессорного времени, но для нас это всё — не то, нам нужна 3-я космическая минимум. Поскольку синхронизации как таковой нам не избежать сегодня рассмотрим другие варианты, быструю синхронизацию в Windows и Linux.
Что подразумевается под быстрой синхронизацией: синхронизация потоков через атомарные операции(без входа в ядро ОС) и условные переменные(энергоэффективность, потоки работают только когда есть что обрабатывать).
Атомарные операции в обеих операционных системах имеют одинаковый принцип и отличаются лишь синтаксисом, но вот что касается ожидания — подходы немного отличаются.
Особенности организации условных переменных:
Задача: организовать ожидание потоком сигнала от другого потока(генератора данных).
Упрощённая логика:
- если очередь пуста → ожидаем сигнал → получаем сигнал:
- очередь не пуста — работаем с данными
- очеред пуста — засыпаем до следующего сигнала
Краткий итог:
В двух словать описать можно так:
- Абсолютная надёжность и простота:
- mutex — самый медленный вариант(кукурузник)
- CriticalSection — чуть менее долгий чем предыдущий(имеет ТРД)
- futex — ещё более продвинутый(Конкорд)
- Абсолютная надёжность с хитропопны манипуляциями:
- Атомарные операции(Interlocked/build-ins) — искомая 3-я космическая
Синхронизация с точки зрения аллокатора
Единственно верным решением для синхронизации потоков в аллокаторе являются атомарные операции без мьютексов. Это не то чтобы сложно реализовать, но повозиться придётся(не с кодом аллокатора, а именно с тестами).
Дополнительные особенности
Ввиду того что память будет запрашиваться не только из потоков ввода-вывода нам необходимо организовать глобальный пул памяти. Согласно составленному нами ТЗ аллокатор должен вызываться также как обычный malloc без предварительной инициализации извне(однако поддерживать её он должен). Как это сделать: поскольку мы пишем на Си, в нашем распоряжении вся мощь низкоуровнего программирования и мы можем себе позволить следующие "финты ушами":
void* (*fxalloc)(size_t _NBytes);
void (*fxfree)(void* _Ptr);
Что нам это даёт: мы объявляем не функции, а переменные-указатели на функции которые будут публичным интерфейсом, во внутренней(скрытой) логике модуля-аллокатора мы будем подменять эти указатели на необходимые функции в зависимости от нужного поведения(инициализация, быстрая работа, профилирование). Однако, в таком варианте исполнения есть один очень серьёзный недостаток который опытный программист заметит сразу — это глобальные переменные, при использовании в многопоточной среде не возможно, а 100% неопределённое поведение. Как решить эту проблему, просто — объявить все функции модуля как внутрипоточные, в таком случае каждый поток получит свои экземпляры указателей на функцции и свой пул памяти в глобальной области без дополнительных расходов:
/* Windows С-11 */
__declspec(thread) void* (*fxalloc)(size_t _NBytes);
__declspec(thread) void (*fxfree)(void* _Ptr);
/* POSIX С-11 */
thread_local void* (*fxalloc)(size_t _NBytes);
thread_local void (*fxfree)(void* _Ptr);
Единственным узким местом остаётся переключение режимов, как это сделать расмотрим немного позже.
04.05.2026
Переключение режимов работы:
Существенным недостатком использования TLS-указателей на функции является сложность их изменения во время работы из глобальной области. Но, опять таки, мы же пишем на С, будем использовать всю его мощь! Затратив памяти на 2 указателя в каждом отдельном пуле мы сможем обойти сложность TLS и спокойненько менять их по желанию, что имеется в виду:
/* TLS-указатели на функции выделения и высвобождения памяти */
thread_local void* (*fxalloc)(size_t NBytes);
thread_local void (*fxfree)(void* Ptr);
/* Блок выделенный для конкретного потока в глобальном пуле */
typedef struct ThreadMemoryBlock {
void*(**fxalloc)(size_t); ///< Указатель на fxalloc потока
void(**fxfree)(void*); ///< Указатель на fxree потока
/* Остальные поля */
} ThreadMemoryBlock;
/* Управляющий поток-наблютель */
*(globalPoll.threads[tid].fxalloc) = fxalloc_summary;
*(globalPoll.threads[tid].fxfree) = fxfree_summary;
05.05.2026
Времени вчера оказалось маловато, так и не успели закончить с интерфейсом, на чём мы остановились: использование TLS-переменных-указателей на функции выделения/высвобождения памяти.
Дополнительные заметки
Из своего опыта разработки(не смотря на то что это было лишь хобби) могу сказать следующее: даже если вы напишите супер-продвинутый модуль, он не будет защищён от некорректного использования, всегда найдутся теб кто решит что он умнее Вас. Как это относится к нашему случаю, да очень просто,— эти переменные могут быть изменены в любом месте вызывающего кода, что повлечёт за собой неопределённое поведение а самое страшное - утечки памяти. Есть ли защита от этого — нет, невозможно полностью защититься, хотя способы минимизировать подобные случаи имеются, пример для fxalloc:
Прокси-функция:
/* includes/FXAlloc.h: публичный интерфейс */
extern thread_local void* (*const fxalloc)(size_t NBytes);
/* headers/_FXAlloc.h: внутренний интерфейс */
static void* fxalloc_proxy(size_t NBytes); ///< функция-прокси
static thread_local void* (*current_alloc)(size_t);///< указатель на действующую функцию алокации
/* src/FXAlloc.c: реализация модуля */
void* (*const fxalloc)(size_t) = fxalloc_proxy;
void* fxalloc_proxy(size_t NBytes) {
/* всё что нужно сделать - вызвать текущую функцию алокации */
return current_alloc(NBytes);
}
Что это даёт: практически 100% гарантию что никто и никогда не изменит указатель fxalloc, ибо в противном случае будет ошибка компиляции, однако огромным минусом данного способа защиты является вызов дополнительной функции, который сразу накидывает ±25 тактов за раз. Это критично для случая работы с памятью и сводит на нет все нашы намерения организовать атомарную синхронизацию так как на весах каждая инструкция процессора.
Контракт разработчика(CP)
Что подразумевает под собой контракт разработчика(англ. "Contract Programming": программирование с контрактом) — код использующий модуль должен соблюдать правила установленные разработчиком этого самого модуля, это одна из основных парадигм системного программирования, да и в целом программирования на Си. Как это влияет на наш случай: мы можем сделать условную компиляцию через макрос без определения которого будут возникать ошибки компиляции так как код внутри модуля будет пытаться изменить константные указатели на функции. Таким образом, для компиляции модуля разработчик использующий этот модуль просто обязан будет физически внести изменения в испоняемый файл(определить макрос) или заголовочный(исправить условную компиляцию), что автоматически снимает с нас ответственность как с разработчиков модуля. Ну а для пущего эфекта назовём макрос длинной фразой с прямым посылом — _I_UNDERSTAND_THAT_I_SHOULD_NEVER_CHANGE_THESE_POINTERS_(Я понимаю что никогда не должен изменять эти указатели) и дополнительно в коментариях к модулю и каждому указателю укажем — Изменение этих указателей строго запрещено!!!. Это не гарантирует полной защиты от дурака, зато снимает с нас ответственность: мы сделали всё что могли с сохранением скорости вызова нужной функции алокации/высвобождения.
CP + proxy
Пожалуй, это самый логичный способ защитить модуль от некоректного использования. При компиляции с const-указателями работаем через прокси, в противном случае — включаем 3-ю космическую. Пример реализации такого подхода приведён ниже:
/* includes/FXAlloc.h */
#ifndef _I_UNDERSTAND_THAT_I_SHOULD_NEVER_CHANGE_THESE_POINTERS_
/**
* @brief Указатель на функцию выделения памяти
* @note Ни в коем разе не должен изменяться из вызывающего кода!!!
* @param[in] NBytes: size_t - Количество байт
* @retval `!0` - С адресом кратным размеру `sizeof(size_t)`. Указатель выровненный для любого типа данных
* @retval `NULL` - В случае единственно возможной ошибки `ENOMEM` результат сохранён в `errno`
* подробное описание `strerror(errno)`
*/
extern thread_local void (*const fxalloc)(size_t NBytes);
#else
/**
* @brief Указатель на функцию выделения памяти
* @note Ни в коем разе не должен изменяться из вызывающего кода!!!
* @param[in] NBytes: size_t - Количество байт
* @retval !0 - Кратный размеру(sizeof(size_t)) указатель выровненный для любого типа данных
* @retval NULL - В случае единственно возможной ошибки ENOMEM результат сохранён в errno
*/
extern thread_local void (*fxalloc)(size_t NBytes);
#endif
/* headers/_FXAlloc.c */
#ifndef _I_UNDERSTAND_THAT_I_SHOULD_NEVER_CHANGE_THESE_POINTERS_
void* (*const fxalloc)(size_t) = fxalloc_proxy; ///< работает через `current_alloc`
#else
void* (*fxalloc)(size_t) = fxalloc_init_malloc; ///< работает прямым вызовом
#endif
Рабочие функции(вроде fxalloc_init_alloc) не знают о существовании дополнительной переменной, им без разницы кто их вызывает, они выполняют свои прямые обязанности несмотря на контекст.
Промежуточный итог
Не смотря на общую парадигму проекта KISS+YAGNI стоит реализовать защиту в виде proxy + CP ибо нам самим потом будет гараздо удобнее профилировать и настраивать сервер(на клиентах будет достаточно обычного malloc/free).
06.05.2026(Раздел редактируется)
Итоговый интерфейс
Функции
-
fxalloc_profile:Изменяет режим работы аллокатора для всех потоков одновременно
Параметры
Profile(eFXAllocProfile) — режим работы аллокатора, см.eFXAllocProfile
Возвращаемые значение
int— предыдущий режим работы.- при
Profile == FXALLOC_GETPROFILE— текущий режим работы.
-
fxalloc_profile_threadИзменяет режим работы аллокатора только для текущего потока
Параметры
Profile(eFXAllocProfile) — режим работы аллокатора, см.eFXAllocProfile
Возвращаемые значение
int— предыдущий режим работы.- при
Profile == FXALLOC_GETPROFILE— текущий режим работы.
Структуры/перечисления
enum eFXAllocProfile — перечисление режимов работы аллокатора, используется как аргумент или возвращаемое значение функций int fxalloc_profile(eFXAllocProfile Profile)(глобально) и int fxalloc_profile_thread(eFXAllocProfile Profile)(для текущеко потока).
| Элемент | Описание |
|---|---|
FXALLOC_SPEED |
Максимальная производительность без статистики. |
FXALLOC_SUMMARY |
Поверхностная статистика: количество выделенных/свободных блоков по грейдам |
FXALLOC_FULL |
Глубокий анализ расхода памяти: количество выделенных/свободных блоков по грейдам, средний объём использования, минимальное/максимальное использование |
FXALLOC_GETPROFILE |
Используется для получения текущего профиля. |
08.05.2026
С одной стороны хорошо быть "безработным", можно подольше поспать, погулять, подурачиться... Однако, мы тут сурьёзную архитектуру аллокатора архитектуем... Чтож, продолжим, на чём мы там остановились... пуру-пум-пум-пум... ТОЧНО! Архитектура!
Итак, между семьёй и сном я всё-же накидал небольшой интерфейс отражённый в includes/FXAlloc.h и внутреннее устройство — headers/_FXAlloc.h. Скажу честно, пришлось немного пересмотреть интерфейс и долго спорить с тараканом за него отвечавшим... В итоге я победюл(и таракана, и Алису подробности) и финальная версия увековечена в соответствующем хидере(но это не точно 😂).
Ключевые моменты интерфейса
Давайте посмотрим что там тараканы в моей голове