gcc и cpu_relax, smb_mb и т. д.?

Я читал об оптимизации компилятора по сравнению с оптимизацией ЦП и volatile по сравнению с барьерами памяти.

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

Однако в статье volatile считается вредным подчеркивает, что volatile не следует использовать. В сообщении Линуса содержатся аналогичные утверждения. Основная причина, IIUC, заключается в том, что пометка переменной как volatile отключает все оптимизации компилятора при доступе к этой переменной (т.е. даже если они не вредны), но все же не обеспечивает защиту от переупорядочения памяти. По сути, главное заключается в том, что с осторожностью следует обращаться не с данными, а с конкретным шаблоном доступа.

Теперь статья volatile считается вредной. дает следующий пример занятого цикла, ожидающего флага:

while (my_variable != what_i_want) {}

и подчеркивает, что компилятор может оптимизировать доступ к my_variable так, чтобы он выполнялся только один раз, а не в цикле. Решение, как утверждается в статье, следующее:

while (my_variable != what_i_want)
    cpu_relax();

Говорят, что cpu_relax действует как барьер компилятора (в более ранних версиях статьи говорилось, что это барьер памяти).

У меня есть несколько пробелов здесь:

1) Подразумевается ли, что gcc обладает специальными знаниями о вызове cpu_relax и что он преобразуется в подсказку как компилятору , так и ЦП?

2) То же самое относится и к другим инструкциям, таким как smb_mb() и им подобным?

3) Как это работает, учитывая, что cpu_relax по существу определяется как макрос C? Если я вручную расширю cpu_relax, будет ли gcc по-прежнему уважать его как барьер компилятора? Как я могу узнать, какие вызовы соблюдаются gcc?

4) Какова область действия cpu_relax в отношении gcc? Другими словами, каков объем операций чтения, которые не может оптимизировать gcc, когда он видит инструкцию cpu_relax? С точки зрения ЦП область действия широка (барьеры памяти ставят отметку в буфере чтения или записи). Я предполагаю, что gcc использует меньшую область видимости — возможно, область C?


person YSK    schedule 26.12.2017    source источник


Ответы (2)


  1. Да, gcc обладает специальными знаниями о семантике cpu_relax или чего-то еще, до чего он расширяется, и должен преобразовать его во что-то, для чего аппаратное обеспечение также будет уважать семантику.

  2. Да, любой примитив ограждения памяти требует особого уважения со стороны компилятора и оборудования.

  3. Посмотрите, до чего расширяется макрос, например. скомпилируйте с помощью «gcc -E» и проверьте вывод. Вам придется прочитать документацию компилятора, чтобы узнать семантику примитивов.

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

person Arch D. Robison    schedule 27.12.2017
comment
Спасибо за подробный ответ! Пояснение к пункту 4 (область): в примере, который я привел выше (cpu_relax в цикле занятости), документ, на который я ссылался, утверждает, что это также заставляет gcc рассматривать память как энергозависимую, т. е. не кэшировать ее в регистре. Как я могу узнать, к какой области это относится? Я бы предположил, что это не отключает кэширование регистров во всей функции или единице компиляции, но откуда мне знать? - person YSK; 27.12.2017
comment
Рассматривать его как изменчивое несколько вводит в заблуждение. Все, что требуется от компилятора, — это сохранить значение в памяти до ограничения и перезагрузить его после ограничения. Остальное время хранить значение в регистре можно бесплатно. Кроме того, если компилятор может доказать, что значение никогда не передается между потоками, то он может свободно хранить его в регистре все время. Например, оптимизатор времени компоновки может определить, когда библиотека потоков никогда не используется, и устранить все ограждающие действия. - person Arch D. Robison; 27.12.2017

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

Идея состоит в том, что вы пытаетесь выполнить параллельную операцию, в которой ваша задача ядра (см. исходный код ядра sched.h для struct task_struct) находится в жестком цикле, сравнивая my_variable с локальной переменной до тех пор, пока она не будет изменена другой задачей ядра (или изменена асинхронно). аппаратным устройством!) Это распространенный шаблон в ядре.

  1. Ядро было перенесено на несколько архитектур, и каждая из них имеет определенный набор машинных инструкций для обработки параллелизма. Для x86 cpu_relax сопоставляется с машинной инструкцией PAUSE. Это позволяет ЦП x86 более эффективно запускать спин-блокировку, так что обновление переменной блокировки становится более заметным для вращающегося ЦП. GCC выполнит функцию/макрос, как и любую другую функцию. Если cpu_relax удален из цикла, то gcc МОЖЕТ считать цикл нефункциональным и удалить его. Инструкцию PAUSE см. в руководствах по программному обеспечению Intel X86.

  2. smp_mb — это инструкция ограничения памяти x86, которая очищает кеш памяти. Один процессор может изменить my_variable в своем кеше, но он не будет виден другим процессорам. smp_mb обеспечивает когерентность кэша по требованию. Посмотрите руководства по программному обеспечению Intel X86 для инструкций MFENCE/LFENCE.

Обратите внимание, что smp_mb() очищает кеш ЦП, поэтому это МОЖЕТ быть дорогостоящей операцией. Текущие процессоры Intel имеют огромные кеши (~ 6 МБ).

  1. Если вы расширите cpu_relax на x86, он покажет asm volatile("rep; nop" ::: "memory"). Это НЕ барьер компилятора, а код, который GCC не оптимизирует. См. макрос барьера, который является asm volatile("": : : "memory") для подсказки GCC.

  2. Я не понимаю, что вы подразумеваете под «областью действия cpu_relax». Некоторые возможные идеи: Это машинная инструкция PAUSE, похожая на ADD или MOV. PAUSE повлияет только на текущий ЦП. PAUSE обеспечивает более эффективную согласованность кеша между ЦП.

Я только что еще немного рассмотрел инструкцию PAUSE - дополнительное свойство заключается в том, что она не позволяет ЦП выполнять неупорядоченные спекуляции с памятью при выходе из жесткого цикла / спин-блокировки. Я не понимаю, что ЭТО означает, но я полагаю, что это может кратко указать ложное значение в переменной? Еще много вопросов....

person dturvene    schedule 05.01.2018
comment
Спасибо за ответ! Некоторые дополнения: 2) насколько я понимаю, smp_mb обычно не вызывает очистку кеша - см., например. этот вопрос. В документах Intel в MFENCE/LFENCE также не упоминается очистка кеша. 4) Под областью действия я имел в виду тот факт, что cpu_relax действует как барьер компилятора, не позволяя gcc перемещать вещи. Мне было интересно, какова область действия этого предотвращения: это область блокировки? Объем функций? Или что-то другое? - person YSK; 05.01.2018
comment
@YSK: Ты прав. MFENCE не очищает кеш. Это чисто аппаратный барьер, который, по-видимому, гарантирует, что все операции загрузки/сохранения будут завершены (глобально видимы) до барьера. Но это подразумевает (для меня) сквозную запись в локальный кеш для операций хранения. - person dturvene; 06.01.2018
comment
Что касается cpu_relax, я думаю, что понимаю, о чем вы спрашиваете, и у меня нет однозначного ответа. Я НАДЕЮСЬ, что gcc -O4 не изменит поведение, которое вы закодировали, агрессивно, но я недостаточно знаю об алгоритмах оптимизации. Я знаю, что в вашем конкретном примере выше компилятор удалит пустой цикл while, но не удалит его при вызове операции, а cpu_relax работает быстро (но барьер также будет работать). atomic, где вашего цикла сравнения, вероятно, нет. Удачи! - person dturvene; 06.01.2018
comment
Насколько я понимаю, барьерные инструкции обычно относятся к буферам записи и спекулятивным чтениям (например, недавняя атака Meltdown). Я считаю, что кеши всегда когерентны, поэтому сброс в основную память не требуется. Однако я видел некоторые ссылки на задержки в обновлениях когерентности кеша, которые я действительно не понимаю... - person YSK; 06.01.2018
comment
Хорошие моменты. Скажу, что я написал высокоскоростной драйвер разделяемой памяти и мне нужно было использовать MFENCE и LFENCE для обработки изменений в переменной блокировки разделяемой памяти. Время от времени одна задача изменяла его, а затем другая не обнаруживала изменения и также не устанавливала его, поэтому обе задачи записывали в общую область памяти, вызывая коллизии. Заборы решили проблему. Я также установил для общей памяти пометку _uc без кэширования, и это, похоже, сработало, но было заметно медленнее. - person dturvene; 07.01.2018
comment
Недавно я наткнулся и начал работать над Документация ядра Linux по барьерам памяти. Он охватывает почти все, что мы обсуждали, и даже кое-что. Это долго читать, но это хорошо. - person YSK; 07.01.2018
comment
Да, это хорошая отправная точка. Если вы углубитесь в кодирование, вам понадобятся руководства для разработчиков программного обеспечения x86. Посмотрите Том 3, глава 4, посвященная пейджингу, и глава 8, посвященная управлению процессами. - person dturvene; 08.01.2018