Единственное стандартное правило, которое регулирует переупорядочение (и в целом оптимизацию), является правилом «как если», что означает, что компилятор может делать все, что ему нравится, если он определяет, что конечный результат тот же, а стандарт выиграл Не мешай. Однако, когда мы доходим до этого уровня, компилятор, вероятно, больше не работает на исходном уровне C++: вероятно, он имеет дело с промежуточной формой, и мышление в операторах может фактически не отражать то, что происходит.
Например, в вашем втором примере:
// both a and b are non-volatile ints
a = rand();
b = rand();
Компилятор можно назвать rand
дважды и сохранить результат второго rand
вызова b
перед сохранением результата первого rand
вызова a
. Другими словами, назначения были переупорядочены, но вызовов нет. Это возможно, потому что компилятор оптимизирует представление вашей программы, которая более зернистая, чем C++.
Различные компиляторы используют различные трюки, чтобы определить, можно ли переупорядочить две инструкции промежуточного представления, но чаще всего вызовы функций являются непреодолимыми барьерами, которые невозможно переупорядочить. Тем не менее, вызовы внешних библиотек (например, rand
) не могут быть проанализированы и, безусловно, не будут переупорядочены, поскольку компилятор предпочтет консервативный, но известный подход.
Фактически, вызовы функций могут быть изменены только в том случае, если компилятор определяет, что они не могут мешать друг другу. Компиляторы пытаются решить эту проблему с помощью анализа псевдонимов. Идея заключается в том, что за пределами зависимости от стоимости (например, в a * b + c
операция +
зависит от результата a * b
, и поэтому a * b
должен произойти первым), упорядочение (почти) имеет значение только тогда, когда вы где-то пишете и читаете назад позже. Это означает, что если вы можете правильно идентифицировать каждую операцию памяти и ее влияние, вы можете определить, могут ли две операции памяти быть переупорядочены или даже полностью исключены. Для этих целей вызов считается большой операцией памяти, которая охватывает все меньшие нагрузки и магазины, которые она делает.
К сожалению, общий случай анализа псевдонимов, как известно, является бесспорным. В то время как компиляторы становятся умнее и умнее, у вас, вероятно, никогда не будет компилятора, который систематически может принять самое правильное решение о переупорядочении вызовов, даже если у вас есть весь исходный код, с которым вы ссылаетесь.
Некоторые компиляторы имеют атрибуты, специфичные для себя, которые определяют, следует ли им учитывать функцию, безопасную для переупорядочения, независимо от их собственного анализа. Например, gcc с радостью переупорядочивает, кэширует или даже устраняет вызовы функций с помощью атрибута [[gnu::pure]]
, если он думает, что это приведет к увеличению производительности.
Не зная наверняка, мое чувство кишки будет заключаться в том, что зависимость между 2 и 3 будет легко наблюдаться компилятором. Это связано с тем, что 'rand()' использует математическую формулу для генерации следующего случайного значения в последовательности. Таким образом, существует побочный эффект для 'a = rand()', где некоторое внутреннее состояние обновляется, от которого зависит 'b = rand()', и, следовательно, никакого переупорядочения. Аналогично, 'cout', скорее всего, окажется в системном вызове, который не может быть переупорядочен. –