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
#list

1. Немного расскажу про эпохи.
Это такой механизм, который предлагается использовать для обратнонесмовместимых изменений в C++, но при этом не ломать эту обратную совместимость всем, кто не хочет.

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

epoch 2023;

В пропозале предлагается следующий пример: в C++ существуют неявные приведения. Потому такой код компилируется:

module f;
void f(float) {}
...
f(3.4); // 3.4 is double


Однако если указана конкретная эпоха, с которой запрещаются неявные приведения, код будет падать с ошибкой компиляции:

epoch 2023;
module f;

void f(float) {}
...
// f(3.4); // CE
f(3.4f); // success


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

Новая эпоха будет появляться с появлением нового стандарта и выражаться четырёхзначным числом, которое является годом выхода этой эпохи. Соответственно вы просто сможете компилировать свой код в привычном формате -std=standard.

Однако эпохи не являются инструментом для ломания ABI. Они лишь предлагаются как некоторый инструмент для другого преобразования кода в AST, после чего компиляция идёт обычным способом. Тут конечно грустно.

В пропозале есть ещё несколько примеров, которые можно починить эпохами. Вот несколько интересных (обратите внимание, что пропозал предлагает только эпохи и примеры ниже демонстрируют, какие необычные изменения можно будет ввести в язык без боли):
- оставить переменную неинициализированной:
// int i; // CE
int i = void; // OK, unitialized

- запретить устаревшие конструкции:
// int a0[]{1, 2}; // CE
std::array<int> a1{1, 2}; // OK
// typedef int I0; // CE
using I1 = int; // OK

- ввести новые/переименовать существующие ключевые слова (co_await -> await, co_yield -> yield);
- форсить использование nullptr вместо NULL или 0;
- форсить использование break и fallthrough в switch;
- юзание условного std::movable_initializer_list вместо обычного там, где это возможно;
- форсить для конструкторов использование одного из двух слов: explicit, implicit;
- форсить одно из слов const/mutable для переменных;
- следить за выполнением некоторых core guidlines на уровне языка;
- добавить ещё несколько атрибутов для методов/функций вроде [[accessible_until_epoch(X)]] и [[accessible_since_epoch(X)]]

и другие.

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

Proposal.

2. Небольшая статья про то, как можно использовать макрос __has_include и feature test макросы.

3. Какая-то забавная интересная статья про то, как чуваки своего начальника продавали.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍71
#list

0. Как-то я в недрах миллиона инфы потерял ещё один пропозал, который стоило бы упомянуть в последней статье про обработку ошибок.

operator ??.
Суть примерно та же, что у operator try, но автор предлагает не использовать эту форму, т.к., несмотря на то, что подобное встречается в других языках, в C++ try очень сильно ассоциируется с исключениями. Это может запутать, т.к. цель подобного оператора только лишь пропагация ошибки выше, и семантически ничего общего с исключениями такой оператор не имеет.
Выглядеть использование должно примерно как в Rust:

int f = foo()??;

1. Тут начали подъезжать первые доклады с CppCon’22. Первым по традиции был доклад Bjarne Stroustrup. Но его я не очень осиливаю послушать, мб потому что у него какие-то очень абстрактные рассуждения о судьбе вселенной, когда мне больше заходят какие-то конкретные штуки. Например доклад Herb Sutter о Cpp2. Он написал экспериментальный компилятор cppfront для нового синтаксиса C++, который тем не менее полностью поддерживает стандартную версию языка. Можно увидеть конструкции вроде

main: () -> int {
std::cout << ”Hello, World!”;
}


new<std::string>(); // шаблонный new возвращает std::unique_ptr

f: (inout: s: std::string) = s = “[" + s + “]” // упрощённый синтаксис для однострочников

И ещё много других набросов.

Его компилятор форсит использование новых инструментов вместо устаревших, следование core guidlines и прочее. Из интересного ещё запрещает арифметику указателей, полный запрет использования слова NULL (потому что у него не очень хорошая семантика, которая не несёт ничего хорошего в ваш код). Ещё это происходит через полный запрет некоторых старых инструментов. Например препроцессор. На этом слайде можно увидеть как много в C++ сложностей, которые в cpp2 неприемлемы.

Вообще синтаксис местами спорный, но как будто следует последним модным веяниям в этом месте, из-за чего [кажется] парсинг кода становится гораздо более простым.

Очень много говорил про проблему Python 2/3, когда инкрементальный переход с одной версии на другой невозможен. Про то, как важно сделать C++ более безопасным и простым. Про то, как подобный подход сможет упростить обучение языку.

В планах рассказывал про поддержку в cpp2 тех самых исключений, о которых я чуть-чуть писал в статье выше, про поддержку сборщика мусора, либу внутри cpp2 для рисования всяких штук и про форматтеры для нового синтаксиса. Всё это хочется сделать и со всем этим нужна помощь.

Тут можно найти больше примеров.

Стоит понимать, что это эксперимент, цель которого показать, в какую сторону может двигаться C++, что язык можно упрощать. Конечно, совсем не факт, что это дойдёт до стандарта и что, если всё-таки да, в таком виде. Возможно кто-то вдохновиться и сделает то же самое, но лучше.

cpp2 можно потрогать на годболте. Держите.

2. [из архива] Приконый доклад Nicolai Josuttis о том, как он пытался написать эффективную реализацию конструктора для структуры с двумя строками (и у него долго ничего не получалось).
👍6🔥1
#cpp

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

Заведём структурку, в которую будем складывать необходимую информацию:

struct AllocationInfo {
const char* function;
unsigned int line;
unsigned long count;
unsigned long bytes;
AllocationInfo* next;
};


Это базовая нода листа, в которой будем хранить всю необходимую информацию об аллокации. Ну и заведём голову листа:

static AllocationInfo root;

Теперь напишем макрос, который будет служить базой для функции аллокации:

#define MYALLOCATE(n, name) \
[name_data = name] (size_t bts) { \
static AllocationInfo here; \
static bool firstCall = [name_data]() { \
/*first call, initialize the struct*/ \
here.function = name_data; \
here.line = __LINE__; \
here.next = root.next; \
root.next = &here; \
return true; \
}(); \
/* Cool, now deposit info about calls */ \
++here.count; \
here.bytes += (bts); \
return malloc(bts); \
}(n)


Создаём новую ноду листа, которую инициализируем при первом появлении в строчке, после чего обновляем инфу если надо. Ну и макрос-обёртка:

#define ALLOC(n) MYALLOCATE(n, __PRETTY_FUNCTION__)

Теперь везде где хочется выделять память теперь будем использовать макрос ALLOC. И в конце можем обойти лист, получим всю необходимую информацию:

auto p = &root;
p = p->next;
while (p) {
std::cout << "Func: " << p->function << std::endl;
std::cout << "Line: " << p->line << std::endl;
std::cout << "Times: " << p->count << std::endl;
std::cout << "Bytes: " << p->bytes << std::endl;
p = p->next;
}


Демо. И можем увидеть какие-то такие результаты:

Func: int main()
Line: 46
Times: 1
Bytes: 128

Func: void some_func(size_t)
Line: 36
Times: 2
Bytes: 48

Func: int main()
Line: 40
Times: 1
Bytes: 8


Видим, что в main в строке 46 за один раз было выделено 128 байт; в some_func в строке 36 за два раза 48 байт и в main в строке 40 за раз 8 байт.

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

И самый сок, что весь лист у вас static, т.е. у нас нет динамических аллокаций на него. Только те, которые лежат под вызовом ALLOC. Т.е. информация категорически точная.
Мне когда-то голову взорвало.

Можно немножко зарефакторить код, юзая std::source_location. Оставим как упражнение пытливому читателю.

===================================
Что-то в последнее время начала ощущаться усталость от 2.5 лет нехождения в отпуск. Потому дольше обычного не было постов (ну и ещё из-за запойного просмотра Доктора Хауса). Но, думаю, скоро получится отдохнуть и всё вернётся в нормальный ритм.
👍15
#common

Быть технически прокачаным прогером это безусловно круто. Это то, без чего не стать специалистом, которого все хотят. Это важно, чтобы называть себя профессионалом. Но это не всё.

Важны ещё и софтскиллы. На одной из первых пар в универе молодой чел сказал, что харды ботаются, а софты нет. Потому, пока молодой, надо пытаться побольше их вкачать. И в целом сейчас я скорее с ним согласен. Это капец как важно.

Ещё год назад, несмотря на то, что у меня было нормальное для студента количество опыта, я абсолютно не имел навыка технического общения. А что было два года назад и подумать страшно. И я уверен, что через год про сегодняшнего себя буду думать так же. По крайней мере, к этому есть тенденции, учитывая, что чуть ли не каждый день я, видя своих старших коллег, понимаю для себя что-то новое. Это выражается в том, как они принимают решения, как они поясняют те или иные моменты, как они решают проблемы.

По последнему кстати можно затрекать молодого/неопытного чувака. Как вы сообщаете кому-то о проблемах? Не набивший руку на общение чел вкинет что-то вроде “это не работает”. Чувак с опытом, потративший на решение проблем мало-мальски приличное время, принесёт больше инфы: что он пытается сделать, что не получается (логи/ошибки), может почему не получается (он же подумал перед тем как спросить?), как он пытался её решить и почему это не помогло. Так получается сформировать более общее представление о проблеме и не тратить на это драгоценное время (может вы конечно не цените своё, не подумайте о чужом).
Если вы когда-нибудь задавали вопрос на stackoverflow или другой платформе из этой серии, там чётко написано как правильно это делать. Помните об этом, даже когда напоминалки перед глазами нет.

Ну и помните про nometa.xyz и neprivet.com.
👍14
#cpp

Быстропост.

1. Очевидный, но приконый факт, что в шаблоны можно пихать выражения вроде сигнатуры функций:

std::function<int(int, double)> f;

С точки зрения реализации вашего класса это выглядит как-то так:

template <typename T>
struct A {};

template <typename Ret, typename… Args>
struct A<Ret(Args…)> {};


Теперь можно в шаблон A пихать выражения вроде сигнатуры функции.

Кроме std::function такое используется в std::is_function (реализация которой тупо перебор всех возможных видов функций; неудивительно, но дико).

2. Вычитал, что выражения

std::is_same_v<char, signed char>;
std::is_same_v<char, unsigned char>;


вопреки ожиданиям возвращают false. Всё потому что char это отдельный тип, который не является (без)знаковым (он какой удобно компилятору на конкретной оси). Ну и signed char не попадает под strict aliasing rule, т.е. unsigned char ближе к char функционально, но формально это разные типы.

3. Возможная реализация std::is_signed/std::is_unsigned выглядит забавно. Тут юзается факт, что беззнаковые числа переполняются:

namespace detail {
template
<typename T, bool = std::is_arithmetic<T>::value>
struct is_signed : std::integral_constant<bool, T(-1) < T(0)> {};

template
<typename T>
struct is_signed<T,false> : std::false_type {};
} // namespace detail

template
<typename T>
struct is_signed : detail::is_signed<T>::type {};


По факту проверяется верность выражения T(-1) < T(0), которое для signed типов верно, а для unsigned типов корректно переполняется (потому что переполнение unsigned типов по стандарту не ub) и становится false.
У unsigned выражение наоборот: T(0) < T(-1). Работает аналогично: либо false, либо переполнение и true.
👍14🤯61
#common

О подходах к реализации шаблонов/дженериков.

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

Интересно, как это сделано в других языках. Какие проблемы есть в них, а каких нет.

В Java используется стирание типов (type erasure).

public class A<T> {
private T obj;


public A(T t){
obj = t;
}

public T getObj() {
return obj;
}
}


Однако в рантайме дженерик-типа не остаётся. Класс будет существовать в таком виде:

public class A {
private Object obj;


public A(Object t){
obj = t;
}

public Object getObj() {
return obj;
}
}


Т.е. в дженерик-типе/функции T заменяется на Object, а в месте использования происходит следующее:

A<String> a = new A<>("qwe");
var aa = a.get();


заменится на

A a = new A("qwe");
var aa =
(String) a.get();

Т.е. снаружи есть информация о том, какой тип отдавался в дженерик, что позволяет сделать нужный каст. И тут есть одно очевидное преимущество: вы можете не перекомпилировать пакет с определением дженерика. Однако сразу можно заметить и недостатки: т.к. происходит стирание типа до Object (который является родительским типом для всех типов, кроме примитивных), дженерики нельзя использовать с примитивными типами (ха-ха, лохи). Потому и появились типы-обёртки вроде Integer, Boolean и прочих.

Хотя это пытаются пофиксить. Как база используется новый вид классов (неожиданно, примитивный). Вы объявляете свой класс с ключевым словом primitive. Для таких классов появляются правила конвертации в примитивные типы, запрет на наличие значения Null и другие разные правила. Но сейчас не об этом. Пропозал (хз насколько такой термин в сообществе джавы применим).

Когда у вас есть примитивные классы, вы можете интерпретировать примитивные типы вроде int/double и другие как примитивные классы, что позволит реализовать дженерики и для них. Получается, что костылят :(
Хотели ввести в Java 19, но не успели, так что мб к Java 20 получится. Линк.

В Rust дженерики работают примерно как в плюсах: на один крейт (единица компиляции в Rust) тип в дженерик подставляется только один раз (грубо говоря, копирование кода для каждого типа в дженерике). Если дженерик из крейта 1 используется в крейте 2, то будет своя инстанцированная версия в крейте 2.

В go всё примерно так же за тем исключением, как работают интерфейсы относительно трейтов в rust. Тип на “подходимость” к интерфейсу проверяется автоматически, тогда как тот факт, что тип удовлетворяет трейту, надо указывать ручками.

UPD: тема go не раскрыта. Чуть позже вкину подробный пост про это в качестве исправления.
👍7🐳41
#common

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

Два основных подхода это использование offset и курсора.

Первый, это просто указание как много данных стоит пропустить от начала выдачи. После того, как вы получили первую 1000, можно запросить тот же запрос с offset=1000 и получить следующие 100 значений (чиселки условные конечно).
Оффсет может быть чуть более сложным, чем просто число (например номер страницы и номер последнего элемента на последней странице), но суть одна -- сдвиг от начала.
Проблема тут в том, что это может быть неэффективно. Например, если данные берутся из базы: вам необходимо сделать ваш [тяжёлый] запрос (мб с сортировкой и прочим) с чем-то вроде OFFSET 1000 LIMIT 100, читай отбросить первую тысячу результатов (== выкинуть работу в мусорку) и взять следующую нужную пачку. Чем больше данных и чем меньше размер одного батча, тем больше работы мы делаем зря. Ещё данные приходится грузить в память (что не так плохо, когда у вас какой-нибудь новостной сайт без персонализации, но для чего-то более сложного не катит). И не оч работает, когда есть вставки/удаления. Не круто.

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

[запрос: моло]

cursor: {
молоко: {
skip: 12,
min_relevant: 0.57,
},
молочное: {
skip: 27,
min_relevant: 0.13,
}
}


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

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

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

Links:
1. Five ways to paginate in Postgres, from basic to exotic.
2. How to Implement Cursor Pagination Like a Pro.
3. Paginating Real-Time Data with Cursor Based Pagination.

======================
Блин вас уже полтысячи. Неожиданно и приятно. Всем спасибо.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍20🔥5
#cpp

Лямбды 1/2.

1. Все мы помним, что список захвата работает только для локальных перенных. В то время как статические и глобальные переменные захватываются, грубо говоря, автоматически. Такое верно и для constexpr переменных, потому что в итоге у вас в коде будет подставлено конкретное значение. Логично и просто.
И тут стоит помнить, что константные интегральные типы неявно являются constexpr:

const int i = 1; // implicitly constexpr
const std::size_t j = 1ull; // implicitly constexpr
const float f = 1.f; // not implicitly constexpr


Т.е. первые две переменные мы можем не захватывать при использовании в лямбде, тогда как третью необходимо.

2. Immediately Invoked Function Expressions (IIFE).
Это довольно полезный подход использования лямбд, когда они вызываются сразу же:

[]{ std::cout << “ASD”; }();

Конечно в таком виде такое не используется. Гораздо более полезным такое будет, когда хочется инициализировать объект каким-то нетривиальным способом:

A a;
if (condition) {
a = firstWay();
} else {
a = secondWay();
}


Мы разделили инициализацию объекта на две части. Между ними он может быть невалидным, и использование до ифа может привести к уб. Как-то небезопасно и не оч поддерживаемо.
А ещё класс может не иметь конструктора по умолчанию, что в целом делает подход выше невозможным.
А ещё инициализируемая переменная может быть помечена const, что так же не даёт инициализировать её в ифе (в отличие от const в C++, в Java ключевое слово final, которое говорит, что переменной можно присвоить что-то только единожды -> код выше в случае final переменной заработает).

Если код достаточно простой, можно заюзать тернарный оператор:

const A a = condition ? firstWay() : secondWay();

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

const A a = [condition] {
if (condition) {
return firstWay();
} else {
return secondWay();
}
}();


Такой подход хорошо подходит для всех случаев, когда у вас действия происходят рядом с perfect forwarding:

std::vector<A> v;
v.emplace_back([condition] {
if (condition) {
return firstWay();
} else {
return secondWay();
}
}());


и другими функциями того же рода.

Есть поинт, что две скобки вызова лямбды в конце могут быть легко просмотрены (и можно подумать, что это просто лямбда, а не результат её выполнения), потому можно использовать std::invoke, но кмк тащить хедер ради такого не оч хорошо (только если он у вас уже был).

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

auto* fptr = +[](auto i) {
return i * i;
};


Что в целом понятно и ожидаемо.

4. Вот тут таска (и чуть ниже ответ) про то, как сообразить call_once и call_n с помощью лямбд.
Кстати прикольно, что на нескольких различных докладах на CppCon чуваки говорили, что на ручных бенчмарках лямбда, инициализирующая статический объект, — более эффективна как реализация std::call_once чем собственно реализации в версиях стандартной библиотеки.

5. С появлением generic лямбд с C++14 вы можете писать примерно все обычные для шаблонов штуки вроде auto&& для универсальных ссылок или даже auto… для variadic templates. Ну и лямбды друг в друга можно передавать. Удобно.

6. Variable template lambda.
С C++14 можно создавать шаблонные переменные. И, что интересно, если вы используете лямбду для инициализации такой переменной, вы имеете доступ к шаблонному аргументу переменной:

template <typename T>
constexpr auto c_cast = [](auto x) {
return (T)x;
};


Тут, грубо говоря, вы получаете не просто шаблонный operator() в вашей лямбде, но и сам класс лямбды становится шаблонным.
5👍10🔥31
#cpp

Лямбды 2/2.

7. Init capture optimisation.
Если у вас есть какой-то код вроде

std::find_if(v.begin(), v.end(),
[&pref](const auto& s) {
return s == pref + “bar”;
});


то вычисление pref + “bar” будет делаться на каждой итерации. Потому лучше вынести его в список захвата:

std::find_if(v.begin(), v.end(),
[str = pref + “bar”](const auto& s) {
return s == str;
});


что позволит немного сэкономить на вычислениях.

8. С C++17 лямбды могут быть constexpr.

auto f = []() constexpr {
return sizeof(void*);
};

std::array<int, f()> arr = {};


Аналогично с consteval с C++20.

9. Вот тут можно почитать, как написать удобный хелпер Overloaded для работы с std::variant и std::visit с помощью наследования от лямбд.

10. C С++20 лямбды умеют в конструирование по умолчанию, что помогает не передавать их в места вроде делитеров для умных указателей или как компараторы в конструкторы std::set/std::map и отдавать только тип в шаблон.

11. Забавный факт.

template <auto = []{}>
struct X {};

X x1;
X x2;

static_assert(!is_same_v<decltype(x1), decltype(x2)>);


Этот код успешно скомпилируется, т.к. при каждом обращении к типу, используется новое выражение []{}, которое имеет абсолютно новый тип относительно прошлых выражений []{}. Соответственно каждый раз шаблон X будет инстанцирован разными типами. И потому типы переменных не совпадают.

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

UPD.
Почему-то потерялся линк на оригинал, откуда взял часть инфы.
Доклад Тимура Думлера на CppCon 2022.
👍4🔥21👏1
#list

Микропост.

1. Женя написал крутой пост про способы ускорения компиляции. Докину ещё пост на хабре про hidden friends.

2. Тут собрали какое-то количество предложений по улучшению structure binding. Есть довольно приятные и интересные штуки, которые даже комитет рассматривал, но пока, как я увидел, ничего никуда не протащили (хотя обсуждения местами ведутся довольно давно). Можно посмотреть для расширения сознания.

3. Можете посмотреть очередной доклад Аксёнова про то, как не говнокодить (ничего конкретного вы не услышите; просто набросы).

4. Если приглядеться во множество примитивных типов в Java, можно увидеть отсутствие unsigned типов. Вроде как это потому что один из авторов языка считает, что мало какие разработчики на C способны рассказать, как работает арифметика с unsigned числами, поэтому такого в Java нет в принципе. Странный поинт честно говоря, но факт есть факт.

5. Какой-то прикольный симулятор тимлида. После работы прогером можно поиграть в менеджера. Хех.
👍10
#cpp

Чуть-чуть о сборке мусора на C/C++.

Почитайте тут небольшую статью на хабре про какую-то базовую имплементацию.
Если кратко, то:
- есть глобальное хранилище всех выделенных объектов;
- есть хранилище корневых объектов;
- с каждым объектом хранится какая-то метаинформация;
- есть жёсткие (которые влияют на сборку объекта) и слабые (которые нет) ссылки;
- есть какой-то способ проверки достижимости объектов. В Unreal Engine например надо пользоваться макросами UPROPERTY [1], с помощью которых сборщик мусора ходит по указаным в макросах указателям и тем самым получает ссылки на объекты, которые связаны с текущим. В случае статьи выше применяется хак, что все интересующие сборщик мусора объекты лежат в начале рассматриваемого объекта, что позволяет пройтись по ним, сделать reinterpret_cast и сделать что требуется (это не всегда работает, но тут чисто proof of concept).

Boehm-gc это прям классический сборщик мусора. Тут прям чуваки реализовали аналог malloc, после которого ничего не нужно удалять. Концептуально всё примерно так, как описано выше (лист блоков памяти, мета в хедере выделяемых объектов, mark-sweep), но сильно сложнее : ) Не будем останавливаться (код читать не советую: стандартное месиво директив препроцессора, C и других радостей жизни).

Sutter в своём проекте gcpp вводит такие объекты как deferred_heap, deferred_ptr и deferred_allocator.

deferred_heap -- регион памяти с объектами, на которые можно уметь указывать с помощью deferred_ptr. Вы можете создавать объекты с помощью метода make<T>() и уничтожать их на конкретной куче изолированно с помощью collect(). Концептуально вы можете завести такую кучу на множество объектов одного типа, на каждый объект свой или на любое подмножество объектов/типов. Как вам удобно.

deferred_ptr почти фулл такой же, как std::shared_ptr за тем исключением, что он умеет в отложенное удаление в зависимости от того, что же происходит с его кучей (ну и там немного по-другому реализован aliasing constructor). Куча кстати фиксируется единожды и поменять её указатель не может.

deferred_allocator это обёртка для своей кучи для использования в контейнерах.

По факту проект Sutter’а это подсчёт ссылок с удалением недоступных циклов. Однако когда “классические” сборщики мусора концептуально управляют сырой памятью, deferred_heap это скорее об управлении существующими объектами (в основном корректным вызовом деструкторов неиспользуемых объектов).
Код у него довольно понятный и читаемый. Можете почекать.
И оч много разной инфы в readme. Тоже можно посмотреть.

Тут можете почитать про проблемы (хех), которые могут возникнуть из-за наличия штук для сборщика мусора в стандарте C++11-20.

[1]. Сборка мусора в UE.

UPD.
Забыл про широко юзаемый сборщик на плюсах: olipan gc — сборщик мусора в chromium, который связан с V8.
🤯9👍5
#common

Про компиляцию в общих чертах.

Представим выражение (строка кода):

pos = init + rate * 60

Что с ним произойдёт при компиляции?

👉 Лексический анализ — читаем поток символов и группируем их в лексемы (значащие последовательности). Для каждой лексемы получаем токен вроде <имя, значение>.

<id, 1> <=> <id, 2> <+> <id, 3> <*> <60>

id в данном случае указывает, что информацию о токене нужно искать в глобальной таблице символов (там инфа о всех встреченных объектах). Номер 1/2/3 — индекс объекта в этой таблице.

<60> можно представить в более общем виде как <number, 4>, но для простоты не будем.

Такой набор токенов и отправляется на следующий этап.

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

=
/ \
<id, 1> +
/ \
<id, 2> *
/ \
<id, 3> 60


👉 Семантический анализ использует синтаксическое дерево и таблицу символов на семантическую согласованность с правилами языка. Тут ещё может собираться информация о типах сущностей, после чего она может сохраняться в дереве для последующего использования. Например, если язык позволяет неявные приведения типов, то он может дополнить дерево например так:

=
/ \
<id, 1> +
/ \
<id, 2> *
/ \
<id, 3> int_to_float
\
60


Или если у операндов некоторого оператора типы не совпадают, и язык запрещает неявные преобразования, уже на этом этапе можно кинуть ошибку.

👉 Генерация промежуточного кода.
В процессе трансляции исходного кода в целевой, компилятор может несколько раз генерировать различные промежуточные представления. В нашем примере можем получить что-то такое:

t1 = int_to_float(60)
t2 = id3 * t1
t3 = id2 + t2
id1 = t3


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

t1 = id3 * 60.0
id1 = id2 + t1


Стоит понимать, что оптимизация (хотя это скорее трансформация) -- всегда эвристический процесс, который ничего не гарантирует и пытается улучшить какой-то основной/несколько критериев, возможно жертвуя другими. Часто, например, можно хотеть уменьшить количество инструкций. Хотя, если у вас какая-то встраеваемая система, вы можете хотеть использовать меньше памяти. Но никто не гарантирует, что не станет хуже.

Ещё такой процесс может проходить несколько раз, т.к. какие-то сделанные оптимизации открывают возможность для новых.

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

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

Если верхнеуровнево, то компиляторы для плюсов состоят из несколько крупных этапов:
1. Frontend: препроцессинг, лексический, синтаксические и семантический анализ, построение high level intermediate representation (hir).
2. Middleend + backend: оптимизации hir, оптимизации mir, оптимизации lir, кодогенерация.

Frontend для каждого языка свой. Middleend и backend работаю с ir, что позволяет переиспользовать их для разных языков.

Послушать про то, как работают компиляторы для C++, можно в плейлисте. Правда речь о toolchain, но от этого только интереснее : )

Ещё можно посмотреть пару лекций про LLVM IR. И почитать пару постов на похожую тему в @cxx95: раз, два.

P.S. пример из dragonbook.
👍12🔥1
#cpp

1. Хочется укрепить понимание того, что же такое модификаторы доступа (public, protected, private), потому что в последнее время вижу тут-там некоторое непонимание у молодых особей, изучающиъ C++.
Это именно модификаторы доступа. Они всего лишь не дают напрямую обратиться к члену/методу класса. Но это не значит, что вы совсем не можете работать со скрываемыми объектами.

class A {
struct T {};
public:
T getT() { return T{}; }
};


// A::T t; // ce
A a;
// A::T t = a.getT(); // ce

auto t = a.getT(); // ok

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

Так же модификаторы доступа это концепция времени компиляции, потому, если не думать, можно ловить неприятные штуки:

struct A {
virtual void f() = 0;
};

struct B : A {
private:
void f() override { std::cout << 1; }
};


A* a = new B();
a->f();


Вроде как f() в B помечена private, но т.к. это требование проверяется на компиляции, а виртуальные штуки работают в рантайме, будет успешно выведена единица. Кстати, интересно, могут ли статанализаторы такое трекать как говнокод🤔 Не пробовал. Но может вы потрогаете и расскажете.

2. В C++11 появились типы фиксированного размера. И почему-то стало принято юзать их вместо стандартных int, long long, и прочей братвы. Мне не очень понятно почему. Ведь вам на самом деле редко нужны типы фиксированного размера. Часто вам хватает знания, что конкретный тип удовлетворяет каким-то ограничениям сверху/снизу. И часто можно просто поставить какой-нибудь static_assert на то, что у вас CHAR_BIT равен 8 или сколько вам там нужно. Короч кмк это специфический инструмент, непонятно почему ставший popular.

3. Можно почитать Женин довольно понятный пост про ABI и почему его не хотят ломать (там много ссылочек с доп инфой).
👍62🔥2🤔2
#cpp

RVO/NRVO 1/2.

Давно хотел рассказать про всякие оптимизации, которые делаются в плюсах. Тут будет пост про RVO/NRVO, а позже про что-нибудь ещё.
Может в начале будут какие-то неявные допущения или грубые формулировки, но к концу поста постараюсь донести все детали, чтобы картина сложилась.

Для начала про терминологию в стандарте. Формально есть общий термин copy elision, который в целом о том, чтобы не делать лишних копий объекта и создавать его сразу там, где нужно. Например ниже примеры copy elision:

T t = T(); // not T() + operator= but only T();
T t = T(T(T(T(T(T(T(1))))))); // not T(1) + many T(const T&) + operator= but only T(1)


Вот что говорит стандарт ([class.copy.elision]):

When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the constructor selected for the copy/move operation and/or the destructor for the object have side effects.

Т.е. компилятор в праве применять эту оптимизацию в обход copy-/move-кторов, даже если в них или деструкторе есть какие-то сайдэффекты.

Так же тут есть 4 пункта. В первом говорится, что тип инициализирующего выражения и тип переменной с итоговым результатом должны совпадать. Это главное условие для RVO/NRVO.

RVO/NRVO это частные случаи copy elision, которые происходят в результате возврата чего-либо из функции. Далее мы будем пользовать примерно таким классом, чтобы понять, что происходит с объектами:

#define TELL(str) { std::cout << str << std::endl; }
struct S {
S() TELL("def")
S(const S&) TELL("cp")
S(S&&) TELL("mv")
};


Так же есть такой термин как temporary materialization – принцип, согласно которому prvalue-выражение не создаётся физически в памяти до тех пор, пока оно не будет присвоено в не prvalue-объект.

Return Value Optimization по сути и есть пример temporary materialization. Посмотрим на базовый пример (godbolt):

S f() { return S(); }
S s = f();


Тут у нас prvalue-выражение S(), благодаря чему мы можем не создавать переменную в момент его появления в return и присвоить создать лишь в s. И из всех интересных нам конструкторов будет вызван лишь конструктор по умолчанию. cv-квалификаторы в сигнатуре функции в данном случае нам ничего не портят (gb).

А если мы можем (не)явно сконструировать возвращаемый тип из выражения в return? Добавим в S такой конструктор:

S(std::string&&) TELL("str&&")

и собственно пример (gb):

S f() { return std::string(10, '1'); }
S s = f();


Всё хорошо, даже не смотря на то, что тип в return и в сигнатуре не совпадают. По факту компилятор представляет выражение в return как S(std::string(10, ‘1’)), что тоже является prvalue. Даже в таком случае всё в порядке (gb):

S f() {
std::string s(10, '1');
return s;
}


Всё корректно. Т.к. компилятор справляется тут даже сделать неявный move переменной s. Причём это всё ещё RVO.

Стоит понимать, что с C++17 RVO — гарантированная штука и даже не оптимизация.
Давайте поймём, как оно работает. У компилятора есть адрес места в памяти, где должен оказаться результат выполнения функции (называется return slot). Когда выполняются условия для RVO, компилятор может сразу создать объект из вашего return в нужном месте в памяти. Т.е. это скорее про свойства инициализации объектов.

Named Return Value Optimization уже оптимизация (причём не гарантированная, тут компиляторы делают кто что может). Базовый пример (gb):

S f() { S s; return s; }

Или пример посложнее, где в S появился int x (gb):

S f() { S s; s.x = 1; return s; }

void gg(S& s) { s.x = 1; }
S g() {
S s; gg(s); return s;
}


Компилятор всё ещё может создавать локальную переменную s в функциях сразу в return slot итогового объекта.
Для NRVO правда volatile для локальной переменной всё портит (gb).

Почему важно знать об этих штуках? Потому что NRVO можно сломать (gb):

S f() {
S s;
return std::move(s); // return S&&
}


Теперь компилятора обязан создать объект и мувнуть его, потому что это то, о чём вы его попросили явно. Так же можно думать, что у выражения в return и в сигнатуре теперь разные типы (S&& и S).
7👍4
#cpp

RVO/NRVO 2/2.

Как понять, убиваете ли вы NRVO? Можно сделать такой же класс, который выводит операции со своими конструкторами. В случае некоторых компиляторов (clang) можно ещё одним способом, о котором рассказали в комментариях в посте про этапы компиляции. На этапе семантического разбора компилятор производит всякие проверки на корректность типов в выражениях и подобном. Например у clang к основному условия про совпадение типов для NRVO ещё проверяется, правда ли, что во всех return возвращается одна и та же локальная переменная. Посмотрим на пример (gb):

void func1(std::optional<std::string>&);
std::string get_s();
void func2(std::string&);

std::optional<std::string> f(bool flag) {
std::optional<std::string> null;
func1(null);
if (flag) return null;
// (1)
std::string s = get_s();
func2(s);
return s; // (2)
}


Во-первых, корректно ли мы в (2) возвращаем s? Ответ нет. Лучше мувнуть. Потому что в данном случае будет вызван конструктор std::optional<std::string>(const std::string&) для RVO. Если же мувать, получим std::optional<std::string>(std::string&&). Хотя с C++20 мувать уже необязательно.

Во-вторых, случится ли NRVO в (1)? В данном случае нет, т.к. эта локальная переменная не возвращается во всех return. Однако если начать возвращать её и во втором return, то в AST можно будет увидеть строчку:

`-VarDecl <col:5, col:32> col:32 used null 'std::optional<std::string>':'std::optional<std::basic_string<char>>' nrvo callinit destroyed

С пометкой nrvo. Получается, можно сэмулировать структуру вашего кода и посмотреть на AST. Оч прикона кмк.

Если подумать, почему же одна и та же локальная переменная во всех return важна, то всё становится понятно: компилятор уверен, что эту переменную в памяти можно создавать сразу в return slot результата функции. В обратном случае это неверно, так что переменная null копируется.

Круто🤭

Если вы об этом паритесь, общие рекомендации такие:
1. Пытаться возвращать prvalue.
2. Возвращать одну и ту же локальную переменную во всех return.

Аналогичные правила примерно применимы и к исключениям, о которых два других пункта в стандарте. Но, честно говоря, если вы пытаетесь оптимизировать и так не очень производительные исключения, что-то пошло не так : )

Тут можно посмотреть хороший небольшой доклад про то, как это всё работает (с чуть бОльшим количеством технических деталей) и с примерами, когда это не работает.
Ещё можно посмотреть вот это бомбическое видео.
Please open Telegram to view this post
VIEW IN TELEGRAM
8🔥4🤯11
В этом году я начал пытаться писать сам время от времени вместо тупого вкидывания ссылочек на разные материалы, просто потому что так полезнее и мне, и вам.

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

Посты:
- необычные возможности C/C++;
- o chaos engineering;
- об аллокаторах в других языках;
- macros tricks;
- о векторах;
- о хеш-таблицах;
- о вероятностных структурах данных;
- о кешах;
- про рефакторинг;
- backend for frontend;
- про ub;
- впечатления от C++ Zero Cost Conf 2022;
- деревья в алгоритмах;
- про полезности из личного опыта;
- немного из метапрограммирования;
- пару кейсов, связанных с грамматикой языка;
- про эпохи;
- про cpp2;
- статический лист для сборки динамических аллокаций;
- чуть-чуть про софтскиллы;
- о подходах к реализации дженериков (помню, что торчу пост про go);
- про пагинацию;
- про лямбды;
- про компиляцию в общих чертах;
- RVO/NRVO;
- пачки пропозалов: раз, два;
- пачки рандомных фактов: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14;

О сборщиках мусора:
- основные алгоритмы;
- G1GC в java;
- другие сборщики в java;
- python gc;
- сборщики в других языках;
- о сборке мусора и C/C++.

Статьи:
- об устройстве популярных аллокаторов в C++;
- об интересных структурах данных;
- обработка ошибок и C++.

Задачки:
- про использование variadic templates;
- хз про что это но вот.

Примерно понимаю, о чём хочу написать ещё минимум одну статью и около миллиарда постов, так что контент в следующем году будет. Другой вопрос, что нагрузки становится всё больше, потому, вероятно, писать буду реже. Или чаще. Хз короч.

Рандомные факты про жизнь.

Закончил универ, прокачался на работе и в целом доволен тем, чем сейчас занимаюсь. Немного участвую в образовании тут-там. Хотя может и много. По крайней мере универа в следующем семестре будет явно больше, чем в некоторые семестры в студенческое время. Оч интересно и оч сложно.

Завёл @dzikart и пытаюсь поддержать жизнь в @memesfromhole.

Набил ещё три татухи и научился жонглировать (кек).

Не умер, что особенно приятно.

Временно на каникулы. Не болейте.
34👍9🔥7
#common

Ещё немного полезностей из личного опыта.

Ещё когда в начале второго курса в каком-то рандомном курсе по python я столкнулся с регулярками, подумал, что это оч крутой инструмент, который, тем не менее, вряд ли часто применяется (потому что так с большинством знаний, к сожалению). Как же я был глуп. Сейчас я пользуюсь ими чуть ли не каждый день, пусть часто и хватает какого-то минимального базового набора. Ниже небольшой список примеров, где они пригодились:
- я не оч люблю codesearch в ide, потому что сижу в clion, который, как известно не самый быстрый (в том числе потому что у него проблемы с индексацией мало-мальски больших проектов). У меня это скорее прокачаный блокнот, по которому я делаю только Ctrl+F в рамках файла. Но когда надо поискать что-то в более глобальной окрестности, я пользуюсь внутренним сервисом для поиска по монорепе. И он поддерживает регулярки (как для строк, так и имён файлов), что вообще-то довольно удобно.
Кажется, недавно github обновил у себя поиск и можно делать что-то похожее.
- во внутреннем userver, в отличие от опенсорсного, есть кодогенерация (статья на хабре, где про это можно почитать), которая позволяет не писать бойлерплейт для раскладывания полей в структурки и обратно. Ты просто задаёшь схему в формате yaml, по которой валидируется реквест/респонс. И там есть возможность для строковых переменных задавать паттерны регулярками, по которым так же будет проводиться валидация. Если что-то не прошло, клиент получает 4хх автоматически.
- иногда в поиске бывают примеры не самой релевантной выдачи, которые обусловлены особенностями реализации. И иногда их проще закрыть чем-то вроде блеклиста, в котором можно поюзать регулярки для обобщения каких-то кейсов и неразрастания блеклиста.

Недавно обходил деревья dfs’ом, что вызывает какие-то детские эмоции радости, потому что после десятков (и может даже сотен) раз, когда он писался для спшных тасок, это знание перестало быть бесполезным с точки зрения применения в проде.
Рядом у нас ещё есть много разных bfs’ов для разных кейсов. А когда-то видел bfs для специфического обхода какой-то графической сетки.
Короче алгоритмы на собесах даже как-то обоснованы (даже k раз приходилось делать что-то с двумя указателями).

В статье про обработку ошибок я упоминал, что не нужно ловить всё подряд (catch (…) {} или ловить std::exception). Опровержений первому я пока не увидел, но ловить std::exception оказалось полезно. Кейс примерно такой: мы умеем явно ловить только ответы ручек от других сервисов, которые описаны в api, но это не всё, что сервис может ответить, т.к. иногда есть неописанные 5xx или технические 429/другие 4хх. И хочется, чтобы неуспешный запрос не влиял на доступность сервиса, который этот запрос и делает. Но мы согласны деградировать некоторую функциональность. Тут как раз хорошим вариантом будет ловить std::exception.

============================
Сейчас верхнеуровнево пытаемся намутить курсов на новый семестр, примерно с нуля. Занимает оч много времени (из-за чего я тут притих). Но есть ощущение, что получится даже не очень плохо. Может попозже чем-нибудь в этом месте поделюсь.
👍97👌2😐1
#cpp

Посмотрел доклад Andrei Alexandrescu на CppCon 2021. Накидаю приконых фактов оттуда:
- предлагает засунуть в стандарт

template <typename… Ts> struct type_sequence {};

потому что если этого не сделать, каждый будет писать это сам (и вероятно с помощью tuple/variant, что концептуально не всегда то, что нужно). В таком виде со списками типов предлагали работать и на CppRussia 2019 с примерами других функций, которые могут понадобиться при работе со списками типов, что выглядит вообще-то довольно приятно и в ногу со временем. И рядом Alexandrescu предлагает три базовые функции в классической манере для работы с металистом типов: head, tail и cons (создать лист из элемента и другого листа). Всё как в Lisp.

- и депрекейтнуть integer_sequence, потому что это не последовательность целых чисел, а последовательность значений какого-то типа. Вместо него ввести value_sequence, потому что это семантически честнее.

Из интересного ещё можно отметить такое. Предположим есть функция

template <typename T, typename… Ts, typename U>
int f(T, U, const Ts&…);


и её вызовы

f(1, 2); // Ts=<>
f<int, int>(1, 2, 3); // Ts=<int>
f<int, int, double>(1, 2, 3, 4); // Ts=<int, double>


Тип U всегда сможет вывестись! Причём из параметра, а не указанного списка типов в шаблоне. В первом случае это int по понятным причинам. Во втором это тоже int. В третьем же Ts выводится как <int, double>, а U — исходя из параметра. Причина в том, что если variadic pack начал матчится, он не остановится и идёт до конца (жадный матчинг). Получается, в таком кейсе у нас нет возможности указать тип U компилятору явно, ведь он всегда будет выводится из параметра.

Соответственно, если у вас есть что-то такое

template <typename... Ts, typename T = int>
T f(Ts…);


тип T может быть только int, потому что в него никогда ничего не сматчится.

Соответственно, если у вас пак типов функции находится не в конце, то он всегда должен быть указан явно:

template <typename... Ts, typename T>
int f(Ts... values, T value);


f(1); // Ts=<>
f<int, double>(1, 2, 3); // Ts=<int, double>
f<int, int>(1, 2); // error, потому что не хватает одного аргумента


Всё это приводит к забавным кейсам вроде такого

template <typename... Ts, typename... Us>
int f(Ts... ts, Us... us);


Тут Us всегда выводится из параметром (deduced), тогда как Ts должно быть указано явно. Можете попробовать понять для себя, чему они будут равны в следующих случаях:

f(1);
f(1, “2”);
f<int, char>(1, ‘2’);
f<int, char>(1, '2', “three”);


Или может быть такой угар:

template <typename... Ts, typename... Us, typename T>
int f(Ts..., Us..., T);


При любом вызове Us будет пустым паком. Что бы вы ни делали. Все компиляторы это кушают, но стандарт говорит, что если один пак всегда пустой, то это no diagnostic required (грубо говоря, нельзя так).

Ещё есть такие штуки как std::tr2::bases и std::tr2::direct_bases, которые возвращают тайплист всех базовых классов указанного и все непосредственные базовые классы указанного соответственно.

Хотя вообще доклад не об этом : ) Так что посмотрите.
👍9
#common #highload #cpp

Пару дней назад вышли записи прошломайского Highload++ Foundation 2022. Накидаю докладов из того, что уже посмотрел.

1. Как выжать 1 000 000 RPS. Андрей Аксёнов (link).
Классический доклад в своей нестандартной манере про бенчмарк Sphinx.

2. Поиск GPS-аномалий среди сотен тысяч водителей. Александр Царьков (link).

3. Как мы ускорили Яндекс Go на несколько секунд. Денис Исаев (link).
Я немного упоминал этот доклад тут в п.4. Просто и прикона.

4. Экскурсия в бэкенд Интернета вещей. Владимир Плизга (link).
Прикона было узнать, какие задачи в целом решают с помощью IoT. Правда я так до конца и не понял, что это такое.

5. Авито: root cause detector. Юрий Дзюбан (link).
Интересная попытка автоматизировать расследование проблем.

6. Избавляемся от кэш-промахов в коде для x86-64. Евгений Буевич (link).
Я не поклонник вот этих низкоуровневых штук, но может вам будет интересно. И заодно тогда можно глянуть LLVM Optimization Remarks by Ofek Shilon с осеннего CppCon 2022 (link).

7. Как наладить CI-процесс в монорепозитории. Алина Власова (link).

8. Настраиваем инцидент-менеджмент: от хаоса до автоматизации. Сергей Бухаров (link).
Тут скорее было интересно посмотреть, что у других. Имхо, у нас лучше.
Основные вопросы к системе это: насколько нужен скрайбер; почему оценкой импакта на бизнес занимаются разработчики в процессе инцидента; и зачем есть secondary дежурный, почему их только двое (понятно, что дальше это кому-то эскалируется, но почему не третьему, а SRE?)?

Пока местами прикона.
👍6
#highload #cpp #go

Досмотрел всё интересующее из майского highload, так что докидываю к прошлому посту (и пару докладов с плюсовых конф).

1. Как мы готовили поиск в Delivery Club. Иван Максимов (link).
Тут интересно, потому что я много работаю над штуками вокруг нашего поиска (не непосредственно качеством, а именно около). Когда ты чуть-чуть в теме, смотреть интереснее, что там чуваки намутили.

2. Хайлоад, которого не ждали. Дмитрий Самиров (link).
Тут аналогично, потому что чуваки намутили какую-то доставку продуктов из Я Еды. Почти Лавка. Только у нас круче.

3. Как мы устроили переезд 10+млн строк С++ кода на новый стандарт. Никита Старичков (link).

4. Статический анализ кода++. Анастасия Казакова (link).

5. Компилируем 200 000 файлов быстрее, чем distcc. Александр Кирсанов (link).
Это интересно с точки зрения, что в ВК намутили миллион каких-то своих способов компилироваться из PHP в C++ эффективнее. И по чуть-чуть чуваки эту свою инфру развивают. Прикольно посмотреть, что нового они там придумывают.

6. Почему всё сложно с оптимизацией и что с этим делать. Константин Шаренков (link).
В целом доклад с вопросами, но мб вам зайдёт. Мне не то чтобы прям, но выше среднего.

7. Go Map Internals. Егор Гришечко (link).

И плюсовые из закромов:

8. C++14 Reflections Without Macros, Markup nor External Tooling. Antony Polukhin (link).
Блин, оч крутой доклад с CppCon 16ого года, который я как-то пропустил. Антон рассказывает про magic_get/boost::pfr и всякие мозговзрывающие штуки, которые там использовались.

9. Back to Basics: C++ API Design. Jason Turner (link).

10. Метаклассы в C++17: фантастика? Реальность! Сергей Садовников (link).

Мб вам что-то на последних конфах понравилось? Можно поделиться в комментариях.
👍132