Какие виды оптимизации предотвращает использование volatile в C++?

Я искал ключевое слово volatile и для чего, и ответ, который я получил, был в значительной степени:

Он используется для предотвращения оптимизации компилятором кода.

Было несколько примеров, например, при опросе оборудования с отображенной памятью: без volatile цикл опроса будет удален, так как компилятор может распознать, что значение условия никогда не изменяется. Но так как был только один пример или, может быть, два, это заставило меня задуматься: есть ли другие ситуации, когда нам нужно использовать volatile с точки зрения избежания нежелательной оптимизации? Являются ли переменные условия единственным местом, где volatile нужно?

Я предполагаю, что оптимизация зависит от компилятора и поэтому не указана в спецификации C++. Означает ли это, что мы должны идти в ногу со словами: " Хм, я подозреваю, что мой компилятор покончит с этим, если я не объявлю эту переменную какvolatile или есть какие-то четкие правила?

8 ответов

Решение

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

Что должно вызвать использование volatile это когда значение изменяется, несмотря на тот факт, что ваша программа не записала в него, и когда отсутствуют другие барьеры памяти (например, мьютексы, используемые для многопоточных программ).

Наблюдаемое поведение программы на C++ определяется чтением и записью в изменчивые переменные и любыми вызовами функций ввода / вывода.

Это влечет за собой то, что все операции чтения и записи в изменчивые переменные должны происходить в порядке их появления в коде, и они должны происходить. (Если бы компилятор нарушил одно из этих правил, он нарушил бы правило "как будто".)

Это все. Он используется, когда нужно указать, что чтение или запись переменной следует рассматривать как наблюдаемый эффект. (Обратите внимание, что статья "C++ и опасности двойной проверки блокировок" затрагивает этот вопрос совсем немного.)


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

Это означает, что компилятор изменяет:

int x = 2;
volatile int y = 5;
x = 5;
y = 7;

к

int x = 5;
volatile int y = 5;
y = 7;

Это хорошо, так как значение x не является частью наблюдаемого поведения (оно не является изменчивым). Что не будет хорошо, так это изменение назначения с 5 на назначение на 7, потому что запись 5 является наблюдаемым эффектом.

Переменные условия не где volatile нужно; строго это нужно только в драйверах устройств.

volatile гарантирует, что чтение и запись в объект не будут оптимизированы или переупорядочены относительно другого volatile, Если вы заняты зацикливанием переменной, измененной другим потоком, она должна быть объявлена volatile, Тем не менее, вы не должны заняты петлей. Поскольку язык не был специально разработан для многопоточности, это не очень хорошо поддерживается. Например, компилятор может переместить запись в энергонезависимую переменную от после до до цикла, нарушая блокировку. (Для неопределенных спин-петлей это может произойти только в C++0x.)

Когда вы вызываете функцию библиотеки потоков, она действует как забор памяти, и компилятор будет предполагать, что все значения изменились - по сути, все является изменчивым. Это либо указывается, либо автоматически применяется любой библиотекой потоков для обеспечения плавного вращения колес.

C++ 0x может не иметь этого недостатка, так как он вводит формальную семантику многопоточности. Я не очень знаком с изменениями, но ради обратной совместимости не требуется объявлять ничего нестабильного, чего не было раньше.

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

Это может быть в реестре:

Его значение можно рассчитать, например, в:

int x = 2;
int y = x + 7;
return y + 1;

Не нужно иметь x а также y вообще, но можно было бы просто заменить на:

return 10;

И еще один пример: любой код, который не влияет на состояние извне, можно полностью удалить. Например, если вы обнуляете конфиденциальные данные, компилятор может расценить это как потраченное впустую упражнение ("почему вы пишете то, что не будет прочитано?") И удалить его. изменчивый может быть использован, чтобы остановить это.

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

(Примечание C#. Много я видел в последнее время на volatile предполагает, что люди читают о C++ volatile и применяя его к C#, и читая об этом в C# и применяя его к C++. Правда, volatile ведет себя так по-разному между двумя, что не полезно считать их связанными).

Volatile не пытается сохранить данные в регистре процессора (в 100 раз быстрее, чем в памяти). Он должен читать его из памяти каждый раз, когда его используют.

Один из способов понять изменчивую переменную - представить, что это виртуальное свойство; пишет и даже читает может делать вещи, о которых компилятор не может знать. Фактически сгенерированный код для записи / чтения энергозависимой переменной - это просто запись в память или чтение (*), но компилятор должен считать код непрозрачным; он не может делать никаких предположений, при которых он может быть излишним. Проблема заключается не только в том, чтобы убедиться, что скомпилированный код замечает, что что-то вызвало изменение переменной. В некоторых системах даже чтение из памяти может "делать" вещи.

(*) В некоторых компиляторах изменяемые переменные могут добавляться, вычитаться из, увеличиваться, уменьшаться и т. Д. Как отдельные операции. Для компилятора, вероятно, полезно:

  volatilevar ++;

как

  inc [_volatilevar]

поскольку последняя форма может быть атомарной на многих микропроцессорах (хотя не на современных многоядерных ПК). Важно отметить, однако, что если заявление было:

  volatilevar2 = (volatilevar1 ++);

правильный код не будет:

  мов топор,[_volatilevar1]; Читает это один раз
  inc [_volatilevar]; Читает снова (упс)
  мов [_volatilevar2], топор

ни

  мов топор, [_ volatilevar1]
  мов [_volatilevar2], топор; Пишет в неправильной последовательности
  инк топор
  мов [_volatilevar1], топор

скорее

  мов топор, [_ volatilevar1]
  MOV BX, топор
  инк топор
  мов [_volatilevar1], топор
  mov [_volatilevar2],bx

Написание исходного кода по-другому позволило бы генерировать более эффективный (и, возможно, более безопасный) код. Если "volatilevar1" не возражал против прочтения дважды, а "volatilevar2" не возражал против того, чтобы быть написанным перед volatilevar1, то разделить утверждение на

  volatilevar2 = volatilevar1;
  volatilevar1++;

позволит более быстрый и, возможно, более безопасный код.

Если вы не находитесь во встроенной системе или не пишете аппаратные драйверы, в которых отображение памяти используется в качестве средства связи, вам никогда не следует использовать volatile

Рассматривать:

int main()
{
    volatile int SomeHardwareMemory; //This is a platform specific INT location. 
    for(int idx=0; idx < 56; ++idx)
    {
        printf("%d", SomeHardwareMemory);
    }
}

Должен производить код как:

loadIntoRegister3 56
loadIntoRegister2 "%d"
loopTop:
loadIntoRegister1 <<SOMEHARDWAREMEMORY>
pushRegister2
pushRegister1
call printf
decrementRegister3
ifRegister3LessThan 56 goto loopTop

тогда как без volatile возможно:

loadIntoRegister3 56
loadIntoRegister2 "%d"
loadIntoRegister1 <<SOMEHARDWAREMEMORY>
loopTop:
pushRegister2
pushRegister1
call printf
decrementRegister3
ifRegister3LessThan 56 goto loopTop

Предположение о volatile является то, что место в памяти переменной может быть изменено. Вы заставляете компилятор загружать фактическое значение из памяти каждый раз, когда используется переменная; и вы говорите компилятору, что повторное использование этого значения в регистре не допускается.

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

x = y+y+y+y+y;

может быть преобразован в

x = y*5;

однако, если переменная может быть изменена вне потока, компилятор не имеет полного знания о том, что происходит, просто исследуя этот кусок кода. он больше не может делать оптимизации, как указано выше. (редактировать: в этом случае это возможно; нам нужны более сложные примеры)

по умолчанию для оптимизации производительности предполагается однопоточный доступ. это предположение обычно верно. если программист явно не укажет обратное с volatile ключевое слово.

Другие вопросы по тегам