LINUX.ORG.RU

как оптимизировать с++ код, чтобы 7000 бинарников не выедали всё cpu

 , , , ,


2

3

https://imgur.com/RcrmzW0.png

https://imgur.com/Z4wdNBA.png

Код простой, в простое опрашивает ивенты, больше ничего не происходит.

- запускаю 1000-3000 бинарников - всё ок
- на 7000 бинарников - картина на скрине

Возможно у кого-то есть какие-то идеи куда смотреть и почему так просходит? откуда это ограничение в 7000

код очереди

std::optional<T> pop() {
        std::unique_lock<std::mutex> lock(this->mutex);

        if (q.empty()) {
            return std::nullopt;
        }

        std::optional<T> value = std::move(this->q.front());
        this->q.pop();

        return value;
    };


код опроса инвентов (он и генерит лоад)
while (true) {
        auto tick_start = std::chrono::steady_clock::now();

        if (auto event = internal_events_.pop(); event) {
            std::visit([this](auto &&casted_event) {
                process_event(casted_event);
            }, event.value());
        }

        if (auto event = my_events_.pop(); event) {
            using namespace td::td_api;
            auto &&object = event.value();
            switch (object->get_id()) {
                case updateMyActivity::ID:
                    process_event(move_object_as<updateMyActivity>(object));
                    break;
                case updateMyActivity2::ID:
                    process_event(move_object_as<updateMyActivity2>(object));
                    break;
                default:
                    break;
            }
        }

        if (auto event = my_q_events_.pop(); event) {
            std::visit([this](auto &&casted_event) {
                process_event(casted_event);
            }, event.value());
        }

        auto tick_end = std::chrono::steady_clock::now();
        auto duration = tick_end - tick_start;
        auto sleep_time = std::chrono::milliseconds(10) - duration;
        if (sleep_time.count() > 0) {
            std::this_thread::sleep_for(sleep_time);
        }
    }

★★★★★

Последнее исправление: smilessss (всего исправлений: 2)

Ответ на: комментарий от smilessss

spurious wake у тебя. Может кондишон вариабле поможет не будить зря вразнобой... а может архитектуру надо переработать, чтоб потоков было меньше, а сроупута больше :)

slackwarrior ★★★★★
()
Последнее исправление: slackwarrior (всего исправлений: 1)
Ответ на: комментарий от hatred

Ну у тредов контекст переключить легче, так что всестанет колом не на 7 тыс а на 20ти;-)

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

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

В общем я лучше пойду физикой займусь, оно как то проще;-)

AntonI ★★★★
()
Ответ на: комментарий от smilessss

посмотри как в golang работают каналы, и подумай - зачем они.

такие каналы - это очереди, на которых тред может ожидать! помещения туда данных.

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

у тебя должно быть то же самое - некий тред ждет на некоем «канале» помещения туда некоего «ивента», и как только другой твой тред или нечто системное, туда ивент помещает - тред срывается извлекает ивент, обрабатывает и снова встает в ожидание.

такова общая схема работы в таких вот задачах.

alysnix ★★★
()
  • запускаю 1000-3000 бинарников - всё ок
  • на 7000 бинарников - картина на скрине

Не надо так. По этому поводу уже все расписали. Даже 1000 одновременно исполняющихся (не спящих, не ждущих, а активно исполняющихся) процессов будут давать большой contemption. Тот же мейк неспроста с JOBS=$(nprocs) запускают.

Далее, если все это вместе логически связано и нет требований к безопасности, то лучше процессов делать меньше – а тредов больше. Они легче, и управлять ими проще.

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


Теперь касательно обработчика.

std::unique_lock<std::mutex> lock(this->mutex);

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

if (auto event = internal_events_.pop(); event) {

if (auto event = my_events_.pop(); event) {

if (auto event = my_q_events_.pop(); event) {

Не надо так делать. Ты замыкаешь три мьютекса по очереди просто чтобы проверить наличие элемента в каждой очереди. Замкнуть один и проверить все три было бы значительно дешевле. Если бы только был какой-то способ знать, когда придет ивент, и сразу его обработать…

Код простой, в простое опрашивает ивенты

Не нужно их опрашивать. Нужно, чтобы приходящий ивент будил твой обработчик, обработчик его обрабатывал и засыпал обратно. epoll, poll, select, что угодно. Если ивенты генерируются внутри процесса, то condition_variable.

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

Чем меньше свичей контекста (т.е. чем меньше тредов/процессов на единицу параллелизма), чем меньше точек синхронизации (мьютексы, condition_variable, даже атомики в seq_cst), тем больше полезной работы будет выполнять твой код.


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

Siborgium ★★★★★
()
Последнее исправление: Siborgium (всего исправлений: 2)
Ответ на: комментарий от smilessss

Каким образом ничего не делает ? А блокировка не в счёт ? Создание объектов не в счёт ? Разделение конечного времени выполнения не в счёт ? А что же в таком случае в счёт ? Завязывайте - создайте пул процессов/потоков и запускайте адекватное количество процессов. А касательно того, что «ничего не делают» - рекомендую всё же подумать или сразу почитать теорию строения ОС, ибо процессы и потоки никуда не деваются, а запуская их больше числа ядер вы только спускаете ресурсы процессора на планирование со стороны ОС и тут даже никакой ввод/вывод оправданием не стал бы - вы просто слили ресурсы и теперь удивляетесь этому факту.

AKonia ★★
()
Ответ на: комментарий от alysnix

У голанга ещё и user-space трединг сделан. Это как взять FreeRTOS/ThreadX/etc и скомпилировать как приложение под Linux, соблюдя все условия в port/ для стекинга и сохранение контекста и придумав таймер для шедулера (man 7 signal / SIGALRM). Т.е. на переключении контекста потока ещё не нужно тратить время на переключение User/Kernel.

Собсно поэтому все так носятся с корутинами, файберами и так далее.

hatred ★★★
()
Ответ на: комментарий от no-dashi-v2

для этого нужно это переделать в блокирующуюся очередь
я пока не придумал как

мне дали примеры, но я пока не разоблася что там(

#include <mutex>
#include <queue>
#include <condition_variable>

template<typename T>
class OptionalQueue {
public:
    OptionalQueue() = default;

    OptionalQueue(const OptionalQueue &) = delete;

    OptionalQueue &operator=(const OptionalQueue &) = delete;

    virtual ~OptionalQueue() = default;

    void emplace(std::optional<T> &&value) {
        {
            std::unique_lock<std::mutex> lock(this->mutex);
            q.emplace(std::move(value));
        }
    };

    std::optional<T> pop() {
        std::unique_lock<std::mutex> lock(this->mutex);

        if (q.empty()) {
            return std::nullopt;
        }

        std::optional<T> value = std::move(this->q.front());
        this->q.pop();

        return value;
    };

private:
    std::queue<std::optional<T>> q;
    std::mutex mutex;
};

smilessss ★★★★★
() автор топика
Последнее исправление: smilessss (всего исправлений: 1)
Ответ на: комментарий от smilessss

там это я так понял раз в 10 мсек проверяет апдейты

прекрасная архитектура)


const double WAIT_TIMEOUT = 10;

void Client::loop() {


    while (!is_closed) {
        auto response = client->receive(WAIT_TIMEOUT);
        process_response(std::move(response));
    }
}

smilessss ★★★★★
() автор топика
Ответ на: комментарий от smilessss

https://imgur.com/2LADDqt.png

а без этого - 30к бинарей и 325 000 тредов с лоад авереж - 1

так что дело там 100% в этом а не в количестве запускаемых бинарей и не в железе

smilessss ★★★★★
() автор топика
Ответ на: комментарий от smilessss

https://imgur.com/Oiq3EIq.png


что имеем


https://stackoverflow.com/questions/3666753/c-threads-and-simple-blocking-mec...


берём

boost::mutex lock;


делаем там где не нужно создавать лоад
lock.lock();


а когда всё таки нужно делаем
lock.unlock();


результат на скрине
теперь на 3500 бинарях и 30000 тредах - лоад авереж 5-8 и cpu в среднем загружен на 35%, уже намного лучше

эксперты по с++ - подскажите все эти boost::mutex lock или mutex::scoped_lock или любые другие мутексы - они одинаково работают в плане производительности?

в мысла неважно чем лочить верно

smilessss ★★★★★
() автор топика
Последнее исправление: smilessss (всего исправлений: 2)
Ответ на: комментарий от smilessss

подскажите все эти boost::mutex lock или mutex::scoped_lock или любые другие мутексы - они одинаково работают в плане производительности?

в мысла неважно чем лочить верно

По большому счёту - монопенисуально: всё равно оно в pthread_mutex_lock() закончится. Если pre-C++11 сборки поддерживать не надо - я бы примитивы из std:: использовал.

bugfixer ★★★★
()
Последнее исправление: bugfixer (всего исправлений: 1)