Чем отличается занятый цикл с помощью функции Sleep(0) и паузы?
Я хотел бы дождаться события в моем приложении, которое должно произойти немедленно, поэтому я не хочу ставить свою ветку на ожидание и просыпаться позже. Интересно, в чем разница между использованием Sleep(0)
и инструкция по аппаратной паузе.
Я не вижу никаких различий в использовании процессора для следующей программы. Мой вопрос не о соображениях энергосбережения.
#include <iostream>
using namespace std;
#include <windows.h>
bool t = false;
int main() {
while(t == false)
{
__asm { pause } ;
//Sleep(0);
}
}
2 ответа
Windows Sleep(0) против инструкции PAUSE
Позвольте мне процитировать справочное руководство по оптимизации архитектуры Intel 64 и IA-32.
В многопоточной реализации популярная конструкция в синхронизации потоков и для выдачи квантов планирования другому потоку, ожидающему выполнения своей задачи, состоит в том, чтобы сидеть в цикле и выдавать SLEEP(0).
Они обычно называются "циклами сна" (см. Пример № 1). Следует отметить, что вызов SwitchToThread также может использоваться. "Спящий цикл" распространен в алгоритмах блокировки и пулах потоков, так как потоки ожидают работы.
Эта конструкция, состоящая в том, чтобы сидеть в узком цикле и вызывать сервис Sleep() с параметром 0, на самом деле является циклом опроса с побочными эффектами:
- Каждый вызов Sleep() требует больших затрат на переключение контекста, которое может составлять более 10000 циклов.
- Это также сказывается на стоимости переходов от кольца 3 к кольцу 0, которые могут составлять 1000+ циклов.
- Когда нет другого потока, ожидающего получения контроля, этот цикл ожидания ведет себя к ОС как к очень активной задаче, требующей ресурсов ЦП, не позволяя ОС переводить ЦП в состояние пониженного энергопотребления.
Пример № 1. Неоптимизированная петля сна
while(!acquire_lock())
{ Sleep( 0 ); }
do_work();
release_lock();
Пример № 2. Потребляемая мощность Дружественный цикл сна с использованием PAUSE
if (!acquire_lock())
{ /* Spin on pause max_spin_count times before backing off to sleep */
for(int j = 0; j < max_spin_count; ++j)
{ /* intrinsic for PAUSE instruction*/
_mm_pause();
if (read_volatile_lock())
{
if (acquire_lock()) goto PROTECTED_CODE;
}
}
/* Pause loop didn't work, sleep now */
Sleep(0);
goto ATTEMPT_AGAIN;
}
PROTECTED_CODE:
do_work();
release_lock();
Пример № 2 демонстрирует технику использования инструкции PAUSE, чтобы сделать энергосберегающий цикл бездействия.
Замедляя "ожидание вращения" с помощью инструкции PAUSE, многопоточное программное обеспечение получает:
- Производительность за счет упрощения задач ожидания для более легкого получения ресурсов из занятого ожидания.
- Экономия энергии благодаря использованию меньшего количества частей трубопровода во время вращения.
- Устранение подавляющего большинства ненужно выполненных инструкций, вызванных накладными расходами вызова Sleep (0).
В одном из примеров этого метода этот прирост производительности увеличился в 4,3 раза, что позволило сэкономить 21% энергии на процессоре и 13% экономии энергии на уровне платформы.
Задержка паузы в микроархитектуре Skylake
Инструкция PAUSE обычно используется с программными потоками, выполняющимися на двух логических процессорах, расположенных в одном и том же ядре процессора, ожидая снятия блокировки. Такие короткие циклы ожидания, как правило, длятся от десятков до нескольких сотен циклов, поэтому с точки зрения производительности выгоднее ждать, занимая ЦП, чем уступая ОС. Когда ожидается, что цикл ожидания будет длиться тысячи и более циклов, предпочтительно уступить операционную систему, вызвав одну из функций API синхронизации ОС, например WaitForSingleObject в ОС Windows.
Инструкция PAUSE предназначена для:
- Временно предоставьте одноуровневому логическому процессору (готовому к продвижению вперед, выходящему из цикла вращения) конкурентно распределенные аппаратные ресурсы. Микроархитектурные ресурсы для совместного использования на конкурентной основе, которые логический процессор-брат может использовать в микроархитектуре Skylake, включают: (1) больше слотов внешнего интерфейса в декодировании ICache, LSD и IDQ; (2) Больше слотов исполнения в RS.
- Экономия энергии, потребляемой ядром процессора, по сравнению с выполнением эквивалентной последовательности команд спин-цикла в следующих конфигурациях: (1) один логический процессор неактивен (например, входит в состояние C); (2) Оба логических процессора в одном и том же ядре выполняют инструкцию PAUSE; (3) HT отключен (например, с использованием параметров BIOS).
Задержка инструкции PAUSE в микроархитектуре предыдущего поколения составляет около 10 циклов, тогда как в микроархитектуре Skylake она была расширена до 140 циклов.
Увеличенная задержка (позволяющая более эффективно использовать ресурсы микроархитектуры с конкурентным разделением ресурсов для логического процессора, готового к прогрессу вперед) оказывает небольшое положительное влияние на производительность в 1-2% для приложений с многопоточностью. Ожидается, что это окажет незначительное влияние на менее многопоточные приложения, если продвижение вперед не блокируется при выполнении фиксированного числа зацикленных инструкций PAUSE.
Есть также небольшое преимущество в энергопотреблении в 2-ядерных и 4-ядерных системах. Поскольку задержка PAUSE была значительно увеличена, рабочие нагрузки, чувствительные к задержке PAUSE, будут страдать от некоторой потери производительности.
Вы можете найти более подробную информацию по этому вопросу в "Справочном руководстве по оптимизации архитектур Intel 64 и IA-32" и "Руководстве разработчика программного обеспечения Intel 64 и IA-32" вместе с примерами кода.
Мое мнение
Лучше сделать логику программы такой, чтобы ни Sleep (0), ни инструкция PAUSE никогда не требовались. Другими словами, избегайте циклов "ожидания вращения" вообще. Вместо этого используйте функции синхронизации высокого уровня, такие как WaitForMultipleObjects()
, SetEvent()
, и так далее. Такие высокоуровневые функции синхронизации являются лучшим способом написания программ. Если вы анализируете доступные инструменты (на ваше усмотрение) с точки зрения производительности, эффективности и энергосбережения - функции более высокого уровня являются лучшим выбором. Хотя они также страдают от дорогостоящих переключений контекста и переходов от кольца 3 к кольцу 0, эти расходы нечасты и более чем разумны по сравнению с тем, что вы потратили бы в целом на все объединенные циклы паузы с "ожиданием вращения" или циклы со сном (0).
На процессоре, поддерживающем гиперпоточность, циклы "спин-ожидание" могут занимать значительную часть пропускной способности процессора. Один логический процессор, выполняющий цикл ожидания вращения, может серьезно повлиять на производительность другого логического процессора. Вот почему иногда отключение гиперпоточности может повысить производительность, как указывалось некоторыми людьми.
Постоянный опрос устройств или изменений файлов или состояний в рабочем процессе логики программы может привести к тому, что компьютер будет потреблять больше энергии, создавать нагрузку на память и шину и создавать ненужные сбои страниц (используйте диспетчер задач в Windows, чтобы увидеть, какие приложения производят больше всего) сбои страниц, когда они находятся в состоянии ожидания, ожидая пользовательского ввода в фоновом режиме - это наиболее неэффективные приложения, так как они используют вышеупомянутый опрос). Минимизируйте опрос (включая спин-циклы) всякий раз, когда это возможно, и используйте идеологию, управляемую событиями, и / или структуру, если таковая имеется - это лучшая практика, которую я настоятельно рекомендую. Ваше приложение должно буквально спать все время, ожидая нескольких событий, настроенных заранее.
Хорошим примером приложения, управляемого событиями, является Nginx, изначально написанный для Unix-подобных операционных систем. Поскольку операционные системы предоставляют различные функции и методы для уведомления вашего приложения, используйте эти уведомления вместо опроса об изменении состояния устройства. Просто дайте вашей программе бездействовать до тех пор, пока не придет уведомление или пользовательский ввод. Использование такого метода уменьшает накладные расходы на код для опроса состояния источника данных, потому что код может получать уведомления асинхронно, когда происходят изменения состояния.
Sleep
является системным вызовом, который позволяет ОС перепланировать время ЦП для любого другого процесса, если он доступен, прежде чем разрешить вызывающей стороне продолжить (даже если параметр равен 0).
__asm {pause};
не является портативным
Что ж, Sleep
нет, но не на уровне процессора, а на уровне системных библиотек.