Не буду долго рассусоливать о том, что такое сборщик мусора и для чего он нужен (на эту тему уже есть достаточно статей). Но хочу отметить несколько важных деталей. Сборщик мусора - один из способов решения проблемы контроля памяти. Основное отличие от "умных" указателей с++ заключается в том, что подсчет ссылок на объекты и дальнейшее решение о высвобождении памяти происходит не в самом указателе, а в неком одном глобальном объекте. Очистка происходит не одного объекта, а всех, или порции невалидных. Сборщик мусора - не аллокатор. Хоть проблема выделения/освобождения памяти затрагивает вопрос о фрагментации/дефрагментации кучи, но сборщик мусора не призван ее решать. Он может работать в связке со своим аллокатором, но основное его преимущество заключается в том, что память очищается не хаотично и не "лавинообразно". При написании игрового движка появилась потребность в автоматическом контроле памяти (удивительно, не правда ли?). Поэтому было решено написать свой сборщик мусора и заодно попытаться выжать из него максимум производительности. Основные требования: Скорость по выделению и освобождению памяти должна быть сравнима с обычными c-style указателями. Объем затраченной памяти на работу сборщика может быть большим, но разумным. Сборщик должен предоставлять удобный интерфейс сравни "умным" указателям. Основная идея жизни объектов - объект жив до тех пор, пока существует кто-то, кто ссылается на него, но возможно прямое удаление объекта. Возьмем за основу архитектуру "умных" указателей и перенесем всю работу с ссылками на объекты в некий singleton. Действующие лица:  В нашем случае отдельный класс под слабые ссылки не нужен, так как наша парадигма подразумевает использование только одного типа указатель, а создание слабой ссылки происходит за счет использования обычного указателя. Подразумевается, что проект со сборщиком мусора использует только предоставляемый новый умный указатель, а обычные указатели использует только либо в контексте слабой ссылки, либо в изолированных системах с ручным контролем памяти. 1) Начало Обозначим интерфейс работы. GGarbageCollector  Как видим, наш сборщик умеет не только собирать мусор, но и отвечать на вопрос о валидности адреса. Адрес - это число, поэтому у него можно спросить об обычном указателе, если конечно тот когда-то был виден сборщику, но об этом позже.   TGCPointer  Здесь аналогичная ситуация. Наш указатель является оберткой над сишным указателем, но умеет говорить о валидности адреса (разумеется через сборщик мусора). Слабые ссылки в виде обычных указателей проверяются через сборщик мусора. Из представленных интерфейсов пока не понятно, как это будет работать, но видна мечта - спрашивать систему о валидности адреса как числа. 2) Реализация Для того, чтобы что-то сказать о валидности адреса, нужно сначала сохранить какую-то мета информацию о нем. В нашей системе эту информацию будет поставлять TGCPointer, но как ее хранить?  Для максимальной скорости динамической вставки в начало нам подходит односвязный список. Но как обеспечить максимальную скорость чтения? Обычный статический массив обладает этим свойством. Но ничего не бывает просто так. За скорость приходится платить памятью. Совместим статический массив и односвязный список. Так потребление памяти сборщиком будет равна const + число отслеживаемых объектов * размер информации об объекте. Теперь обращение к элементу будет происходить не перебором всех элементов списка, а перебором некой его части. Хоть мы и не получили лучше асимптотику (все еще O(n)), но деленная на константу (2^k). Эту константу возьмем большой(65536 эквивалентно 64 KB или 1048576, что эквивалентно 1 MB) По замерам (о них в конце), другие значения сильного прироста не дают. Такое решения является оптимальным только в том случае, если наш сборщик рассчитан на работу менее с чем 2-3 миллионами объектов, но для игр этого оказывается достаточно + мы будем регулярно (но не очень часто) производить очистку. Этот момент является ключевым, дальше будет производиться оптимизация. 3) Обмажемся абстракциями FAddrHandler  Тут ничего необычного. Как видно, мы реализуем сборщик мусора на основе подсчета ссылок. FAnyPtrMap  Здесь и происходит вся основная работа. Замечу некоторые оптимизационные моменты: Имеем два метода на добавление объекта. Один обычный (с проверкой на то, что он новый), второй без проверки. Это сильно экономит процессорное время в случае, если мы уверены, что объект точно новый. Определение номера ячейки в статическом массиве является обычным взятием остатка от количества элементов. Но количество элементов кратно двойке, поэтому можем применить бинарную операцию (компилятор сам бы это сделал, но так спокойнее). FGCPointerBase  Еще одна важная оптимизационная деталь, которая присуща "умным" указателям и мы хотим ее тоже иметь. А именно кеширование данных. В "умных" указателях инкремент количества ссылок происходит напрямую, так почему бы нам тоже так не делать. Ведь обращение к указателю происходит чаще очистки. Давайте один раз синхронизируемся со сборщиком о валидности, а далее только после очистки, да и то, только при обращении к  указателю. Вот во что превратились наши основные объекты: GGarbageCollector  TGCPointer  Как видно, создание нового объекта не вызывает перебор имеющихся, ибо мы точно знаем, что система выдаст новый адрес, не занятый. Кроме того, благодаря синхронизации, работа с нашим указателем сравнима с работой обычного указателя, даже проверка на валидность не вызывает ни какой нагрузки. А это очень важно, так как это чуть ли не основная фича. 4) Последние оптимизационные штрихи До этого мы не рассматривали сам процесс освобождения памяти. А это 50% успеха сборщика мусора. Пора это исправлять. Очистка происходит в FAnyPtrMap. Лобовым решением было бы пройтись по всем ячейкам массива, проитерироваться по списку и удалить все PendingToKill объекты. Но это долго. По сути так мы теряем в скорости из-за большого размера статического массива, который обеспечивает нам скорость на добавление/чтение элементов. Не беда! При удалении объекта (вызов метода RemovePtr или RemovePtrByHandler) будем записывать номер ячейки массива в отдельный "массив". При сборке мусора будем итерироваться уже по нему. Так мы точно не будем совершать холостых итераций. Для определения того факта, что ячейка массива помечена как кандидат на итерирование очистки, заведем маску ячеек. По-простому - еще массив булов размером в количество ячеек нашего основного массива.Как мы знаем, бул занимает 1 байт, т.е 7 бит лежат впустую. Исправим и это(мы и так кучу памяти пожрали). TGarbageBoolMask  Вот так запакуем наши булы. FGarbageListsToClearArray  Тут реализован стек на базе статического массива. Этого нам достаточно. Максимальная скорость чтения/записи. Вот во что превратился FAnyPtrMap  Замечу, что тип запаковки булов в TGarbageBoolMask выбран size_t. Это самый большой тип в системе, благодаря чему физическое количество элементов массива под кусочки маски будет меньше. А это более быстрое создание/удаление статического массива. Мелочь, а приятно. Ну и вишенкой на торте станет многопоточная сборка мусора. Сыграем на том, что все необходимые нам массивы статические, а сам размер массива большой (создание тредов окупает их производительность). Метод ClearInvalidPtrs  Все потоки не конкурируют и обрабатывают примерно одинаковый объем данных.  Замеры производились на одной машине, поэтому конкретные числа времени работы программ очень частные. Лучше смотреть на отношения производительности. Потребление памяти зависит тоже от архитектуры. Платформа: x86-64Процессор: Intel Core i5-11400FОперационная система: Windows 10Среда разработки и замеров: Visual studio 2022 community В тестах используется структура типа: Выбран нестандартный тип. Количество элементов в тесте = 1 миллион. 1) Потребление памяти Сборщик мусора предусматривает 2 режима работы: С меньшими затратами памяти, но "медленнее". С максимально разумными затратами памяти. потребление памяти = базовый размер сборщика + количество элементов участвующих в сборке * размер контейнера мета информации + количество используемых указателей * размер указателя Размер нашего указателя(TGCPointer) = 24 байтРазмер std::shared_ptr = 16 байтРазмер обычного указателя = 8 байт Размер мета информации(FAddrHandler) = 16 байтРазмер мета информации(std::shared_ptr) = 8 байтРазмер мета информации(обычный указатель) = 0 байт Базовый размер сборщика при размере статического массива 65536 ~= 4MBБазовый размер сборщика при размере статического массива 1048576 ~= 69MB 2) Производительность Тест производительности заключается в создании и удалении 1 миллиона объектов максимально быстрым способом. Тест обычного указателя: Тест std:shared_ptr: Тест сборщика мусора: В многопоточном режиме сборки мусора используется 8 потоков.Все числа указаны в микросекундах(ну и округлены в силу погрешности).  Development build Release build  Создание Удаление Сумма Создание Удаление Сумма Original 82000 72000 155000 27000 17000 45000 std::shared_ptr ? ? 380000 ? ? 73000 single thread 64KB 390000 280000 670000 60000 80000 140000 multi thread 64KB 390000 870000 1,3M 60000 30000 90000 single thread 1MB 550000 260000 800000 67000 46000 115000 multi thread 1MB 550000 780000 1,3M 67000 18000 85000 Отношение производительности(для Release build)  Original std::shared_ptr single thread 64KB 3,11 1,91 multi thread 64KB 2 1,23 single thread 1MB 2,56 1,57 multi thread 1MB 1,89 1,16 std::shared_ptr 1,62 1 Скорость сборки мусора в многопоточной версии максимально близка к стандартной очистке памяти. Это победа!Общая производительность в критической ситуации в 2 раза медленнее стандартных указателей. Как видно, объем затраченной памяти сильно влияет на однопоточную версию. В играх обычно объекты создаются (большая часть) в начале и живут долгое время, поэтому, учитывая, что очистку не придется производить слишком часто, то работа с нашими указателями практически полностью превращается в работу как с обычными указателями. Но плюшки в виде точного ответа на вопрос о валидности объекта и спокойствие за то, что память с большей вероятностью не утечет, позволяет закрыть глаза на повышенный расход памяти. Я считаю, что получаемый выхлоп в виде автоматической очистки памяти + большая лояльность к фрагментации кучи + дружелюбный интерфейс - оправдывает себя. Или нет?  Что касается использования этого сборщика. Его место в геймплейной логике, где важна читаемость, скорость разработки и меньше возни с технической частью. Чтобы было проще погрузиться в творческий процесс. Очевидно, в коровых системах подойдут либо "умные" указатели, либо стандартные указатели, без них ни куда. И еще раз отмечу. Сборщик мусора нельзя отождествлять с "умными" указателями. Хоть они и решают одну задачу, но "умные" указатели очищают память сиюминутно, не задумываясь "об окружающих", а сборщик молотит в подходящий момент (например, при загрузке уровня). Думаю, что было бы хорошо не просто удалять объекты, а перемещать невалидные объекты в один линейный кусок памяти и очищать его, а  используемые объекты держать в другом. Так мы поможем системе в выделении новой памяти. P.S. Хоть и можно использовать обычные указатели как слабые ссылки, но лучше создать отдельный указатель, который будет заниматься кешированием по аналогии с представленным в статье умным указателем.