Re[8]: Антипаттерн, противоположный Primitive Obsession
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 25.03.23 07:17
Оценка: +1
Здравствуйте, T4r4sB, Вы писали:

N>>Перемножаем. Первое произведение int64_t, второе int96_t, третье int128_t... Астанавитесь(c), Вите надо выйти!

TB>Перемножение в цикле — это проблема, да, тут увы приходится задать одно ограничение на результат и в каждой итерации следить что вписываемся.

Так нет принципиальной разницы со сложением или вычитанием. Сложил два int32_t, получил int33_t. Прибавил к нему int32_t, можешь формально получить int34_t. И так далее.
Даже если транслятор не оперирует напрямую такими типами (LLVM — может, там любая битность допустима почти до самой зашкальной), то подсчёт диапазонов значений может к такому привести.
Потому возвращаемся к исходному — или своевременный контроль, или наплевательство, или явное разрешение усечения (а местами — насыщения, saturating mode), или, как в С для знаковых, доверие программисту. Но на чём-то надо остановиться.

Почему я постоянно и говорю — кто лучше знает, что именно тут надо, кроме автора кода? А раз он знает — может и должен явно указать. А компилятор должен позволять такие указания и правильно их отрабатывать.

N>>А почему ты тут думаешь именно про i32? А вдруг и его переполнит, тогда оно должно было святым духом догадаться, что надо было конвертить всё к i64? Или как?

TB>В идеале — ограничивать операнды, чтобы компилятор знал, что результат впишется в i32.

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

Но именно в последнем случае — удобно или использовать стандартное "продвижение" целого (integral promotion), как в C/C++/много_где, или самим предварительно расширить все рабочие значения. Иногда этого расширения не хватает, увы. Но по крайней мере в твоём примере с u8 должно хватить.

N>>Да, и вдогонку — а действительно, почему у тебя они в u8? Это же тип скорее для упаковки в структуру, чем для манипулирования. Во всех современных ABI всё равно будет один регистр занят, так что u8 или i32 — одномайственно.

TB>Ещё раз с самого начала — если у тебя шахматная доска 256х256, то для координат фигуры нормальный человек конечно же будет использовать int. Но с точки зрения хипстера, обдолбавшегося доменно-ориентированной хренотой, использовать int это неправильно, надо использовать тип, который в принципе не может содержать некорректных значений. Не знаю зачем ему это надо, но он считает, что это круто.

У тебя тут принципиально проблемные переходы:

1. От типа, который содержит только корректные значения (что правильно) — к конкретным вариантам типа u8. Какое твоё нафиг дело на этом уровне, сколько в нём бит? Может, у него вообще два корректных значения +238 и -245. Сколько ты на него бит выделишь — 1, 9, 16, 32? Пусть компилятор думает об этом, у него голова большая быстрее считает.

2. От диапазона значений конкретного базового типа вроде i32 ты переходишь к тому, как выполняются операции с ним. И вот тут, предполагая, что i32+i32 даёт i32, ты почему-то пытаешься получить, что 0..255 + 0..255 должен дать 0..510, а из этого — что i8+i8 даёт i9. Но почему тогда i32+i32 не должно дать i33? (см. выше)

Посмотри в этом смысле на Ada. Там можно определять типы в стиле

type chess_coord = Integer range 0..255;

После этого, насколько помню, в вычислениях используется продвижение целых до Integer (или, если сказано явно, до LongInt), контролируется переполнение согласно этому размеру, но при укладке в переменную проверяется диапазон (ну кроме если компилятор уверен, что выхода за его пределы не будет). Выглядит удобно и средне-разумно, позволяя отрабатывать максимум типовых ситуаций безболезненно и контролируемо.

(А для нужд всяких кольцевых счётчиков там есть типы целых в стиле "modulo 2**32". Вот с ними операции выполняются в модульной арифметике, как беззнаковые в C/C++.)

TB> Ну типа в функции figure.place(u8 x, u8 y) мы на этапе компиляции знаем что значения валидны! При этом хипстеру насрать, что код валидации просто будет вынесен из сеттера во все места, где мы вычисляем эти координаты и никакого выигрыша мы не получим,


Меняю мысленно u8 на "integer range 0..255". Вообще-то получим выигрыш: точно не будет молчаливого усечения значения. А какая цена проверок — зависит от компилятора. Во многих случаях он может их просто выкинуть.

TB> более того, мы создаём грабли, если напишем figure1.x — figure2.y, случайно забыв скастовать в инт.


Вполне себе аргумент в пользу удобства integral promotion для типовых операций. Но, с другой стороны, операция получения разности координат это особая операция, при которой меняется тип. Это было ещё в C: можно вычитать два однотипных указателя, получим разность между ними (причём разность адресов делится на размер указуемого). Почему не сделать такое же?

Причём это надо делать (в синтаксисе C++/GCC) так:
using coord_t = uint_t<8>;
using coord_diff_t = int_t<9>;
inline coord_diff_t operator-(coord_t a, coord_t b) {
    coord_diff_t ret;
    __builtin_sub_overflow(a, b, &ret); // No overflow happens
    return ret;
}


Ну а дальше у тебя будет просто figure1.x — figure2.y которое молча под капотом правильно выполнится.

TB> Кулхацкеры конечно же никогда (на словах) не забывают сделать лишний каст,


"Кулхацкеры" — да, возможно.
Нормальные хакеры сделают, как я описал выше, и забудут про проблему.

Rust это позволяет, но я не хочу выгибаться вспоминать, как в нём это делается. Но концептуально это 1:1 описанный подход C++.

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


TB>А вот и нет. Лишние запреты со стороны компилятора почему-то воспринимаются как абсолютное добро, но это не так. Во-первых, не все пишут прошивки для ядерных реакторов, я б сказал, что подавляющее большинство пишет код, в котором не требуется абсолютная надёжность.


Подавляющее большинство пишет код, в котором проверки надёжности несущественно влияют на скорость и объём. А те 1-5%, которые пишут критичный к этому код в отдельных местах, заинтересованы в специальных опциях ради этого ровно в этих отдельных местах.

Я недавно с одним проектом работал, в котором принципиально -O0 во всех компиляциях. Ничего, ресурсов хватало (управляющий слой над железкой). Создатели решили, что это лучше, чем бодаться с возможными проблемами даже от простого -O (не знаю, было ли тогда -Og, но они и его не включили).

А вот диагностика ситуации "процесс X не отработал запрос потому, что в этот момент упал процесс Y, а проверка криво сделана в промежуточном процессе Z, который выдал неполные данные" может обойтись на порядки дороже, чем однократное корректное написание. Особенно если это у субкастомера кастомера в пределах концерна-клиента.

Лучше пусть оно диагностируемо упадёт, чем выдаст неправильные данные.

TB> От падения программы просто завершится один процесс и в документ не попадут изменения за последнюю минуту. Память других программ не попортится, это не ДОС с убогим реальным


От падения — да, вероятно — и см. выше какие сценарии бывают (и я это реально видел).
А если оно молча выдаст неверные данные?

TB> режимом. Ещё со времён 386 процессора с его менеджером памяти уже есть абстракция, в которой любая программа, работающая с правами пользователя, уже является безопасной.


Нет.

И вообще, я не понимаю — выше ты против того, чтобы u8 + u8 проверялось на выход за пределы диапазона u8 — а теперь что? Ты уж определись.

TB> И если время, затрачиваемое на борьбу с компилятором больше, чем время, затрачиваемое на обнаружение и устранение бага — то лишние сложности в написании кода не только создают лишний гемор при написании и особенно рефакторинге, но и просто увеличат время создания работающего продукта. К сожалению, эпоха, когда средства повышения надёжности кода повышали и удобство программера, прошла.


Не прошла. Но многие из теперешних метаний выглядят, согласен, бессмысленными.

TB>Во-вторых, не все, что якобы повышает надёжность, действительно её повышает.

TB>Вот те же вектора-точки. Допустим, я использую старпёрскую идеологически невыдержанную библиотеку, где вектор и точка это одно и то же.
TB>Есть точка V. А есть плоскость, заданная уравнением V*N+C=0. И чтоб понять, с какой стороны точка находится от плоскости, я блин беру, и смотрю на знак этого самого V*N+C.
TB>Как выглядит эта ситуация, если векторы и точки это разные сущности?! Я вижу два варианта:
TB>1. Плоскость задаётся уравнением (P-C)*N=0, где P,C это точки, а N это вектор. Но тогда вычисление этой фигни как минимум требует лишние два вычитания.
TB>2. Плоскость задаётся уравнением V*N+C=0, а чтоб понять, с какой стороны от этой плоскости находится точка, надо вычислить
TB>(P-ZeroPoint)*N+C. И тут я боюсь, что любая мало-мальски сложная формула сведётся к бесконечным (P-ZeroPoint) вместо V, ну и в итоге формула будет содержать те же самые выражения, и при этом ошибиться в наборе формулы можно будет ровно в тех же местах. Более того, из-за ухудшившейся читаемости пропустить опечатку станет даже проще, и компилятор ничего не скажет.

Возможно, я что-то упускаю, но почему не создать промежуточную переменную типа "вектор", которая, по твоим описаниям, будет равна P-ZeroPoint, адекватно назвать её и дальше использовать в формулах?

(Причём при нормальной оптимизации компилятор операцию вида P-ZerPoint внутри себя превратит в копирование скаляров, потом проведёт через SSA, устранив копирования.)

>> Для архива оставлю ссылку. (ты там активно участвовал)

TB>Спасибо, добавлю ещё это: https://internals.rust-lang.org/t/subscripts-and-sizes-should-be-signed/17699
TB>К сожалению, мой низкий уровень инглиша и фанатизм сообщества не позволили обратить внимание на проблему(

Почитал. Английский действительно так себе, но на дискуссию, по-моему, это не повлияло совсем. Там и хуже пишут (не в оправдание). Явного фанатизма я тоже не заметил. Что заметил:
1) Есть уже принятые решения (может, и преждевременно), которые менять сложно без полного слома языка: сохранение типа в операциях (что u8*u8 -> u8) и беззнаковый размер (даже если фактически не выходит за пределы знакового).
2) Есть масса приёмов (там приведены в примерах), которые дают возможность избежать подавляющего большинства проблем от этих решений.
Вообще их ответы очень интересны именно этими примерами.

Я лично бы сделал, да, integral promotions контекстно-устанавливаемыми. Но я плохо представляю себе, как это сочетать с дизайном языка. Вот пишешь ты impl для одного своего типа... нужны разные варианты функций в зависимости от этих параметров контекста? Компилятор должен уметь подбирать эти типы? Жестоко.
The God is real, unless declared integer.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.