Простите меня, дорогие подписчеки, за отсуствие постов. Бездельничал и дурачился. Собирал для вас пак тем, про котоыре хочу рассказать. Первая на очереди — многопоточка.
Начнем с чего попроще, закончим какими-нибудь паттернами и подходами. Возможно разберем какую-нибудь прикладную задачку. Короче, должно быть интересно!
Начнем с чего попроще, закончим какими-нибудь паттернами и подходами. Возможно разберем какую-нибудь прикладную задачку. Короче, должно быть интересно!
❤3
#прикольчики C++ №9
Начнем с простой фишечки. Когда вы пишете многопоточные программы, бывают случаи, когда каждому потоку нужен свой собственный экземпляр переменной. И тут умные разработчики из комитета по стандартизации C++ придумали ключевое слово
Это спецификатор, который указывает, что переменная должна быть уникальной для каждого потока. Другими словами, каждый поток создаёт свою "копию" переменной, независимую от других.
Пример:
Выведет:
В целом бывает достаточно полезно иногда, пару раз встречал в продакшн-коде. Но с ними следует быть достаточно аккуратными: во-первых, это не всегда быстрее условного мьютекса сверху (если объект очень большой, например), а во-вторых, даже динамически выделенная память под thread_local переменные будет очищена, когда поток завершится.
Начнем с простой фишечки. Когда вы пишете многопоточные программы, бывают случаи, когда каждому потоку нужен свой собственный экземпляр переменной. И тут умные разработчики из комитета по стандартизации C++ придумали ключевое слово
thread_local
.Это спецификатор, который указывает, что переменная должна быть уникальной для каждого потока. Другими словами, каждый поток создаёт свою "копию" переменной, независимую от других.
Пример:
#include <iostream>
#include <thread>
thread_local int localCounter = 0;
void incrementCounter(const std::string& threadName) {
localCounter++;
std::cout << "Thread " << threadName << ": " << localCounter << "\n";
}
int main() {
std::thread t1(incrementCounter, "A");
std::thread t2(incrementCounter, "B");
t1.join();
t2.join();
return 0;
}
Выведет:
Thread A: 1
Thread B: 1
В целом бывает достаточно полезно иногда, пару раз встречал в продакшн-коде. Но с ними следует быть достаточно аккуратными: во-первых, это не всегда быстрее условного мьютекса сверху (если объект очень большой, например), а во-вторых, даже динамически выделенная память под thread_local переменные будет очищена, когда поток завершится.
❤2
Кстати, мне нравится текущее число подписчиков: 5 в двоичной системе счисления. Запрещаю всем отписываться и подписываться.
Не заводите ТГ канал, пацаны, вы матерям еще нужны , его пиздец как лень вести
💯1 1
Я тут короче смесяц назад наткнулся на статью одного господина из кембриджа. Он в своей статье рассказывает, что придумал способ вставлять элементы (без реордеринга) в плоскую хеш-таблицу оптимальнее, чем при использовании равномерного хеширования. Что, так-то, 40 лет невозможным считалось. Очень и очень рекомендую ознакомиться: "Optimal Bounds for Open Addressing Without Reordering".
Впрочем, вы может уже и видели, много пабликов про это писало с кликбейтными заголовками.
Впрочем, вы может уже и видели, много пабликов про это писало с кликбейтными заголовками.
🔥2❤1
Ну и мне как-то захотелось написать свою реализацию и сравнить ее с другими. Получилось реально хорошо. Можете взглянуть на моем гитхабе: https://github.com/garbart/FlatHashTable.
Это реализация жадного алгоритма из вышеупомянутой статьи. там еще есть и "эластичный" алгоритм, который звучит еще интереснее. Попозже и его реализую, думаю.
Но даже так результаты очень приятные. Сравнивал с
По времени вставки
Я, честно, не понял почему мапа от
В целом я экспериментом супер доволен. Когда нибудь эту мапку допишу до продакш-реди и реально буду использовать в своих проектах.
Это реализация жадного алгоритма из вышеупомянутой статьи. там еще есть и "эластичный" алгоритм, который звучит еще интереснее. Попозже и его реализую, думаю.
Но даже так результаты очень приятные. Сравнивал с
absl::flat_hash_map
и встроенной std::unordered_map
. К результатам, 10M элементов:absl::flat_hash_map benchmark:
Put time: 26947 ms
Get time: 8824 ms
Iterate time: 1522 ms
Remove time: 13798 ms
std::unordered_map benchmark:
Put time: 10444 ms
Get time: 1674 ms
Iterate time: 713 ms
Remove time: 4140 ms
FunnelFlatHashTable benchmark:
Put time: 3416 ms
Get time: 1844 ms
Iterate time: 435 ms
Remove time: 2182 ms
По времени вставки
FunnelFlatHashTable
получилась заметно быстрее std::unordered_map
, и это самое главное. Немного дольше получение по значению — почти наверняка я налажал где-то в коде, как будто бы так не должно быть. Быстрее итерация по значениям (что логично, это же плоская таблица все-таки) и быстрее удаление.Я, честно, не понял почему мапа от
absl
работает так медленно. Просвятите меня, если понимаете. Мне это кажется контринтуитивным.В целом я экспериментом супер доволен. Когда нибудь эту мапку допишу до продакш-реди и реально буду использовать в своих проектах.
🔥2👏1 1
Если вы, кстати, тотально не понимаете о чем я и что за "плоская хеш-таблица", можете перечитать один из моих постов: https://t.me/partypooper_cpp/50
✍1🤔1
Forwarded from Experimental chill
Trivially relocatable
В C++ есть большая группа людей (включая меня), которая любит брать оптимизации из C -- mem* функции возможно являются сильнейшим преимуществом C перед многими другими языками в перформансе. В C++ об этом думали и делали аттрибут std::trivially_copyable, который разрешает копировать как memcpy. Отлично работает на примитивных структурах и в целом C++ такой быстрый в том числе и из-за этого.
Но бывают и более интересные кейсы. Когда вы добавляете элементы в std::vector, рано или поздно вам надо будет реаллоцировать. Вы будете копировать числа/делать std::move объектов из предыдущего вектора. В ситуации с числами можно звать memcpy, они тривиальны, всё отлично. Но на самом деле звать memcpy можно и не только на тривиально копируемые типы, а, например, на unique_ptr<T>, или QString (который с ref count), или vector<int>!
Ведь копирование указателей, чисел вида размера/вместимости при тривиальном std::move ни к чему плохому не приведёт. К сожалению, по стандарту так нельзя. Когда вы начинаете делать mem* функции на типы, у которых примитивный std::move, то стандарт ничего для этого не приготовил и только говорит, что вы обязаны вызвать деструктор, и memcpy не позволяет начать жизнь более сложных объектов.
Поэтому в последние лет 6 идёт борьба за то, чтобы внести понятие тривиально релоцируемых типов -- у которых move оператор и move конструкторы устроены так, что это просто копирование по битам. Если быть точным, нужно ещё, чтобы деструктор не делал странных вещей, например, ничего не делал для пустых векторов, нулевых указателей и тд.
Без этого вставки в середину вектора, удаления из векторов долго не могли использовать memcpy тоже для таких типов. Существует ряд оптимизаций, который открывается из этого. В том числе interop с Rust станет слегка попроще :)
6 лет бились (первые года 4 не сильно, комитету в целом нравилось предложение), и таки уехало в феврале этого года в C++26.
Есть отличный блог из 5 небольших частей почитать о том, как это устроено в Qt.
Сам пропозал
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p2786r13.html#abstract
В C++ есть большая группа людей (включая меня), которая любит брать оптимизации из C -- mem* функции возможно являются сильнейшим преимуществом C перед многими другими языками в перформансе. В C++ об этом думали и делали аттрибут std::trivially_copyable, который разрешает копировать как memcpy. Отлично работает на примитивных структурах и в целом C++ такой быстрый в том числе и из-за этого.
Но бывают и более интересные кейсы. Когда вы добавляете элементы в std::vector, рано или поздно вам надо будет реаллоцировать. Вы будете копировать числа/делать std::move объектов из предыдущего вектора. В ситуации с числами можно звать memcpy, они тривиальны, всё отлично. Но на самом деле звать memcpy можно и не только на тривиально копируемые типы, а, например, на unique_ptr<T>, или QString (который с ref count), или vector<int>!
Ведь копирование указателей, чисел вида размера/вместимости при тривиальном std::move ни к чему плохому не приведёт. К сожалению, по стандарту так нельзя. Когда вы начинаете делать mem* функции на типы, у которых примитивный std::move, то стандарт ничего для этого не приготовил и только говорит, что вы обязаны вызвать деструктор, и memcpy не позволяет начать жизнь более сложных объектов.
Поэтому в последние лет 6 идёт борьба за то, чтобы внести понятие тривиально релоцируемых типов -- у которых move оператор и move конструкторы устроены так, что это просто копирование по битам. Если быть точным, нужно ещё, чтобы деструктор не делал странных вещей, например, ничего не делал для пустых векторов, нулевых указателей и тд.
Без этого вставки в середину вектора, удаления из векторов долго не могли использовать memcpy тоже для таких типов. Существует ряд оптимизаций, который открывается из этого. В том числе interop с Rust станет слегка попроще :)
6 лет бились (первые года 4 не сильно, комитету в целом нравилось предложение), и таки уехало в феврале этого года в C++26.
Есть отличный блог из 5 небольших частей почитать о том, как это устроено в Qt.
Сам пропозал
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p2786r13.html#abstract
KDAB
Qt and Trivial Relocation (Part 1): What is relocation? | KDAB
Discover how Qt optimizes container operations with byte-level manipulations, including trivial relocation for types like int and QString.
❤2