this->notes. – Telegram
this->notes.
4.54K subscribers
25 photos
1 file
330 links
О разработке, архитектуре и C++.

Tags: #common, #cpp, #highload и другие можно найти поиском.
Задачки: #poll.
Мои публикации: #pub.
Автор и предложка: @vanyakhodor.
GitHub: dasfex.
Download Telegram
#cpp
[видео]
CppCon 2019. Timur Doumler. Type punning in modern C++.
Автор рассказывает об алиасинге типов и strict alias rule, выравнивании, неопределённом поведении и как его избежать.
youtube.com/watch?v=_qzMpk-22cc
1👍1
#cpp
Динамический неоднородный плотно упакованный контейнер.
habr.com/ru/post/302372/
1👍1
#cpp
Элементы функционального программирования в C++.

[из Википедии] Частичное применение - возможность в ряде языков программирования зафиксировать часть аргументов многоместной функции и создать другую функцию.
habr.com/ru/post/313370/

Композиции отображений.
habr.com/ru/post/328624/
1👍1
#algo
Довольно приятный плейлист про не очень стандартные алгоритмы, которые ещё и хорошо поясняются.
www.youtube.com/playlist?list=PLc82OEDeni8SGp5CX8Ey1PdUcoi8Jh1Q_
1👍1
1👍1
#cpp
Помоги компилятору помочь тебе.
Автор рассказывает о флагах компиляции, что они делают, и почему стоит их использовать в своих проектах.
habr.com/ru/post/490850/
1👍1
#cpp #proposals
Пачка новых предложений за январь.

1. У бумаги про std::hive уже 19 ревизий. И тут ещё появилась просьба вернуть это предложение в 23й стандарт. Оч интересно, как это предложение летает туда-сюда. Мне почему-то забавно.

2. Пары и туплы очень похожие типы, ведь первое просто частный случай второго. Туплы могут быть сконструированы из пар, но не наоборот. Такое положение наводит на мысли, что std::pair немного избыточен. Удалять его конечно никто не будет, но туплам можно добавить некоторые возможности пар, чтобы последние использовались реже. Proposal.

3. Из C делают современный язык, хех. Но местами выглядит это конечно мда: Basic lambdas for C, Improve type generic programming.

4. Ослабление ограничений для constexpr. На самом деле тут ничего капитального. Просто какие-то мелочи.

5. std::breakpoint для остановки при выполнении в дебаге. Честно говоря, не очень понимаю, зачем тащить это в язык. Дебагеры хорошо справляются.

6. std::is_debuger_present для понимания, отлаживается ли сейчас программа. Вот это уже что-то полезное. Довольно часто приходилось пихать вывод только для отладки.

7. Сделать move_iterator<T*> random_access итератором. Это поможет выполнять некоторые операции с ренджами эффективнее.

8. Более строгое требование для атрибута [[assume(...)]].
1
#c #cpp

Попалась довольно сомнительная статья на хабре про редкие возможности в С. Учитывая половину списка, не очень понятно, что такое редкие возможности в понимании автора. Не предлагаю вам её читать, чтобы не тратить время. Остановлюсь только на самых интересных моментах и прокомментирую/добавлю немного информации.

1. Конкатенация строк времени компиляции.
Думаю, вы тоже видели какой-то подобный код:

std::string very_long_string = "This is first part of long string." "And this is second";

Никаких плюсов и функций конкатенации: компилятор сделает это одной строкой сам. Используя такое поведение можно реализовать интересный макрос:

#define LOG(exp) std::cout << "Result of " #exp "=" << exp;
int x = 12;
LOG(x*5);


Результатом будет "Result of x*5=60".

2. Препроцессорная склейка строк.
Как вы думаете, будет ли инкремент в следующем коде?

int inc(int x) {
// doing increment\
x++;
return x;
}


Вопрос в том, что произойдёт раньше: конкатенация строк, разбитых через \, или замена комментария на пробельные символы? Легко можно убедиться, что x++ станет частью комментария, то есть строки конкатенируются раньше. Я как-то смотрел замечательную лекцию по тулчейну, где пояснялся этот момент и думал "ну разве можно так набагать", а недавно мне рассказали, что обнаружили подобное в проде. Забавно.

3. Функции в стиле K&R.
Не знал, что такая возможность раньше была в C. Суть заключается в том, что вы объявляете функцию следующим образом:

int foo(s, f, b)
char* s;
float f;
struct Baz * b;
{
return 5;
}


в то время как сегодня она выглядела бы более привычно:

int foo(char* s, float f, struct Baz * b) {
return 5;
}


4. tmpfile.
В статье конечно речь идёт и сишной функции, однако есть аналог std::tmpfile (который тем не менее ничем не отличается). Автор не приводит каких-то полезных применений временных файлов. Мне когда-то было полезно при написании внешней сортировки (правда, там было использование std::tmpnam, но сути не меняет), а ещё, если внимательно посмотреть на то, что делает ваш компилятор при билде программы (например --verbose для g++), то можно увидеть, что компилятор создаёт некоторое кол-во временных файлов, которые можно даже попросить его не удалять.

5. Отрицательные индексы.
Делается такое довольно просто:

int* arr = new int[100] + 50;

Теперь можно обращаться по индексу в обе стороны. Только, как и всегда, стоит быть осторожным с границами и очищением памяти : )

6. std::new_handler.
В C++ есть такое понятие как new_handler (не новый обработчик, а обработчик оператора new). В случае, если new не может выделить необходимое количество памяти, он вызывает свой обработчик в надежде, что тот разрулит ситуацию: сделает дефрагментацию, что-то освободит, переназначит обработчик или что-нибудь ещё. В случае повторного неуспеха обработчик будет вызван ещё раз, то есть такая программа вполне приводит к бесконечной рекурсии:

void f() {}

int main() {
std::set_new_handler(f);
int* p = new int[1000000000000];
}
👍4🔥21
#highload

Попытаюсь с умным видом вкинуть инфы про высоконагруженность.

Очень важной задачей при разработке высоконагруженных систем является поддержание отказоустойчивости (часто хочется удержать что-то вроде 99.99% аптайма). У нас проверка работоспособности сервисов проверяется довольно понятно: проводятся учения по отключению одного из датацентров. Такие учения бывают внешние (для всей компании) и внутренние (для конкретных сервисов и продуктов). По своему (небогатому) опыту могу сказать, что такие мероприятия очень полезны: вроде на прошлой неделе всё прошло успешно, а сегодня уже проблемы с коннектами в базе, тайминги выросли или на сервисе начинает загораться congestion control.

Но рассказать хотелось бы об инструментах Netflix: chaos monkey.
Chaos monkey является частью chaos engineering (хотя можно сказать, что стала началом):

Chaos engineering is the discipline of experimenting on a distributed system in order to build confidence in the system's capability to withstand turbulent conditions in production.

Суть chaos engineering в том, что вы проверяете, как ваша система работает при псевдо-случайных отключениях различных своих частей, что в целом довольно близко к естественным ситуациям, которые часто происходят с высоконагруженными системами.

Обычно падения настраиваются, чтобы отключение инстанса происходило в тот момент, когда команда наиболее готова к сражению с падением. "Падать" могут лишь те машинки, которые помечены для этого доступными. Все доступные для "уранивания" машинки делятся на группы. Каждый будний день обезьяна приходит в каждую группу инстансов и бросает монетку (если точнее, то монетка взвешеная), чтобы решить, ронять ли какую-то машинку из этой самой группы. Если она решает, что надо, то шчедулит падение на случайное время в промежутке от 9:00 до 15:00. Каждое приложение/сервис определяет 2 величины, на основе которых бросается монетка: среднее время в рабочих днях между падениями и минимальное время в рабочих днях между падениями.

Из минусов можно отметить:
- для chaos monkey необходимы spinnaker (система развёртывания в нетфликсе) и mysql;
- генерирует только псевдослучайные падения конкретного инстанса приложения/сервиса, в то время как разнообразие проблем в реальной жизни гораздо шире;
- работу chaos monkey нельзя просто так прервать. Если что-то пойдёт не так, остаётся только сражаться.

В какой-то момент одного инструмента стало не хватать, потому появилась simian army (которая, тем не менее, уже не поддерживается).

Simian army включает в себя ещё три основных инструмента:
1. Janitor monkey или сегодня swabbie -- аналог сборщика мусора для облачной экосистемы.
2. Conformity monkey проверял соответствие инстансов некоторым предефайненым правилам. В случае несоблюдения правил, инстанс отключался. Сейчас это часть spinnaker.
3. Security monkey проверял инстансы на различные дыры в безопасности.

и ещё несколько, которые были депрекейтнуты сильно раньше или не опубликованы:
4. Chaos gorilla, который симулировал отключение одной из 25 зон инфраструктуры AWS (т.е. отключение огромной части всех инстансов).
5. Chaos kong -- аналог прошлого инструмента, но симулирующий отключение меньших частей инфраструктуры.
6. Latency monkey -- тулза, увеличивающая тайминги ответов некоторых ручек в сервисах.
7. Doctor monkey -- инструмент, мониторящий состояние инстансов. Если инстанс "заболел" (пятисотит, выросли тайминги или кушает много cpu), doctor убирает его из приложения.
8. 10-18 monkey (i.e. l10n-i18n) проверяет работу сервисов, которые работают в различных географических зонах. Чекаются проблемы связанные с локализацией и всем что около.
1👍1
Массивная и интересная (мне показалось) статья про fork().
https://habr.com/ru/post/586604/
1👍1
#common

Аллокатор как метод управления памятью впервые появился в С (речь не о std::allocator, а о самом термине, который был реализован в malloc/free), а своё, можно сказать, известное воплощение получил в C++ как std::allocator (в 1994м) и std::pmr::allocator (В C++17). Однако не одними C/C++ едины. Аллокаторы также существуют и в других языках программирования (как самостоятельный термин, так и наследие/заимствование C/C++).

В силу развития языка в какой-то момент аналоги malloc/free появились в COBOL и Fortran 90. Аналогичный метод управления памятью есть и в D (malloc, new), т.к. он появлялся как более правильный C++ и тянет за собой часть стандартных библиотек C/C++, хотя сам D имеет свой сборщик мусора.

Но эти примеры немного надуманы, потому что термин аллокатор тут появляется из аналогичности методов управления памятью.
Более честными примерами являются:

1. Rust.
Из интересного можно отметить две вещи.
В расте есть глобальный аллокатор, который в отличие от C/C++
очень легко подменить:

#[global_allocator]
static GLOBAL: MyAllocator = MyAllocator;


До версии 1.28 rustc неявно линковал jemalloc в каждую свою программу. Чтобы ослабить зависимость от libc, это поведение изменили в сторону стандартного системного аллокатора.

update: В C++ тоже можно "подменить" глобальный аллокатор.
Перегрузим глобальные операторы new/delete, засунув в них соответствующие операции из готового аллокатора. Единственное что это всё-таки немного неполноценно в силу того, что используемый аллокатор может работать только с памятью (ведь вызов конструктора/деструктора не переопределяется), а интерфейс std::allocator до C++20 (пусть он и не прав идеологически, что такими вещами занимается) всё-таки хочет и это контролировать. С 20ого стандарта методы construct/destroy удаляются и заменяются на std::construct_at/std::destroy_at. В таком виде можно говорить о корректной замене глобального аллокатора.

2. Zig.
Тут нет дефолтного аллокатора. Чаще всего вы выбираете свой аллокатор согласно рекомендациям и отдаёте его как параметр. Если вы пишете свою библиотеку, авторы предлагают придерживаться подобной концепции и требовать у пользователя аллокатор, который ему нравится. А вообще он предоставляет (не сказал бы что богатый, но) зоопарк из нескольких аллокаторов, которые вам могут понадобиться: от std.heap.c_allocator (C malloc/free) и нескольких пул-аллокаторов до аллокаторов для тестирования и std.heap.GeneralPurposeAllocator, если вам ничего не подошло (так написал, будто между крайними случаями ещё солидное кол-во аллокаторов, хотя на самом деле я перечислил почти все).
3
#common

Сборщики мусора 1/2.

По чуть-чуть начал разбираться со сборкой мусора, так что накидаю вам немного инфы о ней.

Есть несколько базовых алгосов, которые в разных вариациях юзаются в большинстве промышленных garbage collector'ах. В основном они делятся на трассирующие (которые отслеживают достижимость объекта) и прямые. И параллельно делятся на перемещающие и не перемещающие (в моей вольной интерпретации).

Mark-and-sweep.
Самый дефолтный алгоритм. Для каждого объекта хранится бит достижимости. Изначально он ноль. Все объекты указывают друг на друга. Есть специальные корневые объекты. На этапе mark дфсом обходим все объекты, достижимые из корневых, и устанавливаем им бит достижимости в 1. На этапе sweep обходим все объекты и проверяем, если бит достижимости 1, то просто его сбрасываем. Если же он 0, то объект помечается удалённым (обычно пихают его во freelist). Проблемой такого подхода является stop the world -- всё выполнение программы останавливается, пока сборка на закончится.
Есть ещё вариация mark-and-compact, где вместо помечания участка памяти свободным объекты в памяти как-то перемещаются, чтобы немного её дефрагментировать.

Существует улучшенная версия этого алгоритма под названием BF Mark, которая избавлена от этих недостатков.
Вместо двух "цветов" достижим/нет объект используется 3: чёрный, серый и белый. Чёрные объекты доступны из корней и не имеют исходящих ссылок на белые объекты, белые -- кандидаты на удаление, серые -- объекты доступные из корней, но пока не проверенные на наличие ссылок на белые объекты. Алгоритм состоит из трёх шагов: выбрать объект из серого множества и переместить его в чёрное; поместить все белые объекты, на которые есть ссылки из нового чёрного в серое множество; повторять прошлые два шага, пока серое множество не станет пустым. Когда серый набор пуст, сканирование завершено: черные объекты доступны из корней, в то время как белые объекты недоступны и могут быть собраны мусором. Поскольку все объекты, до которых невозможно добраться сразу из корней, добавляются к белому набору, а объекты могут перемещаться только от белого к серому и от серого к черному, алгоритм сохраняет важный инвариант - никакие черные объекты не ссылаются на белые объекты. Это гарантирует, что белые объекты могут быть освобождены после того, как серый набор станет пустым. Такой метод удобен, потому что его можно выполнять на лету.

Ещё популярна копирующая сборка (semispace/Lisp 2/алгоритм Чейни). Алгоритм основывается на том, что выделяется две области памяти одинакового размера. Все объекты создаются в одной из них, вторая при этом содержится пустой. Как и в прошлом алгоритме, есть несколько корневых объектов, которые ссылаются на другие. В некоторый момент все объекты проверяются на достижимость. В случае, если объект валиден, он дублируется во вторую пустую область памяти, иначе удаляется. В итоге все достижимые объекты скопированы в новое место. Произошла очистка ненужных объектов и уплотнение для избежания фрагментации.
👍51
#common

Сборщики мусора 2/2.

Как пример прямого сборщика мусора можно привести считающий ссылки коллектор. Проверка достижимости происходит очень просто: в случае, когда счётчик ссылок у объекта равен нулю, его можно удалять. Однако такая тривиальная реализация не учитывает компоненты связности объектов, где вся группа недоступна, но объекты указывают друг на друга. В таком случае можно усовершенствовать алгоритм и производить поиск циклов или их конденсацию в графе объектов, чтобы бороться с подобными ситуациями. Такой сборщик мусора является довольно простым в реализации, однако имеет значительный недостаток -- все операции со ссылками становятся чуть медленнее (кроме самого подсчёта могут быть проблемы с синхронизациями и подобным).

Одним из самых эффективных алгоритмов сборки мусора является сборщик мусора с поколениями. Он основан на простом утверждении: большинство объектов умирают молодыми.
Новые объекты считаются объектами первого поколения. Если эти объекты переживают n сборок мусора, их поколение становится вторым. Объекты более высокого поколения проверяются реже, т.к. подразумевается, что раз они уже долго прожили, то и далее проживут дольше молодых объектов. В случае, если они опять переживают несколько сборок мусора своего поколения, то они перемещаются в третье поколение. Количество поколений обычно ограничено каким-то небольшим числом (т.е. не растут бесконечно). Правило при обходе графа объектов простое — если нам повстречался объект из более старшего поколения, чем проверяемое в данный момент, дальше в этом направлении не идем. Однако здесь есть тонкий момент. Поскольку объекты в общем случае могут изменяться, они также могут содержать ссылки на объекты более молодого поколения. Поэтому во время выполнения программы необходимо отслеживать ситуации, когда более старый объект начинает ссылаться на более молодой и добавлять в этом случае молодой объект в «корневое множество» объектов соответствующего поколения. Иначе сборщик мусора ошибочно удалит его, как недостижимый.
Иногда полезно знать, как устроен сборщик мусора, которым пользуется разработчик, чтобы не бояться создавать этот самый мусор. Например объекты можно переиспользовать, однако для сборщика мусора с поколениями это будет означать, что объект долгоживущий, из-за чего его поколение вырастет и он будет проверяться реже. Это искуственное продлевание жизни объекта в итоге может сделать операции сборщика мусора менее эффективными.

Чуть позже накидаю про то, как это выглядит в разных языках программирования.
👍71
#cpp

Немножко macros tricks.

Макросы в целом скорее bad practice. Они могут приводить к нечитаемому коду и неожиданному поведению. Но они всё же позволяют делать интересные вещи.

1. При использовании макросов часто совсем непонятно, во что же он раскрывается. Как вам такой код?

int x = 1;
SOME_MACROS(x)


На первый взгляд он кажется неестественным: а почему это компилится? Почему нет точки с запятой? Потому что автор макроса засунул её внутрь. Вы её не поставите и всё скомпилируется. Но кого-то позже введёт это в ступор, кто-то решит поменять ваш макрос и код перестанет компилиться из-за нехватки этой самой ; во всех местах использования. Конечно, можно договориться всегда писать её после макросов, но тогда это лишь рекомендация, которую можно и не выполнять. Лучше будет заставить пользователя макроса писать аккуратно. Например обернув всё тело макроса в do-while:

#define SOME_MACROS(x) \
do { \
++x; \
} while (false)


Ещё вы решаете проблему пересечения имён, т.к. создали новую область видимости. Короч немного накостылили и стало получше.

2. Об этом уже упоминалось, но повторим.
Думаю, вы тоже видели какой-то подобный код:

std::string very_long_string = "This is first part of long string." "And this is second";

Никаких плюсов и функций конкатенации: компилятор сделает это одной строкой сам. Используя такое поведение можно реализовать интересный макрос:

#define LOG(exp) std::cout << "Result of " #exp "=" << (exp);
int x = 12;
LOG(x*5);


Получаем: Result of x*5=60

3. Есть популярные макросы, которые позволяют добавлять немного информации например при отладке (помните, что не все есть везде и их результаты иногда implementation defined).

Первые это __FILE__, __LINE__:

#define assert(expr) \
(static_cast<bool>(expr) \
? void(0) \
: assert_fail(#expr,
__FILE__, \
__LINE__, __ASSERT_FUNCTION))

Или __PRETTY_FUNCTION__:

void f(int) {
std::cout << __PRETTY_FUNCTION__;
}

Получим: void f(int)

Ещё есть __FUNCTION__ и __func__.

И __COUNTER__: по мере вызова в рамках программы он выдаёт натуральные числа от нуля и выше:

std::cout << __COUNTER__ << __COUNTER__ << __COUNTER__;

Результат: 012.

Последний можно использовать для создание макроса для анонимных переменных:

#define CONCATENATE_IMPL(s1, s2) s1##s2
#define CONCATENATE(s1, s2) CONCATENATE_IMPL(s1, s2)

#ifdef __COUNTER__
#define ANONYMOUS_VARIABLE(str) CONCATENATE(str, COUNTER)
#else
#define ANONYMOUS_VARIABLE(str) CONCATENATE(str, __LINE__)
#endif

Теперь вы можете спокойно юзать этот макрос для создания анонимных переменных:

auto ANONYMOUS_VARIABLE(var) = gsl::finally([] {});

Понятно, что это не анонимная в полном смысле этого слова переменная, т.к. её можно использовать, но теперь не нужно думать, какие имена использовать для нескольких переменных, которые нужно лишь создать.

Тут рассказывают, как это сделать чуть более юзабельным.

Кстати бонусом вопрос: почему нам нужен промежуточный макрос CONCATENATE_IMPL?
👍41
#cpp #poll

Задача такая: выполнить какой-то код ровно один раз во время жизни нашей программы. Решение вполне понятное:

void init() {
static bool unused = [] {
std::cout << "print
ed once" << std::endl;
return true;
}();
}


При вызове такой функции сколько угодно раз вывод будет всегда один и тот же (даже из разных потоков, сможете ответить почему?):

printed once

А что, если я хочу вызвать какой-то код ровно n раз? Об этом и предлагаю вам подумать : )

Как обычно, если никто не пробьёт, ответ через пару дней.

UPD: хотелось бы увидеть решение руками, а не с чем-то вроде std::call_once (но ими всё равно можно поделиться :) ).
👍31
this->notes.
#cpp #poll Задача такая: выполнить какой-то код ровно один раз во время жизни нашей программы. Решение вполне понятное: void init() { static bool unused = [] { std::cout << "printed once" << std::endl; return true; }(); } При вызове такой функции…
#cpp #poll

Дисклеймер.
Предвижу, как на меня выливаются вёдра помоев за такой солв, но он интерес
ен чисто с точки зрения языка и приёма, а не как что-то, что стоит юзать в жизни (собственно в оригинальном докладе так и говорится).

Вспомним, как работает этот код:

void init() {
static bool unused = [] {
std::cout << "printed once" << std::endl;
return true;
}();
}


Мы инициализируем статическую переменную. Во-первых, это всегда происходит атомарно (вот такой факт). Во-вторых, только один раз (пруфы на оба утверждения). В-третьих, есть вот такое утверждение:

If the initialization throws an exception, the variable is not considered to be initialized, and initialization will be attempted again the next time control passes through the declaration.

Возникает ужасная идея: поюзать исключения для контроля flow вашего кода (не делайте так пожалуйста). Так что можем сообразить что-то такое:

struct Throwed {};

constexpr int n = 3;

void init() {
try {
static bool unused = [] {
static int called = 0;
std::cout << "123" << std::endl;
if (++called < n) {
throw Throwed{};
}
return true;
}();
} catch (Throwed) {}
}


Как по мне, оч прикона.

Ещё можно найти такой факт:

If the initialization recursively enters the block in which the variable is being initialized, the behavior is undefined.

Никогда не задумывался об этом. Тож интересно.
👍161
#list

Несколько рандомных фактов.

1. Посмотрим на такой код:

struct Kek { int lol; int kek; };
std::vector<Kek> keks;
for (const auto&
[lol, kek] : keks) {
[=] { std::cout << lol << "\n"; }();
}


Неожиданно, он не скомпилируется. Не скомпилируется он на моменте обращения к переменной lol в лямбде. Странно, да?🤔
На самом деле переменные из structure binding не умеют захватываться в лямбды (это, стоит надеяться, временно). Немножко пояснений можно посмотреть тут.

Кстати примерно из-за этого же structure binding становится пацаном довольно неровным, потому что начинает ломать всякое NRVO. Будьте аккуратны.

2. Попался один интересный доклад на CppCon 2016 про оптимизацию вашего кода. Автор предлагал использовать принцип DRY не только в случае написания кода, но и с шаблонами:

struct B {
virtual ~B() = default;
};

template <typename T>
struct D : B {
std::vector<int> get() const { return m_v; }
std::vector<int> m_v;
};


Учитывая, что для каждого инстанцирования код класса по факту будет копироваться, стоит вынести всё что можно в базовый класс:

struct B {
virtual ~B() = default;
virtual std::vector<int> get() const { return m_v; }
std::vector<int> m_v;
};

template <typename T>
struct D : B {};


Так сказать взгляд с другой стороны.
Посмотрите доклад. Там прикона : )

3. При использовании std::function<void(args...)> (с возвращаемым типом void) к такому объекту можно кастовать любой функтор с такими же аргументами, но любым возвращаемым типом. То есть вот такое будет компилироваться:

std::function<void()> f{[] -> int {return 1;}};

Руками полезность этой фичи я пока не ощутил, но говорят, что бывает удобно, когда хочешь показать пользователю, что некоторая функция не использует возвращаемое значение/в схемах с коллбеками и подобном.


И ещё немного ссылочек на выходные:

0. Интересный доклад о переписывании базы данных личных сообщений в Вк.

1. Обзорный доклад про очереди в highload.

2. Статья про то, как важно уметь отказываться от уже сделанного.

3. Удобное сокращение для стандартных стримов в C++: https://github.com/vitaut/_._ (sorry).

4. Статья про некоторые отличия в реализациях частей C++ от разных компиляторов.

5. Новый формат QOI (qoiformat.org), который неплохо сжимает картинки, на уровне PNG, но при этом в десятки раз быстрее, а его спецификация -- одна страница.
👍31
#common #java (сам офигел)

G1GC 1/2.

Я обещал рассказать про сборщики мусора в разных языках программирования, но как-то так вышло, что всё, что хочется вкинуть слишком много для одного раза, так что буду делиться частями, чтобы и вы не померли читать, и я не загнулся писать книжки вместо постов (даже Пиши сокращай не помогает).

Внутри Hotspot JVM (я так понял в мире жавы существует много разных vm прям как компиляторов в плюсах или интерпретаторов в питоне, но это каноничный от Sun microsystems и соответственно Oracle) есть несколько дефолтных сборщиков мусора. Сегодня по ним и пройдёмся.

SerialGC -- последовательный сборщик мусора, состоящий из DefNEW и Tenured. Первый занимается сборкой молодого поколения и является копирующим сборщиком мусора, второй же следит за взрослым поколением и является модифицированным алгоритмом mark-sweep-compact. В силу того, что на обоих этапах происходит перемещение, аллокация аналогично pooled-аллокаторам (выдали указатель, сдвинули cur в пуле).

ParallelGC (ParNEW) -- параллельный сборщик мусора, состоящий из параллельного копирующего для молодого поколения и параллельного mark-compact для взрослого поколения. Аллокация также линейная. Несмотря на параллельность, он всё ещё является stop-the-world, однако паузы в среднем меньше.

CMS -- параллельный сборщик мусора, пытающийся минимизировать паузы приложения и использовать фоновую сборку. При первой остановке приложения фиксируется граф объектов. Далее в фоновом режиме этот граф обходится. На следующей остановке учитываются изменения, которые могли произойти за время фонового обхода, после чего в фоне происходит sweep этап. Структурно используется параллельный копирующий сборщик для молодого поколения, а для взрослого поколения фоновый mark-sweep. Аллокация происходит из freelist'ов, что приводит к фрагментации. Компактификация происходит только в случае, когда памяти начинает не хватать, что приводит к полной сборке мусора (для CMS это полный stop-the-world в один поток🤢). В последних версиях джавы был удалён, т.к. авторы перестали поддерживать.

Я так понял, что первые два есть просто по приколу, и можно юзать их, если вам прям ну не критично совсем, что там да как у вас работает или отдохнуть прилегло. CMS рекомендовался, если хочется паузы поменьше, но куча приложения не оч большая (до 2Гб).
👍31
#common #java

G1GC 2/2.

G1 (garbage-first/G one) является сборщиком мусора общего назначения (i.e. работает со всей кучей). Он является параллельным фоновым сборщиком мусора, который пытается минимизировать паузы приложения. Основной абстракцией является сборка поколениями: существует молодое и взрослое поколение. Молодое поколение содержит три пула: основной (eden) и два дополнительных (survivors). Основной пулл содержит самые молодые объекты, два дополнительных используются для копирующей сборки мусора внутри молодого поколения (то есть внутри молодого поколения существует разделение на подпоколения: прям ну совсем молодые чуваки и объекты, пережившие хотя бы одну сборку). Когда в молодом поколении недостаточно места для новой аллокации, происходит малая сборка мусора. Она заключается в том, что все выжившие в eden объекты перемещаются в один из пулов для копирующей сборки.
Элементы из копирующего сборщика могут быть либо удалены, либо перемещены внутри копирующего сборщика, либо перенесены во взрослое поколение (если сборщик мусора примет такое решение).

Вся куча приложения делится на регионы размером от 1Мб до 32Мб. Каждый регион динамически относится в молодое/взрослое поколение (после сборок мусора регионы могут возвращаться во множество свободных и получать новую роль по необходимости).
В случае молодого регион может выполнять роль eden или survivor.

Особенная роль отводится большим объектам, которые не могут поместиться в один регион. Для них выделяется несколько рядом лежащих регионов. В процессе сборки мусора для таких объектов применяется отдельная логика (правда не обнаружил, какая именно, но вроде как там чуваки борются за дефрагментацию).

Во время сборки мусора выбирается множество регионов, которые будут очищаться (collection set). В него входят все регионы из молодого поколения и возможно некоторые из взрослого поколения (тут есть разделение по типу collection set'а: сборка только в молодых регионах, смешанная сборка и FullGC). Недостижимые объекты удаляются, а достижимые перемещаются в свободные регионы, которые назначаются либо регионами для взрослого поколения, либо survivor-регионами. При хорошем выборе регионов для очистки и засчёт compact новые регионы занимают меньше места, чем занимали объекты ранее (но не обязательно).

Для того, чтобы понимать, какие регионы из взрослого поколения стоит брать в очередной collection set, у каждого региона имеется remembered set, хранящий взаимосвязи объектов между регионами. То есть если объект из региона A ссылается на объект из региона B, то в rset'е B будет запись A. На основе этого ребята делают какие-то выводы.

Все этапы это stop-the-world, но есть этап фоновой маркировки, который стартует при достижении некоторого порога заполненности всей памяти приложения. Тут происходит несколько действий: обновляется информация о достижимости по регионам, освобождение регионов без живых объектов, устранение циклических зависимостей между неживыми объектами.

Когда хорошо юзать:
+ хотите паузы <0.5-1s;
+ минимальные настройки;
+ размер кучи >5Гб;
+ вам норм чиститься только когда больше половины кучи занято;
+ скорость создания объектов сильно меняется;
+ боремся с фрагментацией.

По настройкам кстати флагов у него хватает (кстати они оч забавные: -XX:MaxGCPauseMillis). Можно не пытаться подбирать такие конфигурации размеров регионов, что время пауз станет нормальным, а указать само время, и gc сам попытается подстроиться (не обещает, но постарается).

Вообще вся инфа из доклада одного из разрабов Oracle. Дока.

В планах ещё один java gc и бегло по другиим языкам, потому что пока чего-то крутого/подробного я в них не нашёл.
1
На словах мб не оч понятно, потому вот очень упрощённая картиночка движений объектов при очередной сборке в G1.
1