2016-06-27 4 views
4

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

  1. // both a and b are non-volatile ints 
    a = 123; 
    b = 456; 
    
  2. // both a and b are non-volatile ints 
    a = rand(); 
    b = rand(); 
    
  3. cout << "foo" << endl; 
    cout << "bar" << endl; 
    

Из того, что я понимаю, заявления в (1) может переупорядочиваться компиляторами, а те, что указаны в (2) (3), не могут, поскольку это изменит наблюдаемое поведение программы.

Но как делать компиляторы знают, когда вещи не могут быть перераспределены, когда они не так «очевидно» Зависимые (высказывания типа ++a; b = a * 2;, очевидно, зависит), как и в (2) (3)? Например, возможно, некоторые вещи, такие как вызов функции constexpr, будут препятствовать переупорядочению ...?

+0

Не зная наверняка, мое чувство кишки будет заключаться в том, что зависимость между 2 и 3 будет легко наблюдаться компилятором. Это связано с тем, что 'rand()' использует математическую формулу для генерации следующего случайного значения в последовательности. Таким образом, существует побочный эффект для 'a = rand()', где некоторое внутреннее состояние обновляется, от которого зависит 'b = rand()', и, следовательно, никакого переупорядочения. Аналогично, 'cout', скорее всего, окажется в системном вызове, который не может быть переупорядочен. –

ответ

8

Единственное стандартное правило, которое регулирует переупорядочение (и в целом оптимизацию), является правилом «как если», что означает, что компилятор может делать все, что ему нравится, если он определяет, что конечный результат тот же, а стандарт выиграл Не мешай. Однако, когда мы доходим до этого уровня, компилятор, вероятно, больше не работает на исходном уровне 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]], если он думает, что это приведет к увеличению производительности.

1

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

Если вы не уверены, не можете изменить его. Так что проблем нет.

+0

Существует ли определение в стандарте для «явно» не зависящего? –

+3

доказуемо. Кто бы ни заковал заявление, должен убедиться, что он поймал все случаи. В противном случае он создает компилятор ошибок, и он не будет долго жить на рынке. – Aganju

+1

@ZizhengTai: Определение на английском языке. Это похоже на то, что мне разрешено выходить на круговое движение, когда «очевидно» нет машин - вам не нужно определение термина, чтобы знать, что это значит, и знать, что произойдет, когда окажется, что я не был В конце концов, я уверен. То, как компилятор реализует это, чрезвычайно сложно и выходит за рамки вопроса о переполнении стека. Тем не менее, вы можете зарегистрироваться на четырехлетнем PhD в компиляторах, чтобы узнать :) –

0

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

Смежные вопросы