Если вы хотите немного больше узнать о том, что делает ключевое слово volatile, рассмотрите следующую программу (я использую DevStudio 2005):
#include <iostream>
void main()
{
int j = 0;
for (int i = 0 ; i < 100 ; ++i)
{
j += i;
}
for (volatile int i = 0 ; i < 100 ; ++i)
{
j += i;
}
std::cout << j;
}
Используя стандартные оптимизированные (выпускные) параметры компилятора, компилятор создает следующий ассемблер (IA32):
void main()
{
00401000 push ecx
int j = 0;
00401001 xor ecx,ecx
for (int i = 0 ; i < 100 ; ++i)
00401003 xor eax,eax
00401005 mov edx,1
0040100A lea ebx,[ebx]
{
j += i;
00401010 add ecx,eax
00401012 add eax,edx
00401014 cmp eax,64h
00401017 jl main+10h (401010h)
}
for (volatile int i = 0 ; i < 100 ; ++i)
00401019 mov dword ptr [esp],0
00401020 mov eax,dword ptr [esp]
00401023 cmp eax,64h
00401026 jge main+3Eh (40103Eh)
00401028 jmp main+30h (401030h)
0040102A lea ebx,[ebx]
{
j += i;
00401030 add ecx,dword ptr [esp]
00401033 add dword ptr [esp],edx
00401036 mov eax,dword ptr [esp]
00401039 cmp eax,64h
0040103C jl main+30h (401030h)
}
std::cout << j;
0040103E push ecx
0040103F mov ecx,dword ptr [__imp_std::cout (40203Ch)]
00401045 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (402038h)]
}
0040104B xor eax,eax
0040104D pop ecx
0040104E ret
Глядя на вывод, компилятор решил использовать регистр ecx для хранения значения переменной j. Для энергонезависимого цикла (первого) компилятор назначил i в регистр eax. Довольно просто. Однако есть пара интересных битов - инструкция lea ebx, [ebx] фактически является многобайтовой nop-инструкцией, поэтому цикл переходит к 16-байтовому выровненному адресу памяти. Другое - использование edx для увеличения счетчика цикла вместо использования команды inc eax. Команда add reg, reg имеет меньшую задержку на нескольких ядрах IA32 по сравнению с инструкцией inc reg, но никогда не имеет более высокой задержки.
Теперь для цикла со счетчиком энергозависимых циклов. Счетчик хранится в [esp], а ключевое слово volatile сообщает компилятору, что значение всегда должно считываться / записываться в память и никогда не присваиваться регистру. Компилятор даже заходит так далеко, что не выполняет загрузку / приращение / сохранение как три отдельных шага (загрузка eax, inc eax, save eax) при обновлении значения счетчика, вместо этого память напрямую изменяется в одной инструкции (добавление памяти). , р). То, как был создан код, гарантирует, что значение счетчика цикла всегда актуально в контексте одного ядра ЦП. Никакая операция с данными не может привести к повреждению или потере данных (следовательно, не используется load / inc / store, поскольку значение может измениться во время inc, таким образом, теряясь в хранилище). Так как прерывания могут обслуживаться только после завершения текущей инструкции, данные никогда не могут быть повреждены, даже с невыровненной памятью.
Как только вы введете в систему второй ЦП, ключевое слово volatile не будет защищать от обновления данных одновременно другим ЦП. В приведенном выше примере вам понадобится выровнять данные, чтобы получить потенциальное повреждение. Ключевое слово volatile не предотвратит потенциальное повреждение, если данные не могут быть обработаны атомарно, например, если счетчик цикла имел тип long long (64 бита), то для обновления значения потребовались бы две 32-битные операции, в середине которое может произойти прерывание и изменить данные.
Таким образом, ключевое слово volatile подходит только для выровненных данных, размер которых меньше или равен размеру собственных регистров, так что операции всегда являются атомарными.
Ключевое слово volatile было задумано для использования с операциями ввода-вывода, в которых ввод-вывод постоянно менялся, но имел постоянный адрес, например устройство UART с отображением в памяти, и компилятору не следует продолжать повторное использование первого значения, считанного с адреса. .
Если вы обрабатываете большие данные или имеете несколько процессоров, вам потребуется система блокировки более высокого уровня (ОС) для правильной обработки доступа к данным.