Плюсы и минусы занятого ожидания на современных процессорах

0

Я использую ожидание для синхронизации доступа к критическим регионам, например:

while (p1_flag != T_ID);

/* begin: critical section */
for (int i=0; i<N; i++) {
 ... 
}
/* end: critical section */

p1_flag++;

p1_flag - глобальная изменчивая переменная, которая обновляется другим параллельным потоком. На самом деле, у меня есть два критических раздела внутри цикла, и у меня есть два потока (оба выполняются в одном цикле), которые коммутируют выполнение этих критических областей. Например, критические области называются A и B.

Thread 1     Thread 2
   A        
   B            A
   A            B
   B            A
   A            B
   B            A
                B

Параллельный код выполняется быстрее, чем серийный, но не так сильно, как я ожидал. Профилирование параллельной программы с помощью VTune Amplifier я заметил, что в директивах синхронизации тратится большое количество времени, то есть while(...) и обновление флага. Я не уверен, почему я вижу такие большие накладные расходы в этих "инструкциях", так как область A точно такая же, как в регионе B. Моя лучшая догадка заключается в том, что это связано с латентностью когерентности кеша: я использую Intel i7 Ivy Bridge Machine и эта микроархитектура решает когерентность кеша на L3. VTune также сообщает, что команда while (...) потребляет всю пропускную способность интерфейса, но почему?

Чтобы сделать вопрос понятным: почему while(...) и инструкции флага обновления занимают так много времени выполнения? Почему команда while(...) насыщает пропускную способность интерфейса?

  • 0
    Я предполагаю, что два потока борются друг с другом за владение одной и той же строкой кэша. Я не эксперт по аппаратной синхронизации, хотя.
  • 0
    Не существует ложного разделения между переменными. Однако обратите внимание, что для переменной синхронизации существует параллелизм в тот момент, когда оба потока должны коммутировать области.
Теги:
multithreading
parallel-processing
x86
computer-architecture

2 ответа

1

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

Согласованность кеша диктует, что при изменении строки кэша (p1_flag++) вам необходимо иметь право собственности на нее. Это означает, что это приведет к аннулированию любой копии, существующей в других ядрах, ожидая, что она вернет любые изменения, сделанные этим другим ядром, на уровень общего кэша. Затем он предоставит строку запрашивающему ядру в состоянии M и выполнит модификацию.

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

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

Если вы настроены на ожидание занятости, вы должны хотя бы использовать паузу внутри нее: _mm_pause intrisic или просто __asm("pause"). Это послужило бы для того, чтобы дать другому потоку шанс получить блокировку и освободить вас от ожидания, а также снизить нагрузку на процессор в оживленном ожидании (процессор вне порядка будет заполнять все конвейеры параллельными экземплярами этого оживленного ожидания, потребляя много энергии - пауза будет сериализовать его, чтобы только одна итерация могла запускаться в любой момент времени - гораздо меньше потребления и с тем же эффектом).

  • 0
    Привет Лиор. Я полностью согласен с вашими наблюдениями! Я не думал, что строка кеша может пинг-понг между ядрами до того, как она будет обновлена. Насколько вы уверены, что это может произойти? Я подумал об использовании трюка с pause , однако в будущем я хотел бы перенести эту программу на архитектуру ARM - знаете ли вы, есть ли аналогичная инструкция для ARM? Другое дело: основываясь на экспериментах, я предполагаю, что конвейер полон инструкций загрузки, но почему процессор не ждет завершения одной загрузки, прежде чем выдать другую?
  • 0
    Кстати, для x86 вы также можете использовать cpuid или другие сериализационные модули. Я не уверен насчет ARM ISA, но там тоже должны быть инструкции по сериализации. Возможно, попробуйте ISB ( infocenter.arm.com/help/topic/com.arm.doc.dai0179b/… ). Имейте в виду, что я говорю о сериализации на уровне команд, а не об ограждении памяти (необходимо заблокировать эти ветви).
Показать ещё 5 комментариев
0

Занят-ожидание почти никогда не является хорошей идеей в многопоточных приложениях.

Когда вы заняты - ожидание, алгоритмы планирования потоков не будут знать, что ваш цикл ждет в другом потоке, поэтому они должны выделять время, как будто ваш поток выполняет полезную работу. И это занимает процессорное время, чтобы проверять эту переменную больше и больше, и снова и снова, и снова и снова... пока она не будет окончательно "разблокирована" другим потоком. Тем временем ваш другой поток будет вытеснен вашей оживленной нитью снова и снова, без всякой цели.

Это еще хуже, если планировщик основан на приоритете, а поток ожидания ожидания занят более высоким приоритетом. В этой ситуации поток с более низким приоритетом НИКОГДА не будет вытеснять поток с более высоким приоритетом, таким образом, у вас есть ситуация взаимоблокировки.

Вы должны ВСЕГДА использовать семафоры или объекты мьютекса или обмен сообщениями для синхронизации потоков. Я никогда не видел ситуации, когда занятое ожидание было правильным решением.

Когда вы используете семафор или мьютекс, планировщик никогда не должен планировать этот поток до тех пор, пока не будет выпущен семафор или мьютекс. Таким образом, ваша нить никогда не будет уходить от потоков, которые действительно работают.

  • 2
    Я не думаю, что это верно для всех типов систем. Есть сценарии, в которых вы знаете, что ожидание будет очень коротким (несколько циклов), и стоимость перевода потока в состояние ожидания / ожидания запуска планировщика обычно выше, чем просто вращение в течение нескольких циклов.
  • 0
    Бен, спасибо за ответ. Однако дауфик прав. В некоторых случаях лучше ждать несколько циклов, чем платить цену за переключение контекста. Как я сказал в своем вопросе, A и B - это один и тот же код, и я ожидаю, что их время выполнения будет почти одинаковым, и поэтому я не ожидал, что while (...) будет выполняться слишком долго.
Показать ещё 8 комментариев

Ещё вопросы

Сообщество Overcoder
Наверх
Меню