Эффективный способ остановить цикл через определенное количество секунд

#c #c 17

#c #c 17

Вопрос:

У меня есть цикл на C , который я хотел бы запустить на несколько секунд. Хотя объем работы на каждой итерации разный, от нескольких микросекунд до секунд, можно останавливаться между итерациями. Это высокопроизводительный код, поэтому я хотел бы избежать вычисления разницы во времени на каждой итерации:

 while (!status.has_value())
{
  // do something

  // this adds extra delays that I would like to avoid
  if (duration_cast<seconds>(system_clock::now() - started).count() >= limit)
    status = CompletedBy::duration;
}
  

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

Кстати, цикл может завершиться до сигнала.

Комментарии:

1. Какую ОС вы используете? Такие функции могут зависеть от системы.

2. @MichaelChourdakis вы уверены, что понимаете, как работает сигнализация, скажем, в системах Unix?

3. Это полностью жизнеспособно. Я бы подключил другой поток и перевернул атомарную переменную volatole после некоторого ожидания. Рабочий поток будет проверять переменную перед каждой итерацией. Или действительно запланировать сигнал и перевернуть переменную sig_atomic_t.

4. Вы профилировали свой код и обнаружили, что duration_cast<seconds>(system_clock::now() - started).count() >= limit) это узкое место? Потому что эта строка не должна занимать больше микросекунды.

5. @MichaelChourdakis Но он упомянул сигналы.

Ответ №1:

Я сделал нечто подобное, но на Java. Общая идея состоит в том, чтобы использовать отдельный поток для управления контрольным значением, придавая вашему циклу вид…

 okayToLoop = true;
// code to launch thread that will wait N milliseconds, and then negate okayToLoop
while((!status.hasValue()) AND (okayToLoop)) {
    // loop code
}
  

«Предостерегающее замечание» заключается в том, что многие функции sleep () для потоков используют семантику «sleep по крайней мере», поэтому, если действительно важно, чтобы только спали N миллисекунд, вам нужно будет учесть это в вашей реализации потока. Но это позволяет избежать постоянной проверки продолжительности каждой итерации цикла.

Обратите внимание, что это также позволит завершить текущую итерацию цикла до проверки контрольного значения. Я также реализовал этот подход, при котором «поток управления» фактически прерывает поток, в котором выполняется цикл, прерывая итерацию. Когда я сделал это, я фактически поместил цикл в рабочий поток.

Комментарии:

1. Это потребовало бы, чтобы каждый доступ к okayToLoop вызывал точку синхронизации, что привело бы к отключению многих оптимизаций компилятора, что, вероятно, повредит производительности НАМНОГО больше, чем то, что уже делает OP.

2. Хорошо, я соглашусь с тем, что подход okayToLoop имеет свои проблемы и, как и в большинстве случаев, является компромиссом. Как насчет подхода, основанного на прерываниях? Само прерывание может быть дорогостоящим (я никогда не был в сценарии, где выполнение было критичным для миллисекунды), но этот подход устраняет синхронизированный доступ в okayToLoop (кстати, синхронизация должна быть необходима только в том случае, если есть штраф за дополнительную итерацию цикла, нет?).

3. Подход interupt хорош в чистом C. Но в C для этого потребовались бы таблицы размотки с нулевой стоимостью на уровне инструкций, которые, я думаю, не предоставляет ни один современный компилятор. Если вы готовы сыграть в игру с неопределенным поведением и / или поклясться, что внутри цикла будут задействованы только тривиальные типы, возможно, вам это сойдет с рук. Даже в этом случае компилятор будет иметь полное право все сломать, поскольку вы находитесь на территории UB.

Ответ №2:

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

Теперь steady_clock::now() цикл может быть слишком медленным.

Используя API, специфичные для ОС, привяжите свой поток к смехотворному приоритету и привязке к определенному процессору. Или используйте rdtsc, приняв во внимание все, что может пойти не так. Рассчитайте, какое значение вы ожидаете получить, если (а) что-то пошло не так или (б) вы превысили пороговое значение по времени.

Когда это произойдет, проверьте steady_clock::now() , достаточно ли вы близки к завершению, и если да, то завершите. Если нет, вычислите новый целевой тактовый сигнал высокой производительности и повторите цикл.

Комментарии:

1. system_clock в любом случае, это неправильная идея, потому что ее можно изменить ; вам нужно как минимум steady_clock .

2. Любая форма межпотоковой связи будет намного медленнее — существуют формы межпотоковой связи, которые выглядят как x.load(std::memory_order_relaxed) . Это всего лишь простая mov инструкция для x86_64.

3. @MaximEgorushkin Упрощенный порядок памяти не требует, чтобы вы действительно получали значение до того, как солнце превратится в красного гиганта, который я проверял в последний раз. В основном говорится, что если значение установлено равным 7, а затем 10, вы не будете читать 10, а затем 7. Я полагаю, если вы знаете, что используете AMD-64 и знаете, что это реализовано как mov, единственной возможной проблемой является согласованность кэша, и вы знаете, что операция записи очистит кэш другого процессора.

4. @Yakk-AdamNevraumont Верно, но это все равно не делает ваше утверждение в первом предложении правильным.

5. @MaximEgorushkin Я не считаю код, который на самом деле ничего не передает, коммуникационным. Я имею в виду, что он мог в конечном итоге что-то сообщить, но для того, чтобы заставить его действительно что-то сообщить, вам придется сделать что-то еще… В любом случае, напишите ответ! Может быть более полезным для OP.