Volatile не работает должным образом

рассмотрим этот код:

struct A{ 
  volatile int x;
  A() : x(12){
  }
};

A foo(){
  A ret;
  //Do stuff
  return ret;
}

int main()
{
  A a;
  a.x = 13;
  a = foo();
}

используя g++ -std=c++14 -pedantic -O3 я получаю эту сборку:

foo():
        movl    , %eax
        ret
main:
        xorl    %eax, %eax
        ret

по моей оценке переменная x должно быть написано, по крайней мере, три раза (возможно, четыре), но это даже не написано после (функция foo даже не вызывается!)

еще хуже, когда вы добавить inline ключевое слово foo вот результат:

main:
        xorl    %eax, %eax
        ret

Я думал, что volatile означает, что каждый прочитанный или написать должны произойдет, даже если компилятор не может видеть точку чтения/записи.

что здесь происходит?

обновление:

сдачи декларации A a; снаружи main вот так:

A a;
int main()
{  
  a.x = 13;
  a = foo();
}

генерирует этот код:

foo():
        movl    , %eax
        ret
main:
        movl    , a(%rip)
        xorl    %eax, %eax
        movl    , a(%rip)
        ret
        movl    , a(%rip)
        ret
a:
        .zero   4

что ближе к тому, что вы ожидали бы....Я еще больше запутался, чем когда-либо

2 ответов


Visual C++ 2015 не оптимизирует назначения:

A a;
mov         dword ptr [rsp+8],0Ch  <-- write 1
a.x = 13;
mov         dword ptr [a],0Dh      <-- write2
a = foo();
mov         dword ptr [a],0Ch      <-- write3
mov         eax,dword ptr [rsp+8]  
mov         dword ptr [rsp+8],eax  
mov         eax,dword ptr [rsp+8]  
mov         dword ptr [rsp+8],eax  
}
xor         eax,eax  
ret  

то же самое происходит и с /O2 (Максимальная скорость) и /Ox (полная оптимизация).

летучие записи сохраняются также gcc 3.4.4, используя как -O2, так и-O3

_main:
pushl   %ebp
movl    , %eax
movl    %esp, %ebp
subl    , %esp
andl    $-16, %esp
call    __alloca
call    ___main
movl    , -4(%ebp)  <-- write1
xorl    %eax, %eax
movl    , -4(%ebp)  <-- write2
movl    , -8(%ebp)  <-- write3
leave
ret

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

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

в прошлом (хотя это по общему признанию конкретного Microsoft), https://msdn.microsoft.com/en-us/library/12a04hfd.aspx говорит:

Если элемент структуры помечен как volatile, то volatile распространяется на все структура.

что также указывает на поведение, которое вы видите проблемы компилятора.

наконец, если вы сделаете " a " глобальной переменной, то понятно, что компилятор менее стремится считать ее неиспользуемой и отбросить ее. Глобальные переменные extern по умолчанию, поэтому невозможно сказать, что глобальный " a " не используется, просто посмотрев на основную функцию. Некоторые другие единицы компиляции (.cpp-файл) может использовать его.


страница GCC на летучие доступ дает некоторое представление о том, как это работает:

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

in C standardese:

§5.1.2.3

2 доступ к изменчивому объекту, изменение объекта, изменение файла, или вызов функции, которая выполняет любую из этих операций все сторону эффекты, 11) которые являются изменениями в состоянии среда выполнения. Оценка выражения может производить сторону эффекты. В определенных точках последовательности выполнения точки последовательности, все побочные эффекты предыдущих оценок будут завершены и никакие побочные эффекты последующих оценок не будут иметь проходить. (Резюме пунктов последовательности приводится в приложении С.)

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

[...]

5 минимум на соответствующая реализация являются:

  • в последовательность точки, изменчивые объекты стабильны в том смысле, что предыдущие обращения завершены, а последующие обращения еще не завершены произошло. [...]

я выбрал стандарт C, потому что язык проще, но правила по существу одинаковы в C++. См. правило "как будто".

теперь на моей машине,-O1 не оптимизирует вызов foo(), так что давайте использовать -fdump-tree-optimized посмотреть разница:

-O1

*[definition to foo() omitted]*

;; Function int main() (main, funcdef_no=4, decl_uid=2131, cgraph_uid=4, symbol_order=4) (executed once)

int main() ()
{
  struct A a;

  <bb 2>:
  a.x ={v} 12;
  a.x ={v} 13;
  a = foo ();
  a ={v} {CLOBBER};
  return 0;
} 

и -O3:

*[definition to foo() omitted]*

;; Function int main() (main, funcdef_no=4, decl_uid=2131, cgraph_uid=4, symbol_order=4) (executed once)

int main() ()
{
  struct A ret;
  struct A a;

  <bb 2>:
  a.x ={v} 12;
  a.x ={v} 13;
  ret.x ={v} 12;
  ret ={v} {CLOBBER};
  a ={v} {CLOBBER};
  return 0;
}

gdb показывает в обоих случаях, что a в конечном счете оптимизирован, но мы беспокоимся о foo(). Свалки показывают нам, что GCC переупорядочил доступы так, что foo() даже не нужно и впоследствии весь код в main() оптимизирован подальше. Это правда? Давайте посмотрим выход сборки для -O1:

foo():
        mov     eax, 12
        ret
main:
        call    foo()
        mov     eax, 0
        ret

это, по сути, подтверждает, что Я сказал выше. Все оптимизировано: единственная разница заключается в том, является ли вызов foo() как хорошо.