Не позволяют ли точки последовательности предотвращать переупорядочение кода по границам критического раздела?
Предположим, что у кого-то есть код на основе блокировки, такой как следующий, где мьютексы используются для защиты от ненадлежащего одновременного чтения и записи
mutex.get() ; // get a lock.
T localVar = pSharedMem->v ; // read something
pSharedMem->w = blah ; // write something.
pSharedMem->z++ ; // read and write something.
mutex.release() ; // release the lock.
Если предположить, что сгенерированный код был создан в программном порядке, все еще существует требование для соответствующих барьеров аппаратной памяти, таких как isync, lwsync,.acq,.rel. Я возьму на этот вопрос, что реализация мьютекса позаботится об этой части, предоставив гарантию, что pSharedMem читает и записывает все, происходит "после" get и "before" release() [но окружающие чтения и записи могут попасть в критический раздел, как я ожидаю, является нормой для реализации мьютексов]. Я также предполагаю, что волатильный доступ используется в реализации мьютекса, где это необходимо, но этот volatile НЕ используется для данных, защищенных мьютексом (понимание того, почему volatile не является требованием для защищенных мьютексом данных, действительно является частью этот вопрос).
Я хотел бы понять, что мешает компилятору перемещать доступ pSharedMem за пределы критической области. В стандартах C и С++ я вижу, что существует концепция последовательности. Большая часть текста точки последовательности в стандартах docs я нашел непонятным, но если бы я должен был догадаться, о чем речь, это утверждение о том, что код не должен переупорядочиваться через точку, где есть вызов с неизвестными побочными эффектами. Разве это его суть? Если это так, то какая свобода оптимизации имеет здесь компилятор?
С компиляторами, выполняющими сложные оптимизации, такие как межпроцессорная вставка с профилем (даже через границы файлов), даже концепция неизвестного побочного эффекта становится размытой.
Возможно, это не просто вопрос простого объяснения этого в автономном режиме, поэтому я открыт для ссылки на ссылки (предпочтительнее онлайн и нацелен на смертных программистов, а не разработчиков компиляторов и разработчиков языков).
EDIT: (в ответ на ответ Jalf)
Я упомянул инструкции по защите памяти, такие как lwsync и isync из-за проблем с переупорядочиванием процессора, о которых вы также упоминали. Я, случается, работает в той же лаборатории, что и ребята-компиляторы (по крайней мере, для одной из наших платформ), и, поговорив с разработчиками встроенных функций, я знаю, что по крайней мере для компилятора xlC __isync() и __lwsync() ( и остальные атомные свойства) также являются барьером переупорядочения кода. В нашей реализации spinlock это видно компилятору, так как эта часть нашего критического раздела встроена.
Однако предположим, что вы не использовали специальную реализацию блокировки сборки (как, например, мы, вероятно, не редкость), а просто называем общий интерфейс, такой как pthread_mutex_lock(). Там компилятор не сообщил ничего больше, чем прототип. Я никогда не видел, чтобы он предположил, что код будет нефункциональным
pthread_mutex_lock( &m ) ;
pSharedMem->someNonVolatileVar++ ;
pthread_mutex_unlock( &m ) ;
pthread_mutex_lock( &m ) ;
pSharedMem->someNonVolatileVar++ ;
pthread_mutex_unlock( &m ) ;
будет нефункциональным, если переменная не будет изменена на летучую. У этого приращения будет последовательность загрузки/увеличения/сохранения в каждом из обратных блоков кода и не будет работать корректно, если значение первого приращения будет сохранено в регистре для второго.
Похоже, что неизвестные побочные эффекты pthread_mutex_lock() - это то, что защищает этот пример назад от обратного приращения от неправильного поведения.
Я говорю о том, что семантика такой последовательности кода в потоковой среде на самом деле не строго покрывается спецификациями языка C или С++.
Ответы
Ответ 1
Короче говоря, компилятору разрешено изменять порядок или преобразовывать программу по своему усмотрению, если наблюдаемое поведение на виртуальной машине С++ не изменяется. Стандарт С++ не имеет понятия потоков, и поэтому эта фиктивная виртуальная машина запускает только один поток. И на такой воображаемой машине нам не нужно беспокоиться о том, что видят другие потоки. Пока изменения не изменяют результат текущего потока, все преобразования кода действительны, включая переупорядочение доступа к памяти через точки последовательности.
понимание того, почему волатильность не является требованием для защищенных мьютексом данных, действительно является частью этого вопроса.
Volatile обеспечивает одно и только одно: чтение из изменчивой переменной будет считываться из памяти каждый раз - компилятор не предполагает, что значение может быть кэшировано в регистре. Точно так же записи будут записаны в память. Компилятор не будет хранить его в регистре "какое-то время, прежде чем записывать его в память".
Но это все. Когда происходит запись, будет выполняться запись, и при чтении будет выполнено чтение. Но это не гарантирует ничего, когда это чтение/запись будет иметь место. Компилятор может, как обычно, выполнять операции переупорядочения по мере его соответствия (если он не изменяет наблюдаемое поведение в текущем потоке, то, о чем знает мнимый С++ CPU). Так что волатильность на самом деле не решает проблему. С другой стороны, он предлагает гарантию, что нам это действительно не нужно. Нам не нужно каждую запись в переменную, которая должна быть выписана немедленно, мы просто хотим убедиться, что они будут записаны до пересечения этой границы. Это нормально, если они кэшированы до тех пор - и аналогично, как только мы пересекли границу критического раздела, последующие записи могут быть снова кэшированы для всех, что нам нужно, - пока мы не перейдем границу в следующий раз. Так volatile предлагает слишком сильную гарантию, которая нам не нужна, но не предлагает того, что нам нужно (что чтение/запись не будет переупорядочено)
Итак, чтобы реализовать критические разделы, нам нужно полагаться на магию компилятора. Мы должны сказать, что "хорошо, забудьте о стандарте С++ на какое-то время, меня не волнует, какие оптимизации он допустил бы, если бы вы строго следовали". Вы не должны переупорядочивать любые обращения к памяти через эту границу ".
Критические разделы обычно реализуются с помощью специальных встроенных компиляторов (в основном специальных функций, которые понимаются компилятором), которые 1) заставляют компилятор избегать переупорядочивания по этому внутреннему значению и 2) заставляют его выдавать необходимые инструкции для получения ЦП для того, чтобы уважать одну и ту же границу (потому что CPU также перезагружает инструкции и без выдачи инструкции по защите памяти, мы рискуем, что CPU сделает то же переупорядочение, что мы просто помешали компилятору сделать)
Ответ 2
Нет, точки последовательности не препятствуют перегруппировке операций. Основным, самым широким правилом, которое регулирует оптимизацию, является требование, наложенное на так называемое наблюдаемое поведение. Наблюдаемое поведение, по определению, является доступом для чтения/записи к переменным volatile
и вызовам функций библиотечного ввода-вывода. Эти события должны происходить в том же порядке и давать те же результаты, что и в "канонической" исполняемой программе. Все остальное может быть перестроено и полностью оптимизировано компилятором любым способом, который он считает нужным, полностью игнорируя любые упорядочения, налагаемые точками последовательности.
Конечно, большинство компиляторов стараются не делать чрезмерно диких перестроек. Однако проблема, о которой вы упоминаете, стала реальной практической проблемой для современных компиляторов в последние годы. Многие реализации предлагают дополнительные механизмы реализации, которые позволяют пользователю просить компилятор не пересекать определенные границы при оптимизации перестроек.
Поскольку, как вы говорите, защищенные данные не объявляются как volatile
, формально говоря, доступ может быть перемещен за пределы защищенной области. Если вы объявляете данные как volatile
, это должно помешать этому событию (предполагая, что доступ к мьютексу также volatile
).
Ответ 3
Посмотрим на следующий пример:
my_pthread_mutex_lock( &m ) ;
someNonVolatileGlobalVar++ ;
my_pthread_mutex_unlock( &m ) ;
Функция my_pthread_mutex_lock() просто вызывает pthread_mutex_lock(). Используя my_pthread_mutex_lock(), я уверен, что компилятор не знает, что это функция синхронизации. Для компилятора это просто функция, и для меня это функция синхронизации, которую я могу легко переопределить.
Поскольку someNonVolatileGlobalVar является глобальным, я ожидал, что компилятор не переместит someNonVolatileGlobalVar ++ за пределы критического раздела. Фактически, из-за наблюдаемого поведения, даже в ситуации с одним потоком, компилятор не знает, будет ли функция до и после этой инструкции изменять глобальный var. Таким образом, чтобы сохранить наблюдаемое поведение правильным, оно должно сохранить порядок выполнения, как написано.
Надеюсь, что pthread_mutex_lock() и pthread_mutex_unlock() также выполняют аппаратные барьеры памяти, чтобы предотвратить аппаратное перемещение этой инструкции вне критической секции.
Я прав?
Если я пишу:
my_pthread_mutex_lock( &m ) ;
someNonVolatileGlobalVar1++ ;
someNonVolatileGlobalVar2++ ;
my_pthread_mutex_unlock( &m ) ;
Я не могу знать, какая из двух переменных увеличивается вначале, но это обычно не проблема.
Теперь, если я пишу:
someGlobalPointer = &someNonVolatileLocalVar;
my_pthread_mutex_lock( &m ) ;
someNonVolatileLocalVar++ ;
my_pthread_mutex_unlock( &m ) ;
или
someLocalPointer = &someNonVolatileGlobalVar;
my_pthread_mutex_lock( &m ) ;
(*someLocalPointer)++ ;
my_pthread_mutex_unlock( &m ) ;
Разве компилятор делает то, что ожидает от разработчиков?
Ответ 4
Точки последовательности C/С++ встречаются, например, когда ';' встречается. В этот момент должны произойти все побочные эффекты всех операций, которые предшествовали ему. Тем не менее, я вполне уверен, что под "побочным эффектом" подразумеваются операции, которые являются частью самого языка (например, z увеличивается в "z ++" ), а не эффекты на более низких/более высоких уровнях (например, что фактически делает ОС в отношении управления памятью, управления потоками и т.д. после завершения операции).
Отвечает ли ваш вопрос на ваш вопрос? Моя точка зрения заключается в том, что AFAIK концепция точек последовательности не имеет ничего общего с побочными эффектами, о которых вы говорите.
HTH
Ответ 5
увидеть что-то в [linux-kernel]/Documentation/memory-barriers.txt