С++ std::async медленнее, чем последовательный цикл for

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

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

функция выглядит так:

void mint::physics::PhysicsEngine::SymplecticEuler(mint::physics::PhysicsBody* body)
{
  mint::graphics::Entity *entity = body->GetEntity();

  // -- Symplectic Euler
  glm::vec2 gravity = glm::vec2(0.0f, (1.0f / core::Timer::Instance()->DeltaTime()) * 9.81f) * body->GravityScale();

  glm::vec2 dv = (body->Force() * body->GetMassData()->inv_mass + gravity * core::Timer::Instance()->DeltaTime());
  body->Velocity(body->Velocity() +  dv);

  glm::vec2 dxy = glm::vec2(body->Velocity() * core::Timer::Instance()->DeltaTime());
  entity->Translate(glm::vec3(dxy, 0.0f));
  // -- END -- Symplectic Euler

  // -- update the collider
  body->UpdateCollider();
  // -- END -- update the collider
}

эта функция будет запускаться один раз для каждого физического тела и вызывается в цикле for, например так

auto start = std::chrono::high_resolution_clock::now();
for (auto body : all_bodys)
{
    //SymplecticEuler(body);
    // -- using std::async
    fEulerFutures.push_back(std::async(std::launch::async, SymplecticEuler, body));
    //SymplecticEuler(body);
}
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<float> duration = end - start;
std::cout << "physics update took: " << duration.count() << std::endl;

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

используя код синхронизации, который я написал, последовательный цикл занял 0,00014 с, а многопоточный цикл - 0,005 с. Я бы не ожидал, что многопоточный цикл займет больше времени, чем последовательный цикл, но это произошло, поэтому я предполагаю, что либо использую std::async неправильно, либо использую его в неправильном контексте. Программа, в которой я это запускаю, запускает простую симуляцию частиц с 300 частицами, так что пока ничего особенного.

Может кто-нибудь, дайте мне знать, правильно ли я использую std::async, потому что я все еще очень плохо знаком с концепцией многопоточности, или если я использую слишком много потоков, чтобы замедлить производительность движка, или я должен вместо этого использовать вычислительные шейдеры многопоточности (если использование вычислительных шейдеров улучшит производительность движка, пожалуйста, оставьте несколько ссылок на руководства по использованию вычислительных шейдеров в современном openGL с C++)

обе эти функции являются членами класса физического движка, а функция SymplecticEuler() является статической функцией.

Спасибо


person Ethan Hofton    schedule 21.04.2020    source источник
comment
Вы сделали fEulerFutures.reserve(all_bodys.size())?   -  person Ted Lyngmo    schedule 21.04.2020
comment
Если вы используете gcc 9 (или более позднюю версию) или MSVC, вы можете попробовать использовать политику параллельного выполнения: std::for_each(std::execution::par, all_bodys.begin(), all_bodys.end(), [](auto body) { SymplecticEuler(body); });   -  person Ted Lyngmo    schedule 21.04.2020


Ответы (1)


Я бы не ожидал, что многопоточный цикл займет больше времени, чем последовательный цикл.

Я думаю, это ваша проблема, почему вы думаете, что это займет меньше? Объем работы для передачи задач в параллельную структуру данных (которая, вероятно, включает в себя мьютексы, если они написаны плохо, или по крайней мере cmpxchg инструкций в противном случае), а затем сигнализация объекта синхронизации ядра (событие в Windows) и пробуждение потока планировщик потоков ядра в ответ, который затем должен снова получить доступ к вашей структуре данных потокобезопасным способом, чтобы удалить задачу - это безумный объем работы.

Многопоточность в целом добавляет гораздо больше работы ЦП (и авторам библиотек), выгода заключается в том, что работа может выполняться в других потоках, позволяя вашему потоку реагировать на события графического интерфейса вместо зависания. По этой причине вы хотите, чтобы накладные расходы были на несколько порядков меньше, чем объем работы, которую вы ставите в очередь, и это не так для вас — все, что у вас есть, — это несколько SIMD-инструкций.

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

person Blindy    schedule 21.04.2020
comment
Причина, по которой я хотел использовать многопоточный цикл, заключается в том, что он будет перебирать все объекты и обновлять их параллельно, одновременно, а не все сразу. я также делаю это, чтобы узнать больше о многопоточности. - person Ethan Hofton; 21.04.2020
comment
Я понимаю, но, как я уже сказал, накладные расходы на постановку и удаление этих вещей в очередь потокобезопасным способом выше, чем объем работы, которую вы ставите в очередь в первую очередь. Объединяйте свою работу в большие объемы. - person Blindy; 21.04.2020
comment
Было бы лучше сделать это с помощью вычислительных шейдеров или это слишком много работы? - person Ethan Hofton; 21.04.2020
comment
Или слишком мало работы, вы имеете в виду? Потому что на самом деле перевод и сложение векторов — это буквально ничто! - person Blindy; 21.04.2020
comment
При обновлении позиции он должен обновить матрицу модели объекта, а затем использовать матрицу модели для пересчета положения фигуры в мире, и я не могу сделать это в вершинном шейдере, поскольку я выполняю пакетную визуализацию. Возможно, мне стоит подумать о переносе вычисления матрицы на GPU с помощью компьютерного шейдера? - person Ethan Hofton; 21.04.2020
comment
Теперь я понимаю вашу точку зрения, узкое место было не в этой функции, и использование многопоточности - просто пустая трата времени. Любые идеи о том, как ускорить вычисления матрицы и мировых координат? - person Ethan Hofton; 21.04.2020
comment
SIMD - это ответ, либо на ЦП (что, я считаю, glm должен делать, если вы включите SIMD на уровне компилятора), либо на графическом процессоре, передавая матрицы как они есть и умножая в шейдере. - person Blindy; 21.04.2020
comment
Я посмотрю на SIMID и насчет передачи матриц, если у меня есть много объектов для рендеринга, не вызовет ли это большое узкое место, если я передаю несколько матриц для большого количества объектов? - person Ethan Hofton; 21.04.2020
comment
Это зависит. Матрицы конечно лучше вообще не присылать, но если они у вас есть, то нужно где-то вычислить их умножение. - person Blindy; 21.04.2020