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

Сообщение Re[11]: Carbon от 19.04.2024 17:39

Изменено 19.04.2024 18:06 Sinclair

Re[11]: Carbon
Здравствуйте, vdimas, Вы писали:

V>Не-а, перестаёт выдавать ошибку для многих компиляторов, или меняет поведение на правильное для платформы x86 для некоторых компиляторов, даже если в них всё еще не работает в x64.


"Для некоторых компиляторов" как раз и означает, что программа по прежнему неверна. Даже исходная программа выдавала правильное поведение в некоторых случаях.
Задача, напомню, в том, чтобы выдавать правильный результат всегда, а не когда "звёзды удачно встали". В частности, вести себя одинаково в одном и том же компиляторе вне зависимости от уровня оптимизаций.

V>Я не усложнял твой код, я показывал примеры, где твой код заведомо не работает даже в дебаге.


Лучше покажите примеры, где ваш код работает "в релизе".

V>В данном случае этот твой недостаток выходит на уровень упоротости, бо правильный ответ тебе был уже дан, но ты не проверил и продолжаешь падать.

Правильный ответ дал коллега CreatorCray. А вы продолжаете гнать чушь.

V>Я прошёлся позже, почитал и словил много лулзов, надо сказать.

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

V>Ладно ты не плюсовик, тебе простительно не держать в голове кучу низкоуровневых мелочей...

Вот именно. А вот вы, раз претендуете на профессионализм в плюсах, так плавать в этих нюансах вроде бы не должны.

V>Описание второй ошибки было дано в том же посте, так зачем ты разводишь лишные пинг-понги? ))

Не было у вас там никакого описания "второй ошибки". Был некий пример с битовыми полями, в котором вы намекаете на то, что не понимаете правил integral promotions.

V>Вынуждаешь повторяться — из-за допустимости промежуточных вычислений, выполняемых в более широких типах.

V>И это прямо по стандарту.
Ну, то есть вам было недостаточно один раз сказать глупость, вы хотите на ней настаивать.
Ок, давайте разберём подробно.
Вы намекаете на то, что компилятор, увидев вот такое выражение: (value+1)<value, где value имеет тип int, возможно исполняет его как (long(value)+1)<long(value).
Ну так вот, это — неверно.
Смотрим в справочник:

if the integer conversion rank of T is lower than the rank of int:
val can be converted to a prvalue of type int if int can represent all the values of T;
otherwise, val can be converted to a prvalue of type unsigned int.

То есть типы, более короткие чем int, допустимо расширять до int или unsigned int. Точка. Стандарт не разрешает промотить int до long.
Все более широкие promotions допустимы только для случаев char8_t, char16_t, char32_t, и для битовых полей (что, очевидно, не наш случай, но об этом позже).

Ваше замечание про short справедливо ровно потому, что short короче int. И по стандарту

arithmetic operators do not accept types smaller than int as arguments, and integral promotions are automatically applied after lvalue-to-rvalue conversion, if applicable.

Поэтому sizeof(value+1) > sizeof(value) в случае, когда тип value — short.
А вот для int sizeof(value+1) == sizeof(value) в любом компиляторе, соответствующем стандарту.

V>Кстате, в C# это точно так же допустимо по стандарту.

Нет, недопустимо. Там работают очень похожие правила integral promotions, и даже более строгие. В частности, С++ вот такой код прожёвывает без единого warning:
short x = 42;
x = x + 1;

А на С# это даёт ошибку компиляции.
V>Поэтому, готовься ловить аналогичные прилёты и в этом языке, если/когда ему прикрутят человеческий оптимизатор.
Спасибо, посмешили.
V>Я выше не просто так обратил внимание на твою стрёмную позицию в обсуждении как таковую.
Стрёмная позиция, коллега, тут только у вас. Потому что воинствующее невежество заведомо стремнее простого невежества.

V>Из-за непонимания причины происходящего ты набросился на компиляторы С++, что они, мол, умыли руки из-за UB.



V>Хотя суть происходящего не в С++, та же фигня возможна в любом языке, в котором будут происходит хоть какие-то заметные оптимизации.

Нет. Суть происходящего — исключительно в UB. Можно, к примеру, просто заменить тип x на любой unsigned и убедиться, что все компиляторы мгновенно станут выдавать корректный результат на любом уровне оптимизации.

V>Ключевое в обсуждаемом примере то, что UB в нём — это не побочный эффект "ужасного С++", это насущная необходимость, это даже непосредственная цель!


V>В противном случае пришлось бы обязательно обрезать результат каждого промежуточного вычисления.
Результат каждого промежуточного вычисления и так обязательно обрезается. Бит переполнения остаётся только в регистре флагов, и игнорируется в последующей арифметике.
В дотнете, как вы, должно быть, знаете, никакого UB для integral arithmetics нету, и ничего — никто не умер.

V>Обобщенный код должен вести себя правильно для различных типов, иначе нафига он такой обобщённый нужен? ))

Ну вот у вас пока не получилось написать правильный обобщённый код.

S>>Я правильно понимаю, что вы ожидаете неверного поведения от моей программы на "последних gcc на x64 без оптимизаций" потому, что промежуточные вычисления он будет делать в 64 битах?

S>>Это легко проверить — достаточно
S>>а) попробовать починить программу предложенным вами образом
S>>б) попробовать запретить оптимизации в исходной программе
S>>Если вы правы, то а) даст wow, а b) — oops.

V>Первый вариант:

V>
V>template<typename signed_integral>
V>bool is_max(signed_integral value)
V>{
V>    volatile signed_integral tmp = value + 1;
V>    return tmp < value;
V>}
V>

V>https://godbolt.org/z/fTs1Gz663
V>Этот вариант использует operator<, где ошибка исходного варианта крылась в неуловимости старшего бита.
) нет там никакой неуловимости.

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

И вот опять, вы сами себя перехитрили. volatile явным образом запрещает "регистровость" переменной, вместе с ещё рядом оптимизаций.
Собственно это и показывает, что исходное утверждение про "расширение битности" — полная чушь и невежественный бред.
Для того, чтобы запретить компилятору расширять битность выражения, достаточно было исходного фикса с принудительным приведением (value+1) к signed_integral.
В частности, в коде для short будет использоваться регистр AX, а не EAX.
И в обратную сторону это тоже работает — можно обойтись вообще без "сохранений в переменную", одними чтениями:
template<typename signed_integral>
bool is_max(volatile signed_integral value)
{
    return signed_integral(value + 1) < value;
}

Как видите, никаких переменных и сохранений не надо. Достаточно запретить компилятору полагаться на то, что слева и справа от сравнения стоит одно и то же значение — теперь он вынужден честно его вычислять, и это сразу даёт правильный результат. А без volatile мы налетаем на то, что gcc, к примеру, даже в -O0 включает порядка полусотни оптимизаций, из-за чего программа по-прежнему работает неверно.

V>Сам факт сохранения переменной нас не интересует, нас интересует обязательность операции truncation до нужного кол-ва бит, дабы неуловимый разряд стал уловимым.

Опять чушь. Для int никакого truncation не выполняется.
Ну, я понимаю, вы человек упёртый. Давайте, расскажите мне, в какой битности будут выполняться промежуточные вычисления is_max для long. Там тоже будет какой-то "расширенный тип для проомежуточных результатов", который нужно обрезать руками?

V>Но это всё-равно кривое решение — погоня за неуловимыми битами, детсад...

Нет никакой погони за "неуловимыми битами".
V>Чуть правильней будет не гоняться за неуловимым битом, а проверить его напрямую.
V>
V>template<typename signed_integral>
V>bool is_max(signed_integral value)
V>{
V>    using u_t = make_unsigned_t<signed_integral>;

V>    u_t mask = static_cast<u_t>(0x8000000000000000ul >> (8 - sizeof(value))*8);
V>    u_t u_v = static_cast<u_t>(value);

V>    return ((u_v + 1) & mask) != 0;
V>}
V>

Омг.
V>https://godbolt.org/z/b5f8zxMnv
V>Здесь для избегания UB все битовые операции ведутся в беззнаковых целых.
Ну так всё верно — в итоге вернулись к тому, с чего начинали. Все эти приседания вокруг сдвижки бит и прочая наркомания никакой пользы не несут. Только мешают компилятору работать — вы посмотрите, какой код генерирует выбранный вами кланг. Ключ к успеху именно в уходе от UB. Никаких "расширений" и "обрезаний". Поэтому ответ CreatorCray и является самым правильным.


V>Ради интереса замени тут:

Я с самого начала знал, что переход к беззнаковой арифметике спасёт отцов русской демократии.
А всё потому, что корень проблем не состоит ни в каких "неуловимых битах". В беззнаковых типах бит ровно столько же, сколько в знаковых.

V>Но это по-прежнему кривое решение, потому что достаточно сравнить число сразу с константой, без операции сложения:

V>
V>template<typename signed_integral>
V>bool is_max(signed_integral value)
V>{
V>    return value == 0x7FFFFFFFFFFFFFFFull >> (8 - sizeof(value))*8;
V>}
V>

V>https://godbolt.org/z/a7ajxzdsP
Нда. На всякого мудреца довольно простоты.
Заметим, для начала, что решение от СreatorCray совершенно случайно работает и для беззнаковых типов тоже. А ваше — нет. Ну, понятно, вы купились на имя типа "signed_integral". Увы — это всего лишь имя типа. В вашем шаблонном check параметр даже текстом не намекает пользователю, что беззнаковые типы вы обрабатывать не умеете .

Как я и писал в самом начале этого треда, С++ больнее всего бьёт именно по таким программистам, как вы — тем, кто думает, что "разобрался в тонкостях".

V>В целом обсуждение доставляет лулзы именно этим — эффектом неожиданности. ))

V>Даётся очень странное решение простой задачи, потом "следите за руками", потом "а вот тут фокус удался!" и прочая чепуха.
Ну, вы сумели переплюнуть всех в жанре "очень странное решение простой задачи".
V>Да не сталкиваемся мы с этой хернёй в работе, потому что не пишем так.
V>Не решаем так задачи.
Да я вижу, как вы решаете. Прекрасная антиреклама.
template<typename T>
bool is_max(T value)
{
return std::numeric_limits<T>::max() == value;
}

V>А если ты так пишешь, в духе исходного примера, то однажды будешь ловить прилёты и на любимом C#, когда он чуть подрастёт, бо гигиену надо соблюдать с детства, как грится! ))
Вот вроде бы забрезжил свет понимания — но нет, снова бред.
Процитирую единственную осмысленную вещь, которую вы сумели написать в процессе этой беседы:

Здесь для избегания UB все битовые операции ведутся в беззнаковых целых

То есть единственный надёжный способ борьбы с UB в С++ — избегать undefined behavior.
А как только мы убираем undefined behavior, внезапно самый оптимизирующий из C++ компиляторов начинает вести себя прилично, и никакой разницы между -O0 и -03 не происходит. Так что дело — вовсе не в мифических "взрослых оптимизациях", а только в UB.
Так вот, в дотнете UB возможен только в unsafe context. Всё. В безопасном контексте стандарт явно запрещает undefined behavior. Есть пара моментов, где стандарт допускает implementation-defined behavior, но это опять-таки детерминированное поведение. И ни компилятор, ни джит не имеют права "доопределять" его по вкусу таким способом, что на одной платформе оно противоречит само себе: https://godbolt.org/z/GxvThzs1a
Re[11]: Carbon
Здравствуйте, vdimas, Вы писали:

V>Не-а, перестаёт выдавать ошибку для многих компиляторов, или меняет поведение на правильное для платформы x86 для некоторых компиляторов, даже если в них всё еще не работает в x64.


"Для некоторых компиляторов" как раз и означает, что программа по прежнему неверна. Даже исходная программа выдавала правильное поведение в некоторых случаях.
Задача, напомню, в том, чтобы выдавать правильный результат всегда, а не когда "звёзды удачно встали". В частности, вести себя одинаково в одном и том же компиляторе вне зависимости от уровня оптимизаций.

V>Я не усложнял твой код, я показывал примеры, где твой код заведомо не работает даже в дебаге.


Лучше покажите примеры, где ваш код работает "в релизе".

V>В данном случае этот твой недостаток выходит на уровень упоротости, бо правильный ответ тебе был уже дан, но ты не проверил и продолжаешь падать.

Правильный ответ дал коллега CreatorCray. А вы продолжаете гнать чушь.

V>Я прошёлся позже, почитал и словил много лулзов, надо сказать.

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

V>Ладно ты не плюсовик, тебе простительно не держать в голове кучу низкоуровневых мелочей...

Вот именно. А вот вы, раз претендуете на профессионализм в плюсах, так плавать в этих нюансах вроде бы не должны.

V>Описание второй ошибки было дано в том же посте, так зачем ты разводишь лишные пинг-понги? ))

Не было у вас там никакого описания "второй ошибки". Был некий пример с битовыми полями, в котором вы намекаете на то, что не понимаете правил integral promotions.

V>Вынуждаешь повторяться — из-за допустимости промежуточных вычислений, выполняемых в более широких типах.

V>И это прямо по стандарту.
Ну, то есть вам было недостаточно один раз сказать глупость, вы хотите на ней настаивать.
Ок, давайте разберём подробно.
Вы намекаете на то, что компилятор, увидев вот такое выражение: (value+1)<value, где value имеет тип int, возможно исполняет его как (long(value)+1)<long(value).
Ну так вот, это — неверно.
Смотрим в справочник:

if the integer conversion rank of T is lower than the rank of int:
val can be converted to a prvalue of type int if int can represent all the values of T;
otherwise, val can be converted to a prvalue of type unsigned int.

То есть типы, более короткие чем int, допустимо расширять до int или unsigned int. Точка. Стандарт не разрешает промотить int до long.
Все более широкие promotions допустимы только для случаев char8_t, char16_t, char32_t, и для битовых полей (что, очевидно, не наш случай, но об этом позже).

Ваше замечание про short справедливо ровно потому, что short короче int. И по стандарту

arithmetic operators do not accept types smaller than int as arguments, and integral promotions are automatically applied after lvalue-to-rvalue conversion, if applicable.

Поэтому sizeof(value+1) > sizeof(value) в случае, когда тип value — short.
А вот для int sizeof(value+1) == sizeof(value) в любом компиляторе, соответствующем стандарту.

V>Кстате, в C# это точно так же допустимо по стандарту.

Нет, недопустимо. Там работают очень похожие правила integral promotions, и даже более строгие. В частности, С++ вот такой код прожёвывает без единого warning:
short x = 42;
x = x + 1;

А на С# это даёт ошибку компиляции.
V>Поэтому, готовься ловить аналогичные прилёты и в этом языке, если/когда ему прикрутят человеческий оптимизатор.
Спасибо, посмешили.
V>Я выше не просто так обратил внимание на твою стрёмную позицию в обсуждении как таковую.
Стрёмная позиция, коллега, тут только у вас. Потому что воинствующее невежество заведомо стремнее простого невежества.

V>Из-за непонимания причины происходящего ты набросился на компиляторы С++, что они, мол, умыли руки из-за UB.



V>Хотя суть происходящего не в С++, та же фигня возможна в любом языке, в котором будут происходит хоть какие-то заметные оптимизации.

Нет. Суть происходящего — исключительно в UB. Можно, к примеру, просто заменить тип x на любой unsigned и убедиться, что все компиляторы мгновенно станут выдавать корректный результат на любом уровне оптимизации.

V>Ключевое в обсуждаемом примере то, что UB в нём — это не побочный эффект "ужасного С++", это насущная необходимость, это даже непосредственная цель!


V>В противном случае пришлось бы обязательно обрезать результат каждого промежуточного вычисления.
Результат каждого промежуточного вычисления и так обязательно обрезается. Бит переполнения остаётся только в регистре флагов, и игнорируется в последующей арифметике.
В дотнете, как вы, должно быть, знаете, никакого UB для integral arithmetics нету, и ничего — никто не умер.

V>Обобщенный код должен вести себя правильно для различных типов, иначе нафига он такой обобщённый нужен? ))

Ну вот у вас пока не получилось написать правильный обобщённый код.

S>>Я правильно понимаю, что вы ожидаете неверного поведения от моей программы на "последних gcc на x64 без оптимизаций" потому, что промежуточные вычисления он будет делать в 64 битах?

S>>Это легко проверить — достаточно
S>>а) попробовать починить программу предложенным вами образом
S>>б) попробовать запретить оптимизации в исходной программе
S>>Если вы правы, то а) даст wow, а b) — oops.

V>Первый вариант:

V>
V>template<typename signed_integral>
V>bool is_max(signed_integral value)
V>{
V>    volatile signed_integral tmp = value + 1;
V>    return tmp < value;
V>}
V>

V>https://godbolt.org/z/fTs1Gz663
V>Этот вариант использует operator<, где ошибка исходного варианта крылась в неуловимости старшего бита.
) нет там никакой неуловимости.

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

И вот опять, вы сами себя перехитрили. volatile явным образом запрещает "регистровость" переменной, вместе с ещё рядом оптимизаций.
Собственно это и показывает, что исходное утверждение про "расширение битности" — полная чушь и невежественный бред.
Для того, чтобы запретить компилятору расширять битность выражения, достаточно было исходного фикса с принудительным приведением (value+1) к signed_integral.
В частности, в коде для short будет использоваться регистр AX, а не EAX.
И в обратную сторону это тоже работает — можно обойтись вообще без "сохранений в переменную", одними чтениями:
template<typename signed_integral>
bool is_max(volatile signed_integral value)
{
    return signed_integral(value + 1) < value;
}

Как видите, никаких переменных и сохранений не надо. Достаточно запретить компилятору полагаться на то, что слева и справа от сравнения стоит одно и то же значение — теперь он вынужден честно его вычислять, и это сразу даёт правильный результат. А без volatile мы налетаем на то, что gcc, к примеру, даже в -O0 включает порядка полусотни оптимизаций, из-за чего программа по-прежнему работает неверно.

V>Сам факт сохранения переменной нас не интересует, нас интересует обязательность операции truncation до нужного кол-ва бит, дабы неуловимый разряд стал уловимым.

Опять чушь. Для int никакого truncation не выполняется.
Ну, я понимаю, вы человек упёртый. Давайте, расскажите мне, в какой битности будут выполняться промежуточные вычисления is_max для long. Там тоже будет какой-то "расширенный тип для проомежуточных результатов", который нужно обрезать руками?

V>Но это всё-равно кривое решение — погоня за неуловимыми битами, детсад...

Нет никакой погони за "неуловимыми битами".
V>Чуть правильней будет не гоняться за неуловимым битом, а проверить его напрямую.
V>
V>template<typename signed_integral>
V>bool is_max(signed_integral value)
V>{
V>    using u_t = make_unsigned_t<signed_integral>;

V>    u_t mask = static_cast<u_t>(0x8000000000000000ul >> (8 - sizeof(value))*8);
V>    u_t u_v = static_cast<u_t>(value);

V>    return ((u_v + 1) & mask) != 0;
V>}
V>

Омг.
V>https://godbolt.org/z/b5f8zxMnv
V>Здесь для избегания UB все битовые операции ведутся в беззнаковых целых.
Ну так всё верно — в итоге вернулись к тому, с чего начинали. Все эти приседания вокруг сдвижки бит и прочая наркомания никакой пользы не несут. Только мешают компилятору работать — вы посмотрите, какой код генерирует выбранный вами кланг. Ключ к успеху именно в уходе от UB. Никаких "расширений" и "обрезаний". Поэтому ответ CreatorCray и является самым правильным.


V>Ради интереса замени тут:

Я с самого начала знал, что переход к беззнаковой арифметике спасёт отцов русской демократии.
А всё потому, что корень проблем не состоит ни в каких "неуловимых битах". В беззнаковых типах бит ровно столько же, сколько в знаковых.

V>Но это по-прежнему кривое решение, потому что достаточно сравнить число сразу с константой, без операции сложения:

V>
V>template<typename signed_integral>
V>bool is_max(signed_integral value)
V>{
V>    return value == 0x7FFFFFFFFFFFFFFFull >> (8 - sizeof(value))*8;
V>}
V>

V>https://godbolt.org/z/a7ajxzdsP
Нда. На всякого мудреца довольно простоты.
Заметим, для начала, что решение от СreatorCray совершенно случайно работает и для беззнаковых типов тоже. А ваше — нет. Ну, понятно, вы купились на имя типа "signed_integral". Увы — это всего лишь имя типа. В вашем шаблонном check параметр даже текстом не намекает пользователю, что беззнаковые типы вы обрабатывать не умеете .

Как я и писал в самом начале этого треда, С++ больнее всего бьёт именно по таким программистам, как вы — тем, кто думает, что "разобрался в тонкостях".

V>В целом обсуждение доставляет лулзы именно этим — эффектом неожиданности. ))

V>Даётся очень странное решение простой задачи, потом "следите за руками", потом "а вот тут фокус удался!" и прочая чепуха.
Ну, вы сумели переплюнуть всех в жанре "очень странное решение простой задачи".
V>Да не сталкиваемся мы с этой хернёй в работе, потому что не пишем так.
V>Не решаем так задачи.
Да я вижу, как вы решаете. Прекрасная антиреклама.
template<typename T> 
bool is_max(T value)
{
  return std::numeric_limits<T>::max() == value;
}

V>А если ты так пишешь, в духе исходного примера, то однажды будешь ловить прилёты и на любимом C#, когда он чуть подрастёт, бо гигиену надо соблюдать с детства, как грится! ))
Вот вроде бы забрезжил свет понимания — но нет, снова бред.
Процитирую единственную осмысленную вещь, которую вы сумели написать в процессе этой беседы:

Здесь для избегания UB все битовые операции ведутся в беззнаковых целых

То есть единственный надёжный способ борьбы с UB в С++ — избегать undefined behavior.
А как только мы убираем undefined behavior, внезапно самый оптимизирующий из C++ компиляторов начинает вести себя прилично, и никакой разницы между -O0 и -03 не происходит. Так что дело — вовсе не в мифических "взрослых оптимизациях", а только в UB.
Так вот, в дотнете UB возможен только в unsafe context. Всё. В безопасном контексте стандарт явно запрещает undefined behavior. Есть пара моментов, где стандарт допускает implementation-defined behavior, но это опять-таки детерминированное поведение. И ни компилятор, ни джит не имеют права "доопределять" его по вкусу таким способом, что на одной платформе оно противоречит само себе: https://godbolt.org/z/GxvThzs1a