Подтвердить что ты не робот

Не позволяют ли точки последовательности предотвращать переупорядочение кода по границам критического раздела?

Предположим, что у кого-то есть код на основе блокировки, такой как следующий, где мьютексы используются для защиты от ненадлежащего одновременного чтения и записи

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 или С++.

4b9b3361

Ответ 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