Какие виды оптимизации предотвращает использование volatile в C ++? - PullRequest
25 голосов
/ 31 августа 2010

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

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

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

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

Ответы [ 8 ]

24 голосов
/ 31 августа 2010

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

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

21 голосов
/ 31 августа 2010

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

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

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


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

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

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

К * * 1023

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

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

10 голосов
/ 31 августа 2010

Условные переменные не , где требуется volatile; строго требуется только в драйверах устройств.

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

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

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

4 голосов
/ 31 августа 2010

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

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

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

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

Не обязательно иметь x и y, но их можно просто заменить на:

return 10;

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

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

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

4 голосов
/ 31 августа 2010

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

1 голос
/ 31 августа 2010

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

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

  volatilevar++;

как

  inc [_volatilevar]

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

  volatilevar2 = (volatilevar1++);

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

  mov ax,[_volatilevar1] ; Reads it once
  inc [_volatilevar]     ; Reads it again (oops)
  mov [_volatilevar2],ax

или

  mov ax,[_volatilevar1]
  mov [_volatilevar2],ax ; Writes in wrong sequence
  inc ax
  mov [_volatilevar1],ax

, а скорее

  mov ax,[_volatilevar1]
  mov bx,ax
  inc ax
  mov [_volatilevar1],ax
  mov [_volatilevar2],bx

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

  volatilevar2 = volatilevar1;
  volatilevar1++;

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

1 голос
/ 31 августа 2010

Если вы не находитесь во встроенной системе или не пишете аппаратные драйверы, в которых в качестве средства связи используется отображение памяти, вы должны никогда использовать 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 состоит в том, что область памяти переменной может быть изменена. Вы заставляете компилятор загружать фактическое значение из памяти каждый раз, когда используется переменная; и вы сообщаете компилятору, что повторное использование этого значения в регистре не допускается.

0 голосов
/ 31 августа 2010

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

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

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

x = y*5;

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

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

...