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

Tags: #common, #cpp, #highload и другие можно найти поиском.
Задачки: #poll.
Мои публикации: #pub.
Автор и предложка: @vanyakhodor.
GitHub: dasfex.
Download Telegram
#cpp

Атрибуты в C++ 1/2.

Начнём со стандартных.

С точки зрения синтаксиса поддерживается два основных варианта:

[[attribute_name]] // C++11
[[using attribute-namespace: attribute_list]] // C++17


Из примеров с cppreference ещё можно увидеть такое:
- атрибуты с аргументами: [[deprecated(“text”)]];
- с неймспейсом: [[gnu::unused]].

Атрибуты можно применять к чему угодно: переменным, типам, функциям, классам, блокам кода и целым translation units. Но конечно это должно согласовываться с его реализацией.

[[noreturn]] говорит, что функция не возвращает control flow кода. Например она вызывает std::terminate, бросает исключение, содержит бесконечный цикл и т.д. В C можно использовать _Noreturn или noreturn для аналогичного.

[[carries_dependency]] может использоваться в случаях, когда у вас memory order consume. Я вообще в этих ваших многопоточках не оч шарю, но говорят, что введение такого понятия не самое удачное решение. Мб подскажете адекватные кейсы использования?

[[deprecated]], [[deprecated(“message”)]] говорит, что какой-то объект является deprecated (обычно компиляторы выдают ворнинги при использовании помеченых штук).

[[fallthrough]] говорит, что провал в следующий case в switch является намеренным -> компилятор не будет подсказывать ворнингами об этом.

[[nodiscard]], [[nodiscard(“message”)]] применяются к функции и означают, что не стоит игнорировать её возвращаемое значение. Если да, получите ворнинг.

[[maybe_unused]] говорит, что используемый объект может быть просто объявлен, но нигде в коде не заюзан. Если атрибут есть, вы не получите ворнинга. Удобно иногда, когда пишешь код частями и хочешь проверять, компилится ли, наставить их всему, что мешает. Потом с продвижением по решению таски поубирать. Или когда у вас какой-то общий интерфейс, но не все аргументы нужны для новой реализации.

[[likely]], [[unlikely]] говорят, что некоторый code flow выполняется чаще/реже. Соответственно компилятор может использовать эти знания и оптимизировать hot path. Или наоборот. Мб вы знаете кейсы, когда это прям помогает? Имхо компиляторы сейчас неплохо с такими вещами и так справляются.

[[no_unique_address]] говорит, что член класса может не обладать уникальным адресом в памяти, из-за чего несколько членов класса потенциально могут пересекаться в памяти. Подробнее расскажу про него вместе с empty base class optimization чуть позже.
В msvc игнорится (даже с С++20), потому надо юзать [[msvc::no_unique_address]].

[[assume]] говорит, что некоторое выражение, которое вы в него засунете, будет true c момента указания атрибута. Это может помочь компилятору провести какие-то оптимизации (опять не уверен, что это сильно поможет):

int x = f();
[[assume(x > 0)]];
// далее это утверждение может использоваться для более эффективных оптимизаций


int z = x;
[[assume((h(), x == z))]]; // после вызова h() переменные всё ещё будут равны


[[assume((g(z), true))]]; // g(z) вернёт true

Ещё примерчики можно посмотреть на cppref.

using в атрибутах это про сокращение записи. Например это может быть полезно для нестандартных атрибутов. Вместо

[[gnu::always_inline]] [[gnu::hot]] [[gnu::const]]

можно писать

[[using gnu: always_inline, hot, const]]

Тут и using, и сразу несколько атрибутов в одном attribute-specifier (это [[]]).
👍14
#cpp

Атрибуты в C++ 2/2.

Кроме стандартных атрибутов есть ещё всякие компилятороспецифичные.

Тут стоит сказать, что хотя конечно в целом компиляторы в основном умеют работать с атрибутами со стандартным attribute-specifier ([[]]), в зависимости от компилятора часто используются формы вроде __attribute__(()), __declspec() или, реже, прагм.

По самим атрибутам.

Если вы уверены, что что-то нужно сто процентов заинлайнить, в gcc можно использовать __attribute__((always_inline)). Он игнорирует все лимиты, по которым решается, инлайнить ли функцию/метод, и игнорирует -fno-inline. Есть вот небольшая преза с бенчмарками по этому атрибуту.

Есть и довольно специфические атрибуты. Например в clang атрибут [[clang::preferred_name(some_name)]] может быть применён к шаблону класса и указывает предпочтительный способ присвоения имени специализации шаблона.

Или например using_if_exists, который позволяет не ругаться на юзинги, если в них используются несуществующие/необъявленные сущности. Таким образом ошибки откладываются до момента использования:

namespace empty_namespace {};
__attribute__((using_if_exists))
using empty_namespace::does_not_exist; // no error!

does_not_exist x; // error: use of unresolved 'using_if_exists'


Или например noescape к параметру-указателю говорит, что этот указатель никуда не “убежит”. Т.е. вы его ни во что, что может использоваться дальше, не присвоите (вызвать free можно):

int* gp;
void no(__attribute__((noescape)) int* p) {
*p += 100; // OK.
}

void yes(__attribute__((noescape)) int* p) {
gp = p; // Not OK.
}


Из полезного ещё есть __attribute__((weak)) в clang (__attribute_((weak_import)) в gcc). Weak линковка это когда вы можете пометить некоторую реализацию функции подобным атрибутом, но если будет найдена другая без подобного атрибута, ей отдаётся предпочтение. Типичный пример — перегрузка operator new/operator delete.
В msvc подобного результата (насколько я знаю) можно достичь с прагмами. Рядышком есть ещё [[selectany]], которые разрешает компилятору выбрать любую реализацию.

Из прекрасного про msvc факт, что [[no_unique_address]], как уже говорилось, на нём не работает. Нужно юзать специфичный компиляторный. Но это только если у вас msvc. Если хотите под windows собирать с clang’ом, земля вам пухом.

И много других: enable_if (который делает примерно то, что вы и думаете), format (который говорит, что функция принимает printf/scanf-like format строку и аргументы для неё), malloc (говорит, что функция ведёт как функция для выделения памяти из системы, no_sanitize (отключающий проверки санитайзеров для конкретной функции или глобальной переменной), constructor (позволяет выполнить некоторый код до запуска main(), подробнее Женя писал в https://news.1rj.ru/str/cxx95/109) и destructor (после выхода из main()), call_once (утверждающий, что параметр-функтор вызывается ровно однажды).

Больше атрибутов можно увидеть тут: clang, gcc.
👍10
#cpp

Пачка фактов.

0. Оч кайфовый доклад про приемлемый способ починить большинство текущих проблем в C++: link.
Так сказать, never gonna give you up.

1. Вместо того, чтобы писать operator==, гораздо лучше отдать пользователю это решение. Пусть при каждом сравнении он сам скажет, равны ли объекты!

https://godbolt.org/z/9KT15jPor

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

Но лучше всегда возвращать true, потому что все равны.

2. Подруга мамы назвала своего сына std::victor.

3. Вы же знали про оператор стремления к нулю?

while (x —-> 0);

4. А про оператор головастик?
https://habr.com/ru/post/258811/

5. В Польше запретили std::abort.

6. Если вам не нравится std::vector<bool>, можете использовать std::vector<std::optional<std::monostate>>. Или даже лучше std::vector<std::variant<std::true_type, std::false_type>>.
😁30🔥7🤯6🤣5🐳2🌚2👍1
#cpp

Два факта про неочевидные (и к счастью редкие) кеки C++.

Первое про union.

Важно помнить, что у union в C++ могут быть конструкторы/деструкторы, потому что важно уметь выбирать кого вы конструируете по умолчанию, и соответственно уметь вызывать корректный деструктор в зависимости от активного члена. И вот если у вас объектам надо вызывать конструктор, и вы union’у его не реализовываете, то код просто не компилируется, потому что дефолтный по умолчанию удалён. Но это лирическое отступление. Имеем такой union:

union U {
std::string s_;
std::vector<int> v_;
U(std::string s) { new (&s_) std::string(s); }
U(std::vector<int> v) { new (&v_) std::vector<int>(v); }
~U() {}
};


И собственно сам кек. Т.к. по дефолту деструктор удалён, то в данном случае = default для него означает, что деструктора нет. Т.е. поведение при пустом кастомном деструкторе и при ~U() = default отличается.
Вы спросите, а почему в деструкторе ничего не делаем? Я отвечу: потому что этим должен заниматься пользователь union снаружи. Он знает, какой член является активным. Он пусть с этим и разбирается.
Ну и чтобы соблюсти формальности, уточним, что вызывать деструктор std::string не как

s_.~string()

а как

s_.~basic_string<char>()

потому что это же алиас.

Второе про function-try-block.

В C++ существует альтернативный синтаксис для определения тела функции, позволяющий навесить на него целиком перехват и обработку исключений:

// Стандартный способ
void f() {
try {
may_throw();
} catch (...) {
handle_error();
}
}

// Альтернативный синтаксис
void f() try {
may_throw();
} catch (...) {
handle_error();
}


Эта фича позволяет нам ловить исключения из списка инициализации в конструкторах:

struct S {
A a_;
B b_;

S(A a, B b) try : a_(a), b_(b) {
} catch (…) {
// handle
}
};


Правда тут есть один опасный момент. После обработки ошибки в catch ошибка будет неявно проброшена выше. Это в целом логично: если при инициализации полей класса вылетело исключение, мы никак не можем исправить ситуацию и починить объект.

А что с деструкторами? Из них же крайне нежелательно бросать исключения, потому словить всё было бы удобно. Но тут пранк — поведение такое же, как и в конструкторах. Т.е. catch из function-try-block неявно пробрасывает исключение выше. Вы только что нарушили неявный noexcept(true). Но в отличие от конструкторов, для деструкторов есть мегакостыль: просто добавьте return!

struct A {
~A() try {
throw std::runtime_error("err");
} catch (…) {
std::cout << “ERROR”;
return; // исключение не будет перевыброшено!
}
};


Таким образом словили кейс, когда return в конце void-функции меняет её поведение.

На всякий напомню, что ловить исключения через catch (…) не очень хорошо. Вообще можно почитать мою статью про обработку ошибок.

Ну а плюсы что. Кринж.
👍13😱5😁31
#common

Про ревью.

Что вы думаете, когда видите что-то такое:

> Это какой-то бред.

Оценочное суждение. Никакой конкретики, что же сделано не так. Никаких предложений по исправлению.

> Так лучше не делать.

Как??????????

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

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

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

Будьте добрыми😎
Please open Telegram to view this post
VIEW IN TELEGRAM
👍46🥰10💔1
#cpp

Ух. Влетел с рассказом про обработку ошибок в плюсах на яндексовый Intern Meetup Week. В целом понятны точки роста, где над чем можно поработать, но опыт очень прикольный и полезный. Запись можно глянуть тут: https://www.youtube.com/live/5stJKC6UGyI?feature=share&t=532

Текстовая версия: https://habr.com/ru/articles/690038/
🔥39
#highload #cpp

Подсобирал ссылочек на разные приконые статьи и доклады.

1. Про то, как discord хранит сообщения. TLDR: история про смену БД.

2. Про переезд с монолита на микросервисы в Twitch: part one, part two.

Появились записи прошлогоднего C++ Russia. Пока правда не на youtube.

3. (Core) библиотеки: идеи и примеры про то, как писать общие библиотеки и различные подходы на двух примерах.

4. Reflection TS: будущее рефлексии в C++.

5. Маленький, но довольно глубокий Type Sanitizer: способ обнаружения нарушений правил strict aliasing в C++.
12👍4🤯2
#highload

Вышли записи Saint Highload++ 2022. Вот пару ссылочек из интересного. Сначала технические:

0. Какие архитектурные решения в Яндекс Go позволяют запускать десятки продуктовых экспериментов. Олег Ермаков.

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

1. Векторный поиск в ClickHouse. Артур Филатенков.

2. Про историю и будущее поиска. Андрей Аксёнов.

3. Архитектура: история и будущее на примере ВКонтакте. Александр Тоболь.

Ну ВКонтакте со своими костылями это угар какой-то местами. Но доклад приконый и оч живой.

И не очень:

4. Под красным флагом: как инженер может понять, что в проекте происходит что-то не то. Даниил Подольский.

5. Как понять, что проекту плохо, если ты инженер. Юлия Белозёрова.

Тянка, кстати, жоская. Под конец доклада понял, что читал k её статей в vas3k.club. И все кайфовые.
🔥82👍1👎1
#common

Есть желание немножко поразбираться и пописать про поиск и связанные штуки, так что время от времени что-то с этим связаное будет возникать.

Сегодня про задачи и специфику поиска в различных контекстах.

Давайте представим, как может выглядеть простейший поиск. Это примерно просто нахождение подстроки в строке (или во множестве строк). И это вполне себе полезная задача. Например, вы сидите в своей любимой ide и грепаете что-то по файлику. Тут обычно не нужно мутить чего-то чуть более сложного вроде исправления опечаток, семантики запроса и т.д. Нужно просто найти, что просят (может, проигнорировав кейс). Чтобы не делать это втупую, можно заюзать Бойера-Мура или Кнута-Морриса-Пратта. Даже можно какое-нибудь суффиксное дерево по текущему файлику построить, но имхо звучит как оверинжиниринг. И тут это почти наверняка хорошо работает, потому что файлы у вас обычно небольшие (думаю, даже несколько тысяч строк подобным спокойно закрываются). Другой вопрос конечно, как сделать подобное быстро, если вы ищете по целому проекту или по огромной монорепе, но про такой codesearch как-нибудь потом.

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

Обычно пользователей интересует поиск по естественным текстам: сайтам с их страничками, статьям, форумам и прочему. Причём поиск тут может вестись разными способами: по коротким фразам целиком, по каким-то длинным фрагментам, по различным ключевым словам (которые надо уметь выделять и оценивать их ценность в запросе), иногда даже по семантике (тут уже надо какой-то ml наворачивать). Учитывая, что хочется, чтобы ваш сайтик искался и индексировался лучше, рядом возникает задача search engine optimization (SEO).

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

Тут получилось просто, абстрактно и общо. Дальше будет чуть больше конкретики.
👍8🤔84🐳2🗿2💔1
#cpp #highload

1. Ещё один доклад Daisy Holman про всякие необычные C++ tricks. Можно полистать презу.
Ранее упоминал её тут и тут.

2. Статья про устройство real-time messaging в Slack.

3. Статья про Tinder API Gateway.

4. Migrating Critical Traffic At Scale with No Downtime в Netflix.

5. Monoliths are not dinosaurs.
👍7❤‍🔥4
#common

Про то, что в поисковых движках (для полнотекстового поиска) обычно есть.

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

Базой почти всегда служит инвертированный индекс — множество id документов для каждого ключевого слова, в которых это самое слово встречается. Выглядит это примерно вот так:

using Index = Map<String, Vector<int>>;

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

Собственно если у вас именно вектор интов в значении мапки, вы можете его очень сильно пожать. Ловите профит, правда инфы из такого индекса вы получаете мало. Хотя если научиться каким-то образом ужимать весь ваш Posting в одно чиселко, можно тут и пооптимизировать.

Тут правда миллион вопросов возникает: как должен быть устроен Map? а Vector<Posting>? Эти структуры могут быть примерно чем угодно в зависимости от вашей прикладной области и требований.

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

11, 15, 16, 21, 37, …
11, 4, 1, 5, 16, …

Если же у нас и отрицательные, применяют что-то около zig-zag кодирования.
Но вообще как числа жать, уже давно придумали: elias gamma, golomb, rice, huffman и другое. С ними правда тоже вопросики есть, но можно крутиться.
Можно ещё посмотреть в сторону varint (или group varint/pfor/simple9/simple16).
Но в любом случае надо что-то тут делать, потому что сжимать оч важно.

Важной частью ещё является ранжирование. Учитывая развесистые модели, которые для решения этой задачи используются, эта часть может занимать огромное количество времени. И сверху ещё (т.к. обычно там какой-никакой мль), результаты могут быть с вопросом. Тут рядом считают какие-нибудь статистики вроде TF/BM25, которые в поисковых движках уже стали классикой. Иногда ещё движки, если они разрабываются как SaaS решение, могут предоставлять возможность создавать свои факторы для ранжирования и как-то их крутить. Очень полезно в рамках различных предметных областей, т.к. непонятно, где ваша разработка будет использоваться. Хотя конечно почти наверняка делают более специализированные штуки (например в силу специфики работы, видел движки для екома, но скорее внутри, чем снаружи).

Обычно в движке у вас есть ещё какой-то матчинг (чтобы какие-то результаты по запросу получать), опечаточник, саджест, иногда какие-то атрибутивные real-time обновления, чтобы индекс каждый раз заново не варить (иногда умеют делать фулл индексы real-time, но не совсем, а иногда умеют варить только дельту). Короч очень много всего.

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

Постик основан на докладе, но ссылку на него я вам пока не дам, потому что он в закрытом доступе. Как появится запись, обязательно поделюсь.
👍72
#common

Узнал про такую штуку как hashcash. Он используется в различных криптоштуках в качестве proof-of-work. Но сейчас мы не будем останавливаться на битке и подобных вещах. Хочется рассказать о том, как такое можно использовать в житейских простых задачах вроде пагинации (писал немного про неё тут).

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

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

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

Собственно примерно поэтому и называется hashcash: вы кешируете множество объектов с помощью кеширования каждого из них.

Ещё из проблем тут есть:
- т.к. хеширование двух различных объектов может давать один хеш, вы можете отсеять что-то, что на самом деле не показывали, но, насколько я могу понимать, это обычно не крит, потому что с таким же успехом товар мог отфильтроваться на каком-нибудь другом этапе рекомендаций (отбор кандидатов, после ранжирования оказаться слишком низко, реранкинг). Если такое возникает часто, опять же можно посмотреть в сторону других хеш-функций, которые дают более разнообразные результаты;
- т.к. с каждым запросом кол-во показанных сущностей растёт, сам курсор тоже будет расти. Тут можно выбрать какое-то ограничение для длины курсора с трейдоффом кол-во показанного юзеру контента <-> технические возможности вашей системы. Если вы понимаете, что из всех пользователей долистывает до 300ого айтема только полпроцента, может не так страшно не показывать больше 300 объектов? Ну и оч длинную строку парсить на бекенде не хочется, т.к. это потенциально таймауты (а ещё та же проблема на клиенте, а ещё нагрузка на сеть вырастает). Можно опять же выбрать хеш-функцию так, чтобы она давала более короткие хеши, но тут важно не переборщить, чтобы не получить прошлую проблему в большом объёме.

Концептуально про рекомендательные системы расскажу попозже.
👍141🤔1🤯1
#common

Рекомендательные системы 1/2.

Опустим часть с мотивацией нужности подобных штук.

Пусть у нас есть множество пользователей и айтемов, которые мы рекомендуем. И пусть у нас есть история оценок по каждому пользователю, как он оценивал те или иные айтемы. Задача — научиться предсказывать оценку пользователя по какому-то ранее неоценённому айтему.
У вас может не быть явной системы оценивания, но могут быть другие сигналы — клики на айтемы, покупки айтемов, их просмотр/прослушивание, если вы какой-то видео-/аудиохостинг. Потому фидбек от пользователя делится на explicit (когда он явно сообщает, что ему что-то (не)нравится) и implicit (когда непонятно, нравится ли пользователю что-то, но можно выдвигать гипотезы, что действие пользователя означает).

Часто в таких штуках применяется коллаборативная фильтрация — методы в рексистемах, основанные на похожести айтемов и взаимодействии пользователя с айтемами.
Из терминов рядом ещё есть User2User (user based) и Item2Item рекомендации. Думаю, тут смысл понятен.
Есть ещё контентный подход — когда рекомендуют айтемы по схожести контента в них.
Ну и гибридные есть, которые сразу обе концепции совмещают, потому что у каждого подхода есть свои pros/cons, которые при совмещении могут закрываться.

На этапе ранжирования важной проблемой является кол-во данных. Фактически не представляется возможным отранжировать все существующие айтемы и взять из них топ, просто потому что это очень трудозатратно. Потому есть отбор кандидатов — какой-то относительно дешёвый способ сузить общее кол-во айтемов к небольшому множеству, чтобы все последующие действия производить только с ними. Тут конечно важно не испортить полноту айтемов, чтобы было вообще из чего в итоге выбирать. Как это можно делать:
- эвристически: выбирать самое популярное (в целом, или по геопозиции, или среди такого же возраста, или что угодно ещё);
- коллаборативно (item2item): офлайн посчитать 100 похожих айтемов для каждого, сложить это в какое-нибудь key-value и в онлайне взять для ста интересующих нас айтемов по 100 кандидатов. Отскорить итоговые 10k;
- юзать контентные похожести (взять какое-нибудь hnsw и для каждого айтема брать 100 самых близких в пространстве объектов);
- что-то продуктовое: докинуть чего-нибудь нового, например.

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

В коллаборативной фильтрации есть проблема холодного старта: если у айтема/юзера нет никакого рейтинга/оценок, непонятно, кому и стоит ли вообще это рекомендовать.
Для юзеров можно попытаться собрать как можно больше инфы (хотя бы при регистрации задать пару лишних вопросов или как-то поонбордить его и предложить сразу что-то оценить).
Для айтемов тут можно использовать контентный подход.
👍2🤔2🆒1
#common

Рекомендательные системы 2/2.

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

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

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

Но я не настоящий сварщик в мльных штучках всяких. Просто интересно стало.
👍6🆒1
#cpp

Немножко отдельных фактов из одного доклада с C++ Russia.

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

delete [] { return new int; }();

Скомпилируется ли этот код? Ответ: нет. А почему?

Семантически этот код можно считать корректным. Мы вызываем лямбду, которая возвращает нам указатель на выделенную память, которую тут же удаляем.
Давайте посмотрим на грамматику для [expr.delete]:

delete-expression:
:: opt delete cast-expression
:: opt delete [] cast-expression


Т.е. тут либо delete с [expr.cast], либо delete []. Вроде всё корректно. У нас может быть выбрана первая альтернатива, потому выражение является корректным. Однако если почитать стандарт повнимательнее, мы увидим

Whenever the delete keyword is immediately followed by empty square brackets, it shall be interpreted as the second alternative.

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

2. Из интересного ещё такой пример:

int override = 0; // ok
int private = 0; // error


Почему первое работает, а второе нет?
В языке есть специальные слова, которые не являются ключевыми. Они являются идентификаторами со специальным значением (final, import, module, override). И если у компилятора есть сомнения, является ли это специальным значением, он разрешает это в пользу ответа нет. Т.е. подстраивается под существующий контекст.

Эти два примера из очень приятного доклада Константина Владимирова про семантические процессы в C++.

У автора, кроме потрясающих курсов по плюсам на youtube, есть ещё канал в тг: @cpp_lects_rus.

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

=========================
У меня тут на днях google benchmark не хотел в cmake подтягиваться, потому что чуваки переименовали master в main. Вот она западная толлерантность. Из-за неё проекты не собираются.
🔥14👍6💩2💔2
#common #cpp

1. Newsletter Вадима Кравченко про документацию. Довольно годный.

2. Какая-то приконая статья про организационные структуры. Понравилась из-за конкретной истории в качестве примера.

3. Статья про различные способы организации транзакций.

4. Про статус C++26 и несколько интерсных пропозалов: link.

5. API Evolution Without Versioning.
Тут конечно немножко обман, потому что я почему-то ожидал какую-то хорошую альтернативу, но увидел знакомые вещи, которые мы используем регулярно рядом с обычным подходом с версиями. Версии кстати очень больно. Даже в нашем случае, когда клиентов не очень много. Потому что и их хватает, чтобы поддерживать легаси год+, пока все не перейдут.

===========================
6. Тут на днях пытались понять, почему такой код (https://godbolt.org/z/v55f5zW6o) падает на ассерте. Потому что если зайти на cppreference/rehash, можно увидеть текст

Sets the number of buckets to count and rehashes the container, i.e. puts the elements into appropriate buckets considering that total number of buckets has changed. If the new number of buckets makes load factor more than maximum load factor (count < size() / max_load_factor()), then the new number of buckets is at least size() / max_load_factor().

Который я читаю следующим образом:
- кол-во бакетов становится count;
- если max_load_factor превышается, кол-во бакетов увеличивается, чтобы не превышался.

Стандарт же говорит немного по-другому:

a.rehash(n)

Postconditions: a.bucket_count() >= a.size() / a.max_load_factor() and a.bucket_count() >= n.


Т.е. кол-во бакетов нестрого больше минимального необходимого количества. Что вообще-то означает, что может быть вполне себе + один/сто/миллион бакетов сверху.

Эх. И не поправить сппреф🥳
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2🔥21🤔1
#cpp

Немножко про оптимизации и C++ 1/2 [это база].

0. SSO -- small/short string optimization.
Если мы посмотрим на sizeof(std::string), почти наверняка (без упоминания специфических архитектур и старых компиляторов) мы увидим 32 (что больше, чем ожидаемые 24 при стандартной схеме с указателем на данные и двумя чиселками size и capacity). Примерно потому что на самом деле строка выглядит так:

template <typename T>
struct basic_string {
char* begin_;
size_t size_;
union {
size_t capacity_;
char sso_buffer[16];
};
};


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

У bfilipek есть небольшая статья про то, как узнать размер этого буфера с помощью constexpr/constinit. А тут на so можно посмотреть какой-то домашний бенчмарк с пруфом, что это действительно что-то ускоряет.

sso -- частный случай small object/buffer optimization.
Такой подход также применяется в std::function. Затрекать можно следующим кодом:

auto f = [i = 0]() { std::cout << &i << ' '; };
std::function<void()> alpha = f;
alpha();
std::function<void()> beta = std::move(alpha);
beta();


Если данные хранятся на куче, то при муве мы просто переложим указатель на область памяти из alpha в beta. Если же данные будут хранится на стеке, то придётся перекладывать сам объект и адреса в выводе будут разные (gb).

Ещё sbo используют в различных реализациях small_vector. Причём если в случае std::string/std::function вы храните либо на стеке, либо на куче, то тут иногда делают вперемешку: могут часть оставить в буффере, а часть вынести на кучу; ну или на кучу всё, чтобы данные лежали в непрерывном куске памяти.

1. Empty base [class] optimization (ebo/ebco).

Имеем такой код:

struct A {};
struct B : A {};


Как известно, в C++ sizeof пустого объекта -- 1 байт. Т.е. у B размер должен быть хотя бы 2 байта. Но конечно же это не так. Он всё ещё 1 байт, потому что в дело вступает ebco.

В случае нескольких родителей каждый компилятор делает что хочет. В msvc, например, оптимизация применяется только к последнему родителю (остальные занимают по байту). Clang и GCC же применяют ко всем.

Предположим, мы хотим использовать кастомный делитер с std::unique_ptr. Как сделать так, чтобы хранение делитера не занимало памяти? В случае, если делитер это какой-то stateless класс-функтор, от него можно отнаследоваться. Тогда в дело вступает ebco, что позволяет не занимать лишний байт, но при этом использовать operator(). Обычно это делают с помощью compressed_pair (реализовано это примерно разбором случаев на компиляции, когда можно наследоваться от типа, а когда нет; если нельзя, то тип просто хранится как член класса). Так что, если вам нужен кастомный делитер, лучше делать его без состояния.

Рядом с ebco обычно говорят ещё про атрибут [[no_unique_address]] (C++20). Когда вы помечаете какой-то из членов класса подобным атрибутом, вы говорите, что этот объект может не иметь уникального адреса в памяти и потенциально перекрываться с другими членами класса. Так с C++20 вы можете реализовать compressed_pair гораздо проще:

template <typename T, typename U>
struct compressed_pair {
[[no_unique_address]] T _val1;
[[no_unique_address]] U _val2;
};


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

Про другие атрибуты писал тут: https://news.1rj.ru/str/thisnotes/218.
2👍2🔥1
#cpp

Немножко про оптимизации и C++ 2/2.

2. Про RVO/NRVO писал ранее: t.me/thisnotes/198.

Рядом с этой темой можно посмотреть доклад про move-only C++ design.

3. Про девиртуализацию писал Женя: t.me/cxx95/88.

==============================
4. strlen elision.
Как известно, функция strlen работает за длину строки. Соответственно, если вы используете её в циклах:

for (int i = 0; i < strlen(str); ++i) {…}

Вы автоматом ловите квадрат в асимптотике. Потому хорошей практикой является выносить вот эту границу справа в отдельную переменную:

for (int i = 0, end = strlen(str); i < end; ++i) {}

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

int main() {
return std::strlen(“hi”);
}


компилятор сделает вам:

main: # @main
mov eax, 2
ret


Ну это база.

Недавно на собесе меня попросили написать strlen. Потом ещё каким-то образом попросили это дело поускорять. Кек, что таким ещё занимаются. Хотя конечно, если погуглить, можно найти что-нибудь вроде такого или такого. Так что может и не без оснований, но лучше я вкину вот такую статью про полезность собеседований подобного рода.

Как писали на каком-то сабреддите:

Optimize strlen by not using it.
👍112🔥2
#highload

Пару ссылочек.

1. What I Wish I Had Known Before Scaling Uber to 1000 Services.

У uber есть вот такая статья про domain-oriented архитектуру. Ключевая идея — объединять сервисы в домены по бизнесовому назначению и ходить в домены снаружи исключительно через gateway (там конечно ещё много всяких тонкостей и сложностей), что позволяет навести некоторый порядок в вашей архитектуре и том, как ваши бекенды взаимодействуют.

2. What is a Vector Database?

Хорошая и понятная статья про хранение векторов.

==========================
Оч устал за последнее время. Отваливаюсь отдыхать на пару недель.
🔥81