Files
fxalloc/DAIRY.md
T

43 KiB
Raw Blame History

DAIRY.md

Дисклеймер: Дневник не является технической документацией. Его основная цель показать как работает больная фантазия автора проекта. Вся документация по проекту описана в файлах README.md соответствующих модуей и самого проекта.

23.04.2026

Эта запись - скорее мысли вслух, или самоуспокоение, я пока не решил, но - не суть... Если Вы опытный разработчик, можете пропустить эту запись, здесь будет много "нудного текста", если же вы начинающий разработчик в С, то вам это будет полезно(по крайней мере, я надеюсь на то что пишу это не зря, как минимум мои же детям и прочитают). Рано или поздно каждый разработчик сталкивается с решением нетривиальной задачи по реализации собственного аллокатора, почему:

  • наименьшее из зол - время выделения памяти стандартными методами malloc, calloc и new.
  • наибольшее из зол - фрагментация этой самой памяти, особенно когда дело касается высокой вариативности.

Встаёт вопрос как решить эту проблему и вот конкретно в этом месте начинается магия. Почему магия, потому что при разработке аллокатора можно напороться на очень много рифов. Итак, присаживайтесь по-удобнее, запасайтесь печеньками, а я - продолжу. ;) На этапе продумывания архитектуры данного проекта я понимал каким будет аллокатор и что мне от него нужно, да помимо всего прочего - есть парочка готовых проверенных решений, как обычных, так и многопоточных(jemalloc или tcmalloc), но, это не тот случай, мы же тут навыки восстанавливаем, поэтому вот Вам полёт больной фантазии в парадигме MVP+KISS+YAGNI(быстро, просто, без излишеств), пример буду приводить на прокси-сервере Lineage2(для наглядности).

Включаем режим "Архитектор lvl-80" MVP+KISS

Делаем себе своеобразное ТЗ:

  1. Разнородные блоки(проектируем сервер)
  2. Многопоточность(всё ещё проектируем сервер)
  3. Один из важнейших аспектов - делегирование памяти между потоками(оптимизация: уменьшение дополнительных выделений и копирования).

Препрофилирование: расчёт предполагаемой нагрузки на стадии проектирования архитектуры

Для определения градаций блоков памяти серьёзные дяди-тёти берут статистику типовых нагрузок в схожих условиях, чтож последуем их примеру, открываем "Я.браузер" → "АлисаAI" → "Новый чат" и просим Алису расчитать максимальную нагрузку на аллокатор для прокси-сервера Lineage2 при 200 линий(400 подключений), получаем от неё примерную нагрузку:

Ключевые метрики нагрузки

Параметр Средняя нагрузка Пиковая нагрузка
Пакеты/сек 4 000 30 000
Трафик 4 Мбит/сек 60 Мбит/сек
Аллокации/сек 4 000 30 000
Потребление памяти 3,2 МБ 10 МБ

Далее просим её вывести подробные градации для этой нагрузки , получаем следующие варианты:

Сводная таблица по всем группам

Группа Диапазон размеров (байт) Доля трафика (%) Общая частота (пакетов/сек) Фрагментация (%) Оптимизация
1. Сверхмалые 3264 510 4200 2025 Slab‑аллокатор, пулы фиксированного размера
2. Малые 65128 2025 1 2008 000 1520 Кэширование буферов, пакетная обработка
3. Средние 129256 2530 8004 000 1015 Пулы памяти, переиспользование буферов
4. Крупные 257512 1520 2002 000 510 Предварительное выделение буферов
5. Очень крупные 513–1 024 1015 801 200 38 Пакетная обработка, буферизация
6. Гигантские 1 025–4 096 25 4200 < 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:

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;

Как это влияет на суммарную сложность:

  1. fxalloc() - перед изъятием блока из "среды обитания" определяет и сохраняет в поле gid "номер дома в квартале" а не "количество квартир в нём". Алгоритмическая сложность O(n).
  2. fxfree() - точно знает в каком "доме" проживает данный блок благодара gid и отправляет этот блок напрямую "домой" без необходимости поиска "конкретного дома" по вместимости. Алгоритмическая сложность O(1).

Это микрооптимизация, но, на уровне архитектуры это важный нюанс, принцип хеширования - основа оптимизации. Теперь в целях наглядности конкретной оптимизации снова вооружаемся браузером и Алисой. Просим её расчитать примерное время для новой концепции и наших градаций, радуемся результату и смакуем(эта радость будет недолгой): Сводная таблица: расчётное время выполнения fxalloc() + fxfree() для приведённых градаций

Группа Диапазон размеров (байт) Кол‑во градаций (n) Сложность fxalloc() Сложность fxfree() Суммарная сложность Расчётное время (операции, худший случай)
1. Сверхмалые 3264 7 O(n) O(n) (без gid) / O(1) (с gid) O(n) / O(n)* 14 (7+7) / 8 (7+1)
2. Малые 65128 7 O(n) O(n) / O(1) O(n) / O(n)* 14 / 8
3. Средние 129256 7 O(n) O(n) / O(1) O(n) / O(n)* 14 / 8
4. Крупные 257512 7 O(n) O(n) / O(1) O(n) / O(n)* 14 / 8
5. Очень крупные 5131 024 7 O(n) O(n) / O(1) O(n) / O(n)* 14 / 8
6. Гигантские 1 0254 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 и накидаем немного полей в структуру пула памяти :

/// @brief Группа блоков одной градации
typedef struct FXGradedMemoryPool {
    /// @brief Указатель на последний свободный блок
    FXMemoryBlock* free;
    /// @brief Всего блоков в данной группе
    umword_t total;
    /// @brief Количество преаллоцированных блоков
    umword_t count_pre;
    /// @brief Количество используемых блоков
    mword_t used;
    /// @brief Количество свободных блоков
    mword_t free;
} FXMemoryPoolGrade;

Таракан отвечавший за раздел свалил в неизвестном направлении...

26.04.2026

Что ж, примерное представление о работе сети и нагрузках на асинхронный ввод-вывод мы получили, но, мы брали в рассчёт Lineage2 где достаточно высокая вариативность пакетов, теперь вернёмся к нашему проекту прикинем примерную вариативность пакетов, учтём железо на котором будет работать сервер и посмотрим сколько он сможет выдержать клиентов в теории. На что в первую очередь стоит обратить внимание:

  • Железо
  • Ресурсы потребляемые ОС
  • Ресурсы потребляемые сторонними сервисами при их наличии(Web-сервер, почтовый сервер и т.д.)
  • Минимальный необходимый запас прочности(как правило вкладывается 20-30% из-за деградации и вариативности нагрузок как отдельно взятого элемента, так и системы в целом)

Железо:

Отсчётная точка любого инфраструктурного проекта

Параметр Значение
OS: Ubuntu Server 24.04
CPU: Intel Core i53470, 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(заявленые изготовителем):

Параметр Значение
Тип оборудования Маршрутизатор (роутер) / точка доступа
Стандарты WiFi 802.11a/b/g/n (2.4 ГГц), 802.11ac (5 ГГц)
Частотные диапазоны DualBand: 2.4 ГГц и 5 ГГц
Скорость Wi‑Fi (макс.) 2.4 ГГц: до 600 Мбит/с — 5 ГГц: до 1 300 Мбит/с Суммарно: AC1900
Порты 1 × RJ45 WAN (1 Гбит/с) — 4 × RJ45 LAN (1 Гбит/с)
Технологии MUMIMO (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               < Максимальное количество файловых дескрипторов на процесс

Ничего, лично я,— "пингвин со стажем", можем поднять и до 65 535 на процесс, однако на данном этапе нам нет такой необходимости.

27.04.2026

На часах давно уже сегодня, пора пошугать тараканов, кофе налил, семья спит, продолжаем...

О сути расчётов по вафляю, что они нам дают 426к+ клиентов - весьма сомнительное число, но почему-бы на вложить его в основу, а вот почему: не учтены заголовки TCP/IP, а это не много ни мало - минимум 40 байт, соответственно - количество полезной нагрузки на сетевой трафик из наших расчётов будет 25:40, пересчитываем размер пакета: $\frac{85,3×10^6}{65×8}≈163961$ guiuser-ов, вот, это уже ближе к теме.

Предварительный итог профилирования сети: расчётное время между аллокациями оценено ~2,5 мкс, что критически мало, это, конечно не самое худшее что может быть, но, и не так уж красиво, среднее время на выделение оптимизированными аллокаторами ~10-15 мкс, есть куда расти.

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

Промежуточный итог над принципом рабботы с памятью(вставлено как есть):

Итоги обсуждения архитектуры

  • Основные решения приняты:
    • Разделение потоков ввода/вывода
    • Циклическое управление памятью
    • Минимизация аллокаций вне TLS
    • Классификация пакетов по размеру
  • Следующий шаг:
    • Проработка деталей реализации
    • Документирование принятых решений
    • Подготовка к имплементации
  • Важные моменты для дальнейшей работы:
    • Детальное проектирование системы памяти
    • Реализация механизма кругового буфера
    • Оптимизация работы с TLS
    • Тестирование производительности
  • Рекомендации перед сном:
    • Зафиксировать текущие решения в документации
    • Подготовить чек-лист для реализации
    • Определить приоритетные задачи на следующий этап
  • План на завтра:
    • Проработка схемы памяти
    • Проектирование системы буферов
    • Оценка необходимых изменений в текущей архитектуре
    • Начало подготовки документации по новой архитектуре

Удачи с отдыхом!

Завтра будет новый день и новые возможности для оптимизации архитектуры. Главное — хорошо отдохнуть и подойти к работе с новыми силами.