TB>>> Оператор + левоассоциативный
σ>>При чём тут это?
TB>При том что второе сложение обязано выполниться после первого
Что в стандарте это подтверждает?
TB>>> второе сложение в любом случае выполнится после первого.
σ>>А чтения из a — в любом порядке. Оба до вызова bar, оба после, или с вызовом между.
TB>И что же гцц даже с о3 зассал один раз прочитать а и удвоить в регистре?
ХЗ. Missing optimization? MSVC вроде не ссыт https://godbolt.org/z/v8TzGK13K
TB>Ты не видишь что от порядка чтений меняется результат?
И?
Re[10]: Когда это наконец станет defined behavior?
σ>>Что в стандарте это подтверждает?
TB>Выражение в теле функции однозначно парсится как
TB>Operator+(a,operator+(bar(), a))
Допустим. Это как-то противоречит > чтения из a — в любом порядке. Оба до вызова bar, оба после, или с вызовом между.
?
σ>>ХЗ. Missing optimization? MSVC вроде не ссыт https://godbolt.org/z/v8TzGK13K
TB>Ок, плохой пример, bar()+a неоднозначно.
TB>
TB>int foo(const int& a) {
TB> int ll = a;
TB> int l = ll + bar();
TB> return l + a;
TB>}
TB>
TB>Хотя ret 0 это жесть
А сколько должно быть
TB>>>Ты не видишь что от порядка чтений меняется результат?
σ>>И?
TB>Ну уб получается.
Почему?
Re[12]: Когда это наконец станет defined behavior?
TB>>>Потому что от желания оптимизатора может поменяться результат функции
σ>>Как-то мало для UB
TB>Лол это и называется уб
Я думаю, тебе не помешало бы разобраться с такими понятиями как undefined behavior, unspecified behavior и implementation defined behavior. По иронии судьбы первые два пункта в этом списке имеют одинаковую аббревиатуру (UB), но при этом очень разный смысл. Можно почитать здесь: https://en.cppreference.com/w/cpp/language/ub.
Вдобавок в параграфе с описанием unspecified behavior есть еще очень полезная ссылка — Order of evaluation, имеющая непосредственное отношение к обсуждаемому вопросу:
There is no concept of left-to-right or right-to-left evaluation in C++. This is not to be confused with left-to-right and right-to-left associativity of operators: the expression a() + b() + c() is parsed as (a() + b()) + c() due to left-to-right associativity of operator+, but c() may be evaluated first, last, or between a() or b() at run time
То есть, порядок выполнения операций (ассоциативность) и порядок вычисления операндов (подвыражений) — это две разные и независимые вещи. Порядок вычисления операндов регулируется отношениями "sequenced before/after", но он определен далеко не для всех случаев. Во многих случаях этот порядок отдан на откуп разработчикам компиляторов и относится к UNSPECIFIED behavior. И это совсем не то же самое, что UNDEFINED behavior. И аббревиатура UB, сколько мне попадалось, всегда употребляется только в смысле undefined behavior.
--
Не можешь достичь желаемого — пожелай достигнутого.
Здравствуйте, rg45, Вы писали:
TB>>Лол это и называется уб
R>Я думаю, тебе не помешало бы разобраться с такими понятиями как undefined behavior, unspecified behavior и implementation defined behavior.
То, от чего меняется логика при оптимизации — это как раз undefined behavior.
Что и написано по твоей ссылке, целый раздел есть "UB and optimization".
R>То есть, порядок выполнения операций (ассоциативность) и порядок вычисления операндов (подвыражений) — это две разные и независимые вещи.
Да, мой изначальный пример был плох, потому что зависел от порядка вычисления операндов.
Я уже исправил.
int bar();
int foo(const int& a) {
int ll = a;
int l = ll + bar();
return l + a;
}
Так вот, почему гцц ссыт убрать лишнее чтение из памяти?
Здравствуйте, T4r4sB, Вы писали:
TB>То, от чего меняется логика при оптимизации — это как раз undefined behavior. TB>Что и написано по твоей ссылке, целый раздел есть "UB and optimization".
Я не очень понимаю, для чего тебе понадобилось пeрефразировать собственные высказывания, но перед этим ты утверждал буквально следующее:
TB>Потому что от желания оптимизатора может поменяться результат функции
TB>Лол это и называется уб
Так вот это не верно. Может поменяться результат вызова функции и это не обязательно будет UB (undefined behavior).
Пример:
int a = 42;
int b = foo(a, a += 1);
Результат может быть разным. Но это проявление unspecified behavior, а не undefined.
--
Не можешь достичь желаемого — пожелай достигнутого.
Здравствуйте, 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;
}
Одну и ту же "a" читаем дважды, bar() как бы выполнилось между ними, хотя её результат подставлен после.
Вывод — срабатывал принцип "мы не знаем, что делает bar(), значит, он мог поменять память как угодно".
А вот что меня таки смущает — почему это происходит при том, что в аргументе было "const int& a", а не "int& a". Clang — точно так же. Это значит, что константность для них действует одинаково как "мы не имеем права это менять", но не действует как "это не может поменять кто-то снаружи", несмотря на то, что формально это const.
Вот это, скорее всего, очевидно, если правильно и дословно вкурить стандарт, но я сейчас не осилил вкурить его нужным образом. Тут если кто-то ещё прокомментирует, с разбором конкретных пунктов, будет полезно.
И, естественно, всё это ещё в контексте предположений о том, что компилятор не может переставить порядок выполнения сложений в этой последовательности. Вот этот момент меня тоже смущает, но, может, его тут уже разрешили (надо перечитать, но рассказ про левоассоциативность сложения может решать этот аспект).
Здравствуйте, kov_serg, Вы писали:
IM>>>Однако я не нашёл в стандарте C++20 упоминания, что reinterpret_cast может начать lifetime объекта. Хотя malloc и memcpy теперь легальны с этой точки зрения, т.е. этот пропозал был включён в стандарт, но обошли reinterpret_cast.
Z>>Вроде для этого предложили новую функцию std::start_lifetime_as ,
_>Надо больше обрядов. Что это за религия если мало странных обрядов
Ну почему обряд... в этом есть своя логика, если думать о том, как реализуется контроль алиасинга (на всех уровнях).
Обрядом здесь скорее является желание активизировать его в любом случае, а кто не спрятался — сам виноват. Я по умолчанию бы предпочёл видеть все операции с памятью как неявные барьеры, а кому от этого медленно — вот там уже помечать нужные куски кода всеми подобными средствами.
_>ps: пара уже добавлять std::bless<function_name>
Здравствуйте, netch80, Вы писали:
N>Таки нет. Undefined — это когда от твоего нарушения может взорваться вообще всё и не только в этом месте.
Я говорю о том, как оно работает на этапе компиляции в реальности.
N>А вот что меня таки смущает — почему это происходит при том, что в аргументе было "const int& a", а не "int& a".
Ага, и я о том же.
Вот тут я даже не знаю, чагойта стандартизаторы испугалися
Re[18]: Когда это наконец станет defined behavior?
TB>int bar();
TB>int foo(const int& a) {
TB> int ll = a;
TB> int l = ll + bar();
TB> return l + a;
TB>}
TB>
N>А вот что меня таки смущает — почему это происходит при том, что в аргументе было "const int& a", а не "int& a". Clang — точно так же. Это значит, что константность для них действует одинаково как "мы не имеем права это менять", но не действует как "это не может поменять кто-то снаружи", несмотря на то, что формально это const.
Так ясно почему — константность ссылки не гарантирует константности объекта. Один и тот же объект можно привязать одновременно к множеству разных ссылок, как константных, так и неконстантных. А функция bar может может иметь непостредственный доступ к объекту, помимо той ссылки, которая передается в функцию foo:
Здравствуйте, rg45, Вы писали:
R>А функция bar может может иметь непостредственный доступ к объекту, помимо той ссылки, которая передается в функцию foo:
Надо стандартизаторам идею подкинуть: а почему б такую ситуацию тоже не назвать UB? Типа передали константную ссылку — значит компилятор вправе предполагать что содержимое не меняется! Это ж сколько можно долей процента выиграть в реальных приложениях! А сколько будет новых непонятных падений!
Здравствуйте, T4r4sB, Вы писали:
TB>Надо стандартизаторам идею подкинуть: а почему б такую ситуацию тоже не назвать UB? Типа передали константную ссылку — значит компилятор вправе предполагать что содержимое не меняется! Это ж сколько можно долей процента выиграть в реальных приложениях! А сколько будет новых непонятных падений!
Другими словами, ты предлагаешь запретить привязывать константные ссылки к неконстантным объектам? Сомнительная идея как по мне. Передали константную ссылку — значит, тот участок логики, которому передали ссылку, может только читать данные, но не имеет права менять их. Например, это процедура вывода данных на экран или в файл — ее дело только вывод данных. Но это ж не значит, что эти данные не может менять вообще нигде и никто. Таким образом ссылки выступают как средство разграничения прав доступа — кто-то может и читать, и писать, а кто-то только читать. И это очень даже полезная возможность.
--
Не можешь достичь желаемого — пожелай достигнутого.
Здравствуйте, T4r4sB, Вы писали:
TB>Типа передали константную ссылку — значит компилятор вправе предполагать что содержимое не меняется!
Если объект создается константным — вправе, и вполне себе предполагает (например, если объект статический, и есть возможность разместить его в RO-секции). А если откуда-то просто приходит ссылка на уже существующий объект, то весь смысл const заключается лишь в запрете его изменения через данную ссылку.
Здравствуйте, T4r4sB, Вы писали: N>> Я по умолчанию бы предпочёл видеть все операции с памятью как неявные барьеры TB>То есть в цикле TB>
TB>while (*dst++ = *src++);
TB>
TB>Значит каждый раз не только читать содержимое указателей src,dst, но и каждый раз читать содержимое куска памяти, где лежат значения этих указателей?
Так... я не знаю, как ответить, потому что твой вопрос настолько нелепо звучит, что или я его вообще не понял, или ты не понял, что тут происходит.
По пунктам:
1. Если src и dst это локальные переменные функции, и никто не берёт их адрес для каких-то целей (или делает это позже данного кода), то то, что я говорю, на них не распространяется. Эти переменные могут быть в стеке, в регистре, прыгать туда-обратно по настроению компилятора — неважно.
Аргументы функции в этом смысле включаются в локальные переменные — если они не приняты по ссылке (а нахрена?)
2. А вот содержимое памяти по этим указателям — да, должен читать. И ровно это, если ты имеешь в виду логику strcpy (ты ведь зачем-то такой пример взял, да?), происходит и сейчас. Компилятор обязан читать и писать память под указателями, причём даже по двум причинам: указатели одинакового типа — значит, логика алиасинга "память под указателями разных типов независима", и указатели типа char* — что убивает независимость операций (есть такое специальное правило).
3. Я что-то не понял отношения между частями твоего вопроса. Если бы это было "не только читать содержимое куска памяти... но и читать содержимое указателей", было бы понятно. А так — нет.
То, что я имел в виду изначально, это вещи следующего вида.
Самое простое: если мы видим, например,
int a, b, c; // глобальныеvoid foo() {
b = a;
c = a;
}
то объединять их не положено, пока не будет разрешено, например, в стиле
[[aliasing(relaxed)]]
void foo() {
b = a;
c = a;
}
Или, например, у нас есть код:
void boo(float *a, int n, int *c) {
for (int i = 0; i < n; ++i) { a[i] *= *c; }
}
Сейчас за счёт разнотипности *a и *c считается, что присвоение любому a[i] не может влиять на значение *c, поэтому при любой оптимизации значение *c начинает кэшироваться.
Берём тот же gcc:
asm1
boo:
.LFB0:
testl %esi, %esi
jle .L1
pxor %xmm1, %xmm1
leal -1(%rsi), %eax
cvtsi2ssl (%rdx), %xmm1 <-- Вот тут один раз прочитал и запомнил
leaq 4(%rdi,%rax,4), %rax
.L3:
movss (%rdi), %xmm0
addq $4, %rdi
mulss %xmm1, %xmm0
movss %xmm0, -4(%rdi)
cmpq %rax, %rdi
jne .L3
.L1:
ret
А теперь меняем int *c на float *c, и это разрешение компилятору уходит, потому что он уже подозревает, что присвоение a[i] может повлиять на *c:
asm2
boo:
testl %esi, %esi
jle .L1
movq %rdi, %rax
leal -1(%rsi), %ecx
leaq 4(%rdi,%rcx,4), %rcx
.L3:
movss (%rax), %xmm0
mulss (%rdx), %xmm0 <-- Читает на каждой итерации
movss %xmm0, (%rax)
addq $4, %rax
cmpq %rcx, %rax
jne .L3
.L1:
ret
А теперь я добавляю слово restrict (увы, только C, не C++) к float *c, и оно возвращается к однократному чтению:
asm3
boo:
testl %esi, %esi
jle .L1
movss (%rdx), %xmm1 <-- Прочли один раз и запомнили
movq %rdi, %rax
leal -1(%rsi), %edx
leaq 4(%rdi,%rdx,4), %rdx
.L3:
movaps %xmm1, %xmm0
mulss (%rax), %xmm0
movss %xmm0, (%rax)
addq $4, %rax
cmpq %rdx, %rax
jne .L3
.L1:
ret
Вот то что я хочу видеть по умолчанию — это такой себе anti-restrict (как в asm2) для всех операций с памятью (то есть с любым, что не является локальной переменной, у которой не брали адрес), а кроме того запрет переупорядочения доступа к ним.
В примере 2, если кто-то считает при этом, что недостаточно скорости для доступа к *c, то он может сложить в локальную переменную — или таки атрибутами выставить облегчение, ослабив тотальный алиасинг — в пределах одного куска исходного кода.
Здравствуйте, T4r4sB, Вы писали:
N>>Таки нет. Undefined — это когда от твоего нарушения может взорваться вообще всё и не только в этом месте.
TB>Я говорю о том, как оно работает на этапе компиляции в реальности.