Информация об изменениях

Сообщение Re[17]: Когда это наконец станет defined behavior? от 29.04.2023 8:37

Изменено 29.04.2023 8:51 netch80

Re[17]: Когда это наконец станет defined behavior?
Здравствуйте, T4r4sB, Вы писали:

R>>Я думаю, тебе не помешало бы разобраться с такими понятиями как undefined behavior, unspecified behavior и implementation defined behavior.


TB>То, от чего меняется логика при оптимизации — это как раз undefined behavior.


Таки нет. Undefined — это когда от твоего нарушения может взорваться вообще всё и не только в этом месте. Unspecified — это когда что-то произойдёт на выбор из описанных вариантов, но что именно — на этапе написания зафиксировать нельзя; причём в стандарте все такие случаи имеют локальный эффект (например, порядок вычисления аргументов функции или операндов в a+b — это не повлияет уже на соседний оператор).
И вот такие вещи, как порядок вычисления аргументов функции, от оптимизаций могут меняться весьма значительно.

R>>То есть, порядок выполнения операций (ассоциативность) и порядок вычисления операндов (подвыражений) — это две разные и независимые вещи.


TB>Так вот, почему гцц ссыт убрать лишнее чтение из памяти?


Тут и банально, и нет.

С одной стороны, он не знает, что делает этот bar(), может ли он поменять ту переменную, которая тебе передана по ссылке как a.
Запиши вместо декларации bar(), например: int bar() { return 1; } и увидишь, что он соптимизировал оба чтения в одно: (gcc 9.4.0, -O2, убрал незначащее)

Z3fooRKi:
        movl    (%rdi), %eax
        leal    1(%rax,%rax), %eax
        ret

Видно, что bar() заинлайнилась, а чтение переменной "a" одно на оба случая.

А вот теперь фокус — подставляем барьер памяти в bar():

int bar() {                                                                    
  std::atomic_signal_fence(std::memory_order_acquire);                         
  return 1;                                                                    
}


И код получился вообще потрясающий:

_Z3fooRKi:
        movl    (%rdi), %edx
        movl    (%rdi), %eax
        leal    1(%rdx,%rax), %eax
        ret


Одну и ту же "a" читаем дважды, bar() как бы выполнилось между ними, хотя её результат подставлен после.

Вывод — срабатывал принцип "мы не знаем, что делает bar(), значит, он мог поменять память как угодно".

А вот что меня таки смущает — почему это происходит при том, что в аргументе было "const int& a", а не "int& a". Clang — точно так же. Это значит, что константность для них действует одинаково как "мы не имеем права это менять", но не действует как "это не может поменять кто-то снаружи", несмотря на то, что формально это const.

Вот это, скорее всего, очевидно, если правильно и дословно вкурить стандарт, но я сейчас не осилил вкурить его нужным образом. Тут если кто-то ещё прокомментирует, с разбором конкретных пунктов, будет полезно.
Re[17]: Когда это наконец станет defined behavior?
Здравствуйте, T4r4sB, Вы писали:

R>>Я думаю, тебе не помешало бы разобраться с такими понятиями как undefined behavior, unspecified behavior и implementation defined behavior.


TB>То, от чего меняется логика при оптимизации — это как раз undefined behavior.


Таки нет. Undefined — это когда от твоего нарушения может взорваться вообще всё и не только в этом месте. Unspecified — это когда что-то произойдёт на выбор из описанных вариантов, но что именно — на этапе написания зафиксировать нельзя; причём в стандарте все такие случаи имеют локальный эффект (например, порядок вычисления аргументов функции или операндов в a+b — это не повлияет уже на соседний оператор).
И вот такие вещи, как порядок вычисления аргументов функции, от оптимизаций могут меняться весьма значительно.

R>>То есть, порядок выполнения операций (ассоциативность) и порядок вычисления операндов (подвыражений) — это две разные и независимые вещи.


TB>Так вот, почему гцц ссыт убрать лишнее чтение из памяти?


Тут и банально, и нет.

С одной стороны, он не знает, что делает этот bar(), может ли он поменять ту переменную, которая тебе передана по ссылке как a.
Запиши вместо декларации bar(), например: int bar() { return 1; } и увидишь, что он соптимизировал оба чтения в одно: (gcc 9.4.0, -O2, убрал незначащее)

Z3fooRKi:
        movl    (%rdi), %eax
        leal    1(%rax,%rax), %eax
        ret

Видно, что bar() заинлайнилась, а чтение переменной "a" одно на оба случая.

А вот теперь фокус — подставляем барьер памяти в bar():

int bar() {                                                                    
  std::atomic_signal_fence(std::memory_order_acquire);                         
  return 1;                                                                    
}


И код получился вообще потрясающий:

_Z3fooRKi:
        movl    (%rdi), %edx
        movl    (%rdi), %eax
        leal    1(%rdx,%rax), %eax
        ret


Одну и ту же "a" читаем дважды, bar() как бы выполнилось между ними, хотя её результат подставлен после.

Вывод — срабатывал принцип "мы не знаем, что делает bar(), значит, он мог поменять память как угодно".

А вот что меня таки смущает — почему это происходит при том, что в аргументе было "const int& a", а не "int& a". Clang — точно так же. Это значит, что константность для них действует одинаково как "мы не имеем права это менять", но не действует как "это не может поменять кто-то снаружи", несмотря на то, что формально это const.

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

И, естественно, всё это ещё в контексте предположений о том, что компилятор не может переставить порядок выполнения сложений в этой последовательности. Вот этот момент меня тоже смущает, но, может, его тут уже разрешили (надо перечитать, но рассказ про левоассоциативность сложения может решать этот аспект).