Как игровой движок распределяет тысячи корутин между потоками?
В статье разбирают shuttle_async, экспериментальный планировщик задач для Vulkan-движка ShuttleEngine. Автор показывает, как локальные очереди и work stealing помогают распределять работу между потоками.
В статье разбирают:
• как устроены локальные деки Chase-Lev и кража задач
• зачем корутинам нужен диспетчер и structured concurrency
• как устроены три режима запуска задач
• что показали замеры на малых, больших и неравномерных задачах
Результаты относятся к конкретному проекту и тестовой машине.
📣 C++ Ready | #статья
В статье разбирают shuttle_async, экспериментальный планировщик задач для Vulkan-движка ShuttleEngine. Автор показывает, как локальные очереди и work stealing помогают распределять работу между потоками.
В статье разбирают:
• как устроены локальные деки Chase-Lev и кража задач
• зачем корутинам нужен диспетчер и structured concurrency
• как устроены три режима запуска задач
• что показали замеры на малых, больших и неравномерных задачах
Результаты относятся к конкретному проекту и тестовой машине.
Продолжай читать на Habr!
Please open Telegram to view this post
VIEW IN TELEGRAM
❤8👍2🤝2👎1🔥1
Знаете, почему optional.value_or может выполнить лишнюю работу?
Аргументы функции вычисляются до её вызова. Поэтому fallback внутри value_or запустится даже тогда, когда optional уже содержит значение.
Например, результат загрузки может не понадобиться:
Здесь loadNameFromDisk вызывается заранее, а не только при пустом cachedName. Для чтения файла, запроса к базе или построения объекта это лишняя работа.
Если запасное значение дорого получать, проверяем optional отдельно:
Условный оператор вычисляет только выбранную ветку. Поэтому загрузка выполняется лишь при отсутствии готового значения.
Для дешёвого значения value_or остаётся коротким и удобным:
Ленивое вычисление лучше выражать условием или отдельной функцией.
📣 C++ Ready | #совет
Аргументы функции вычисляются до её вызова. Поэтому fallback внутри value_or запустится даже тогда, когда optional уже содержит значение.
Например, результат загрузки может не понадобиться:
auto name = cachedName.value_or(
loadNameFromDisk());
Здесь loadNameFromDisk вызывается заранее, а не только при пустом cachedName. Для чтения файла, запроса к базе или построения объекта это лишняя работа.
Если запасное значение дорого получать, проверяем optional отдельно:
auto name = cachedName
? *cachedName
: loadNameFromDisk();
Условный оператор вычисляет только выбранную ветку. Поэтому загрузка выполняется лишь при отсутствии готового значения.
Для дешёвого значения value_or остаётся коротким и удобным:
auto port = configuredPort.value_or(8080);
Ленивое вычисление лучше выражать условием или отдельной функцией.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8❤7🔥3
Объединяем пересекающиеся интервалы на C++!
Дан список занятых временных интервалов. Нужно объединить пересекающиеся и соприкасающиеся отрезки.
Представляем каждый интервал парой начала и конца. Считаем границы полуоткрытыми, поэтому отрезки [2, 5) и [5, 7) тоже объединяются:
Сначала сортируем пары по началу, а при равенстве по концу. После сортировки каждый следующий интервал можно сравнить только с последним результатом:
Если новый интервал начинается позже конца последнего, добавляем его отдельно. Иначе расширяем конец уже собранного интервала до максимальной границы:
Строгое сравнение оставляет соприкасающиеся интервалы в одной группе. Для полуоткрытых границ результат примера будет таким:
После сортировки алгоритм делает один проход и работает за O(n log n), где основное время уходит на сортировку.
📣 C++ Ready | #практика
Дан список занятых временных интервалов. Нужно объединить пересекающиеся и соприкасающиеся отрезки.
Представляем каждый интервал парой начала и конца. Считаем границы полуоткрытыми, поэтому отрезки [2, 5) и [5, 7) тоже объединяются:
#include <algorithm>
#include <utility>
#include <vector>
using Interval = std::pair<int, int>;
std::vector<Interval> busy{
{1, 3}, {2, 5}, {8, 10}, {10, 12}
};
Сначала сортируем пары по началу, а при равенстве по концу. После сортировки каждый следующий интервал можно сравнить только с последним результатом:
std::sort(busy.begin(), busy.end());
std::vector<Interval> merged;
Если новый интервал начинается позже конца последнего, добавляем его отдельно. Иначе расширяем конец уже собранного интервала до максимальной границы:
for (const auto& current : busy) {
if (merged.empty() ||
merged.back().second < current.first) {
merged.push_back(current);
} else {
merged.back().second = std::max(
merged.back().second, current.second);
}
}Строгое сравнение оставляет соприкасающиеся интервалы в одной группе. Для полуоткрытых границ результат примера будет таким:
[1, 5) [8, 12)
После сортировки алгоритм делает один проход и работает за O(n log n), где основное время уходит на сортировку.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤8👍4🔥3
Схема протокола acquire-release в C++!
Release-store и acquire-load помогают одному потоку опубликовать подготовленные данные, а другому безопасно увидеть их после сигнала готовности. Синхронизация возникает, когда acquire-load получает значение от release-store того же atomic-объекта.
На картинке показаны оба потока, общий флаг x и порядок операций до и после передачи сигнала. Подпись отдельно подчёркивает важное условие. Обе операции должны использовать один и тот же atomic-флаг.
📣 C++ Ready | #совет
Release-store и acquire-load помогают одному потоку опубликовать подготовленные данные, а другому безопасно увидеть их после сигнала готовности. Синхронизация возникает, когда acquire-load получает значение от release-store того же atomic-объекта.
На картинке показаны оба потока, общий флаг x и порядок операций до и после передачи сигнала. Подпись отдельно подчёркивает важное условие. Обе операции должны использовать один и тот же atomic-флаг.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6❤3🔥3🤝1
Знали, что ключ в std::map можно переименовать без пересоздания узла?
Ключи внутри map нельзя менять напрямую, потому что контейнер поддерживает порядок элементов. Начиная с C++17 элемент можно временно извлечь как node handle, изменить его ключ и вставить обратно без копирования хранимого значения.
Сначала извлекаем элемент и проверяем, что он найден:
Пустой node handle означает, что такого ключа в контейнере не было. Пока узел извлечён, он не участвует в сортировке map, поэтому ключ можно изменить:
Сам объект и его mapped value остаются внутри узла. Это полезно, когда значение дорого копировать или оно не копируется вовсе:
Теперь пробуем вернуть узел и проверяем результат, потому что newKey мог уже появиться в map:
При конфликте запись не теряется, она остаётся в result.node. Можно восстановить старый ключ и вернуть её в контейнер:
Успешная вставка возвращает тот же узел без пересоздания значения. Такой приём удобен для переименования ключей, если важно сохранить объект и явно обработать возможный конфликт.
📣 C++ Ready | #совет
Ключи внутри map нельзя менять напрямую, потому что контейнер поддерживает порядок элементов. Начиная с C++17 элемент можно временно извлечь как node handle, изменить его ключ и вставить обратно без копирования хранимого значения.
Сначала извлекаем элемент и проверяем, что он найден:
auto node = users.extract(oldKey);
if (node.empty()) return;
Пустой node handle означает, что такого ключа в контейнере не было. Пока узел извлечён, он не участвует в сортировке map, поэтому ключ можно изменить:
auto oldKey = node.key();
node.key() = newKey;
Сам объект и его mapped value остаются внутри узла. Это полезно, когда значение дорого копировать или оно не копируется вовсе:
auto& profile = node.mapped();
profile.lastSeen = now;
Теперь пробуем вернуть узел и проверяем результат, потому что newKey мог уже появиться в map:
auto result = users.insert(std::move(node));
if (result.inserted) {
std::cout << "renamed\n";
}
При конфликте запись не теряется, она остаётся в result.node. Можно восстановить старый ключ и вернуть её в контейнер:
if (!result.inserted) {
result.node.key() = oldKey;
users.insert(std::move(result.node));
}Успешная вставка возвращает тот же узел без пересоздания значения. Такой приём удобен для переименования ключей, если важно сохранить объект и явно обработать возможный конфликт.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤6👍4🔥2
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥12❤4👍3
Please open Telegram to view this post
VIEW IN TELEGRAM
😁6👎4
Почему компаратор с <= может сломать std::sort?
При сортировке чисел по возрастанию хочется написать сравнение так:
Проблема проявляется, когда в диапазоне есть одинаковые значения. Для двух равных чисел компаратор возвращает true, хотя ни одно из них не должно стоять раньше другого.
Проверим самую простую ситуацию:
Результат равен 1. Но std::sort требует строгий слабый порядок, элемент не может быть меньше самого себя. Использование такого компаратора нарушает требования алгоритма.
Правильный вариант для возрастания использует строгое сравнение:
Если у объектов несколько полей, добавь запасной критерий. Например, при одинаковом приоритете можно сравнить id:
Так порядок будет предсказуемым и для равных приоритетов. Если же требуется сохранить исходный порядок равных элементов, используй std::stable_sort с корректным строгим компаратором, а не заменяй < на <=.
📣 C++ Ready | #совет
При сортировке чисел по возрастанию хочется написать сравнение так:
std::sort(values.begin(), values.end(),
[](int a, int b) { return a <= b; });
Проблема проявляется, когда в диапазоне есть одинаковые значения. Для двух равных чисел компаратор возвращает true, хотя ни одно из них не должно стоять раньше другого.
Проверим самую простую ситуацию:
auto lessOrEqual = [](int a, int b) { return a <= b; };
std::cout << lessOrEqual(5, 5);Результат равен 1. Но std::sort требует строгий слабый порядок, элемент не может быть меньше самого себя. Использование такого компаратора нарушает требования алгоритма.
Правильный вариант для возрастания использует строгое сравнение:
std::sort(values.begin(), values.end(),
[](int a, int b) { return a < b; });
Если у объектов несколько полей, добавь запасной критерий. Например, при одинаковом приоритете можно сравнить id:
if (a.priority != b.priority) return a.priority < b.priority;
return a.id < b.id;
Так порядок будет предсказуемым и для равных приоритетов. Если же требуется сохранить исходный порядок равных элементов, используй std::stable_sort с корректным строгим компаратором, а не заменяй < на <=.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤3👍3🔥2
Собираем конфигурацию с приоритетом источников на C++!
Настройки могут приходить из файла, переменных окружения и аргументов запуска. Соберём единый конфиг и сохраним источник каждого итогового значения.
В этой задаче:
Так конфигурацию можно не только собрать, но и понять, какой источник переопределил параметр.
📣 C++ Ready | #задача
Настройки могут приходить из файла, переменных окружения и аргументов запуска. Соберём единый конфиг и сохраним источник каждого итогового значения.
В этой задаче:
• описываем слой настроек и его источник
• применяем слои от низкого приоритета к высокому
• сохраняем источник последнего значения
Так конфигурацию можно не только собрать, но и понять, какой источник переопределил параметр.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5❤4🔥3
Gemini Pro на 24 месяца за 800₽!
Вместо 39.000₽ — всего 800₽ ($9.9) за 2 года. По акции сейчас это 33₽ в месяц, в 48 раз выгоднее!
➡ Argon 4.0 + Claude Opus 5.5 — для кода
➡ Nano Banana 2 Pro — для картинок
➡ Генерация видео — ролики и анимации
➡ AntiGravity + 5 ТБ облака
➡ Opus 5.5 и Sonnet 5.5
➡ Youtube Premium
Приобрести можно в Магазине с 20.000+ покупок и 2000+ отзывов (есть возвраты и замены).
Вместо 39.000₽ — всего 800₽ ($9.9) за 2 года. По акции сейчас это 33₽ в месяц, в 48 раз выгоднее!
Акция ограничена по времени, дальше будет официальная цена 480$!
Приобрести можно в Магазине с 20.000+ покупок и 2000+ отзывов (есть возвраты и замены).
Please open Telegram to view this post
VIEW IN TELEGRAM
👎10❤2👍1😁1
Объединяем две хронологии событий без повторной сортировки!
Представим, что приложение получило события из двух источников. Каждый источник уже отсортировал свои записи по времени. Чтобы получить общую хронологию, не нужно складывать всё вместе и сортировать заново, два упорядоченных диапазона можно слить.
Опишем событие и подготовим два входных списка. Внутри каждого время должно идти по возрастанию:
Заранее выделим место под все записи. Это не меняет размер вектора, но избавляет его от лишнего расширения при добавлении элементов:
Теперь используем std::ranges::merge. Проекции сообщают алгоритму, что сравнивать нужно поле time, а не весь объект:
Результат будет идти в порядке 10, 20, 30, 40. Если времена совпадут, алгоритм сохранит порядок элементов внутри каждого исходного списка. Записи с одинаковым временем не исчезают, merge переносит их все.
Проверим результат обычным обходом:
Слияние работает за время, пропорциональное сумме длин списков, и удобно для логов, событий и результатов нескольких запросов.
📣 C++ Ready | #практика
Представим, что приложение получило события из двух источников. Каждый источник уже отсортировал свои записи по времени. Чтобы получить общую хронологию, не нужно складывать всё вместе и сортировать заново, два упорядоченных диапазона можно слить.
Опишем событие и подготовим два входных списка. Внутри каждого время должно идти по возрастанию:
struct Event {
int time;
std::string message;
};
std::vector<Event> app = {{10, "start"}, {30, "save"}};
std::vector<Event> worker = {{20, "job"}, {40, "done"}};Заранее выделим место под все записи. Это не меняет размер вектора, но избавляет его от лишнего расширения при добавлении элементов:
std::vector<Event> timeline;
timeline.reserve(app.size() + worker.size());
Теперь используем std::ranges::merge. Проекции сообщают алгоритму, что сравнивать нужно поле time, а не весь объект:
std::ranges::merge(app, worker, std::back_inserter(timeline),
{}, &Event::time, &Event::time);
Результат будет идти в порядке 10, 20, 30, 40. Если времена совпадут, алгоритм сохранит порядок элементов внутри каждого исходного списка. Записи с одинаковым временем не исчезают, merge переносит их все.
Проверим результат обычным обходом:
for (const Event& event : timeline) {
std::cout << event.time << " " << event.message << '\n';
}Слияние работает за время, пропорциональное сумме длин списков, и удобно для логов, событий и результатов нескольких запросов.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤4👍4🔥3