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

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    $12, %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    $12, %eax
        ret
main:
        movl    $13, a(%rip)
        xorl    %eax, %eax
        movl    $12, a(%rip)
        ret
        movl    $12, a(%rip)
        ret
a:
        .zero   4

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

9
DarthRubik 7 Май 2016 в 04:38

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    $16, %eax
movl    %esp, %ebp
subl    $8, %esp
andl    $-16, %esp
call    __alloca
call    ___main
movl    $12, -4(%ebp)  <-- write1
xorl    %eax, %eax
movl    $13, -4(%ebp)  <-- write2
movl    $12, -8(%ebp)  <-- write3
leave
ret

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

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

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

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

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

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

2
Sami Sallinen 7 Май 2016 в 16:42

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

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

На стандартном языке C:

§5.1.2.3

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

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().

0
uh oh somebody needs a pupper 7 Май 2016 в 06:48