Как стать автором
Обновить

Комментарии 640

Правильно ли я понимаю, что:

  • разработчики научили компилятор предотвращать выстрелы в ногу и убрали из него саму возможность выстрелить себе в ногу

  • пользователи требуют вернуть возможность стрелять в ногу, на том основании, что (1) раньше можно было, (2) из-за предыдущего пункта многие программы по-прежнему стреляют в ногу, (3) из-за предыдущего пункта есть необходимость тестировать такие выстрелы.

?

Переполнение знакового - это такая бездна, в которую даже приближаться не хочется.

Если бы проблема была только в переполнении знакового... Но да, уже это весьма неприятно. =)

Си должен умереть, да, и пока что Rust - самое близкое из того, что у нас есть для замены Си. Однако, я с большим интересом смотрю на срачики вокруг Rust-драйверов в ядре. Претензии, которые предъявляют к Rust'у весьма любопытны. Ядро не должно падать по "одной ошибке", даже если это логическая ошибка, а в Rust весьма трудно избежать паник при нарушении инвариантов (а драконы из земли uninitialized memory весьма хотят их нарушить...)

Претензии, которые предъявляют к Rust'у весьма любопытны. Ядро не должно падать по "одной ошибке", даже если это логическая ошибка, а в Rust весьма трудно избежать паник

Это на мой взгляд самый слабый аргумент против. В изначальном ядре, без патчей для добавления поддержки Rust, есть функция "panic", и куча ее вызовов с помощью макросов BUG/BUG_ON и так далее. Поэтому почему Rust в отличие от С должен быть особенным и не содержать неявных вызовов panic! совершенно непонятно.

В Rust много здравых идей, но мне он кажется избыточно сложным. А сложность языка имеет свойство перекладываться на сложность программ, что в свою очередь провоцирует ошибки. Кроме того, про Rust важно понимать и помнить, что он не избавляет от всех ошибок, а только от некоторых (прошу прощения если оскорбил -- есть знакомые растовцы, которые считают что их код абсолютно безопасен и корректен ведь написан на Rust).

Что касается замены Си, Rust мне кажется больше заменой плюсам, но если потянет -- интересно будет посмотреть на мир, где железо заржавело. =)

Не совсем так. Насколько я понимаю, речь идёт о том, что в rust некоторые виды действий не имеют Option, и если условие не выполняется, то BUG_ON/panic! единственная опция. А в Си с этим умудряются жить.

Если ядро обнаруживает, что pagetable коррапченная, то это BUG_ON. Но если драйвер GP устройства обнаруживает, что ему не дали 4кб памяти, то это не BUG_ON никаким образом (на что именно ругались я не смотрел, я читал сам текст ругани).

В принципе, на Rust'е можно писать полностью низкоуровневый код (включая naked functions для обработки прерываний). Хотя я погуглил, плюсы тоже умеют naked. Так что разница только в здравом смысле Rust'а (наличии safe-подмножества).

... В то же самое время, как в условиях ядра справятся с ownership - это интересно.

Насколько я понимаю, речь идёт о том, что в rust некоторые виды действий не имеют Option, и если условие не выполняется

А какие именно "действий"? В голову приходит только "fallible allocation", но обертку над менеджером памяти ядра все равно нужно будет делать отдельно, стандартные функции выделения памяти использовать вряд ли удастся, например из-за наличия kmalloc и vmalloc. А раз придется делать заново, то в чем сложность добавить Result/Option в новые функции выделения памяти не очень понятно.

Собственно первый ржавый патч c драйвером как-то так и делает. Там макрос типа try_allocate! который вернет либо результат аллокации либо ошибку.

Не совсем так.

Именно так.

Я сильно сомневаюсь, что простому системному язычку нужно 5 (пять) видов макросов. И никакой язык не может избавить от всех ошибок. Это просто невозможно.

Учтите, что для языка - замены С нужна возможность писать компиляторы по любому чиху для чего угодно. Это, в сущности, должен быть уровень магистрской.

А если язык сложен как Rust, написать второй компилятор очень тяжело.

В целом, требование второго компилятора вполне обосновано, и я его много раз слышал в контексте "коммититься в язык".

В целом, вот, люди пытаются. https://github.com/thepowersgang/mrustc

Нет, "я требую" не второго компилятора, а двадцать второго.

То есть, для замены текущего С, как lingua franca современных ЯВУ, должна быть спецификация, компилятор по которой усердный студент 6 курса реализует ну за пол года. Без оптимизаций, разумеется, без хитрых проверок, но вполне рабочую. Стандартной библиотеки тоже несколько реализаций.

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

А си-то сможет усердный студент реализовать за полгода? Прямо по стандарту или просто "какое-нибудь минимальное" подмножество языка?

89 сможет, кмк.

И как много кода сейчас пишется или, прости господи, компилируется в режиме С89?

Пишется, кмк, достаточно много того, что легко отпортируется на С89. Тот же ocamlrun - интерпретатор Ocaml до недавнего времени был на C89. И, не сказать, что это достоинство новых ревизий С - относительная сложность спецификации.

Кроме того, C вроде бы множество компиляторов уже есть. А для предполагаемой перспективной замены С как системного языка - нет.



Вот простейшая штука - SPARC 64. Где для него компилятор Rust'а? Хоть какой-нибудь, пусть без borrow-checker'а, без оптимизаций, но способный запустить экосистему? А как там у Эльбрусовцев - оно работает или нет?

sparc64-unknown-linux-gnu

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

Ну вот плохо - это как?

Вопрос к Эльбрусовцам в то и упирается, что коллектив разработчиков нового ЦП должен малыми силами суметь разработать компилятор системного языка. Этот самый lingua franca.

А ведь Эльбрус - это мощнейшая контора с гос. финансированием, долгими традициями и т.д. А что могут авторы какого-то исследовательского процессора?

Это, собственно, и ответ на вопрос - почему мне нужна в идеале возможность студенту написать хоть какой-то, но компилятор.

Портировал gcc и llvm, дальше оно само подтянется.

mrust как раз хорошая цель для начала портирования. Хотя без llvm ничего не выйдет, извините.

Вообще, задача неоптимизирующего минимального компилятора Rust с нативной кодогенерацией (без LLVM) - это отличная интересная область. Например, все generic'и (в вопросах кодогенерации) превращаются в динамическую диспетчеризацию... Хотя, может, лучше llvm спортировать?

Портировал gcc и llvm, дальше оно само подтянется.

llvm и gcc в отличие от спецификации c89 эволюционируют. И у маленького коллектива может просто не хватить ресурсов на поддержку.

Эльбрус — это мощнейшая контора с гос. финансированием, долгими традициями и т.д.

Не хотелось бы расстраивать стройную картину мира, но Эльбрус это никому неизвестный мелкосерийный проект из страны третьего мира с тиражом в десяток тысяч экземпляров за всю продолжительную историю, а не "мощнейшая контора".


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

Ну, то есть, все группы с исследовательскими ЦПУ идут в известное место?

Что за группы такие? Давайте предметно разговаривать, пока получается гипотетический разговор в пользу бедных — какие-то мифические группы, которые не могут осилить порт gcc/llvm, но могут осилить свой процессор и компилятор. И чего дальше они с этим компилятором делать будут? Любоваться на свой процессор? Давайте ссылку на пример таких исследователей, о которых вы говорите.

А ведь Эльбрус - это мощнейшая контора с гос. финансированием, долгими традициями и т.д. А что могут авторы какого-то исследовательского процессора?

че-то ору....

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


Вот простейшая штука — SPARC 64. Где для него компилятор Rust'а?

Я может не туда смотрю, но rustc для SPARC 64 и есть компилятор раста для SPARC 64. А насчёт эльбрусов — они что-то там хвалились насчёт поддержки ещё в начале года, но это ж чисто гипотетическая платформа, зачем ей какая-то поддержка Раста :)

А чем вообще ценность такого упражнения, кроме занятости студента?

Возможность разработки какой-то новой экосистемы, с новыми процессорами, новыми шинами и т.д.

Иначе мы завязнем в болоте x86. О, блин.

Вот что говорит о поддерживаемых платформах тулчейн LLVM у меня на компе, на который опирается компилятор раста:


Registered Targets:
aarch64 — AArch64 (little endian)
aarch64_32 — AArch64 (little endian ILP32)
aarch64_be — AArch64 (big endian)
arm — ARM
arm64 — ARM64 (little endian)
arm64_32 — ARM64 (little endian ILP32)
armeb — ARM (big endian)
avr — Atmel AVR Microcontroller
bpf — BPF (host endian)
bpfeb — BPF (big endian)
bpfel — BPF (little endian)
hexagon — Hexagon
mips — MIPS (32-bit big endian)
mips64 — MIPS (64-bit big endian)
mips64el — MIPS (64-bit little endian)
mipsel — MIPS (32-bit little endian)
msp430 — MSP430 [experimental]
nvptx — NVIDIA PTX 32-bit
nvptx64 — NVIDIA PTX 64-bit
ppc32 — PowerPC 32
ppc32le — PowerPC 32 LE
ppc64 — PowerPC 64
ppc64le — PowerPC 64 LE
riscv32 — 32-bit RISC-V
riscv64 — 64-bit RISC-V
sparc — Sparc
sparcel — Sparc LE
sparcv9 — Sparc V9
systemz — SystemZ
thumb — Thumb
thumbeb — Thumb (big endian)
wasm32 — WebAssembly 32-bit
wasm64 — WebAssembly 64-bit
x86 — 32-bit X86: Pentium-Pro and above
x86-64 — 64-bit X86: EM64T and AMD64

Мало? А теперь давайте компилятор С, кроме gcc и clang'a, который может без переписывания половины кода такой же список показать.

Справедливости ради, переписывание кода на расте, всё-же, может потребоваться.

Конечно, запустить все что угодно на любом железе — это фантазии, не более. Программа размером сто мегабайт на восьмибитном контроллере не особо запустится, даже если её и возможно было бы каким-то чудом под него скомпилировать :)

> Программа размером сто мегабайт на восьмибитном контроллере не особо запустится

Вообще-то банально — для этого нужно банкирование памяти :) (которое и так очень часто есть на таких контроллерах, но не в таких масштабах).

Но в здравом уме и твёрдой памяти, конечно, никто так делать не будет (превышение в 64 раза, как на старших PDP-11, вроде был максимум достигнутого), поэтому это замечание чисто вскользь.

В теории есть, а на практике такие компьютеры существуют? А что на них будет делать эта гипотетическая гигантская программа — запускаться первые N лет после включения устройства? :)

Емнип, была где-то статья про линукс на ардуино.

Не, я про использование, к примеру, 64-битных примитивов на 32-битных платформах и тому подобное.

А почему, собственно, без gcc ? А если я потребую у Вас Rust без llvm ?

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


А если окажется, что замечательная поддержка различных платформ у Си благодаря gcc и llvm, то это уже не так уж и сильно отличается от раста :)

А почему надо писать для новой экосистемы весь компилятор а не только бекэнд?

Весь раст прячет за абстракциями llvm, так что порт кодогенерации из llvm IR в спарковский байткод вполне решит проблему. Расту останется только указать необходимый бэкэнд. Есть неофициальная поддержка и 16-битных досов и под амигу помнится что-то делали. Это уже не говоря про зоопарк embed микроконтроллеров, часть из которых потом в спутниках вокруг земли вертится.

И вы начинаете зависеть от llvm. Нет, это путь в никуда для небольшой группы.

Кроме того, системный язык на то и системный, чтобы быть близок к текущему железу => у него должен быть достаточно простой компилятор.

И вы начинаете зависеть от llvm

Выбор небольшой — или стоять на плечах гигантов, или валяться на кладбище истории. Можно называть это "зависеть", а можно называть это "пользоваться плодами". С тем же успехом ваша небольшая группа будет "зависеть" от фаба, производящего железку физически, но уверен, что вы не будете говорить, что единственно верный путь — это использовать техпроцессы, которые может повторить студент за полгода.


системный, чтобы быть близок к текущему железу => у него должен быть достаточно простой компилятор.

Непонятно как одно следует из другого.

Насколько должна быть небольшой группа, чтобы ей понадобился отдельный компилятор под собственное не мейнстримное железо и при этом страдало бы от зависимости от llvm? Звучит как мифический персонаж из африки у которого из девайсов только телефон с парой мегабайт памяти и ллвм туда просто не влезет. Опять же встаёт вопрос каков шанс, что такая мифическая железка нужна в массовых количествах и будет иметь хоть какие-то требования к безопасности и отказоустойчивости? Мне кажется более вероятным что кто-то будет писать сразу на ассемблере для такого, чем пытаться изобразить ещё один компилятор Си.

Просто исследовательская группа в НИИ/каком-нибудь университете. Мультиклет тот же самый. Или вот - https://www.cs.utexas.edu/~trips/

Утверждение про писанину на собственном ассемблере все ещё валидно. Мультиклет тоже поставляет llvm-based компиляторы.
Нужды trisp, вероятно, оправданы, только кому сейчас нужен тот trisp? Выглядит так что они так и не перешли путь в никуда, даже с собственным компилятором. Который был нужен по сути только для выжимания скорости в академических условиях, какие в нем баги и уязвимости одним только исследователям известно.

Емнип, я перешел на С99 только из-за каких-то нюансов с хвостовой рекурсией (надо было сэкономить десяток то ли байт, то ли тактов), а так в embedded C89 вполне норм себя чувствует.

И как много кода сейчас пишется или, прости господи, компилируется в режиме С89?

Много чего для видео. FFMpeg, например. До сих пор ругается на смешанные объявления переменных и код.

Вопрос интересный. Я, с одной стороны, понимаю мотивацию требовать простой для реализации язык. Но, с другой стороны, вы же этого не требуете от ОС? Времена, когда вы могли нашкрябать аналог MS-DOS за пол-года 6 курса давно прошли, и чем дальше, тем сложнее, однако, никто не выкидывает новые компьютеры на том основании, что студент под новую умную сетевуху драйвера уже не осилит написать. Аналогично с компиляторами. Альтернативные (условно простые) реализации компилятора - да, требование охватности компилятора неподготовленным мозгом - нет, это произвольное требование.

Альтернативную библиотеку к расту уже написали/пишут (насколько оно применимо не смотрел): https://github.com/eloraiby/alt-std

Потомучто далеко не все процессоры предназначены для запуска ОС.

Разумеется. Хотя нижняя планка "можно запустить ОС" (в центах на штуку) с каждым годом всё ниже и ниже.

Можно не означает нужно. От применения зависит.

О, Брайан Кантрил совсем недавно о подобном рассказывал, что скоро вычислительные ядра будут у каждого резистора и что других альтернатив кроме раста особо и не наблюдается (потому что memory safety и другие гарантии, и потому что no_std позволяет запускать код на любом странном чипе, но при этом всё равно пользоваться чужими наработками, потому что зависимости с no_std будут прекрасно работать и что полноценная ОС трамбуется в пару сотен килобайт):


Простите, но зачем?..

Миллионы несовместимых компиляторов С (и стандартных библиотек, не забывайте еще и про них) - это огромная проблема, причем это проблемы программистов, вы вспомните, сколько ифдефов нужно написать, просто чтобы обеспечить совместимость с gcc/clang/msvc!

Когда появился С - не было интернета, распространить единый компилятор было очень трудно, а книжку - относительно легко, вот все и писали эти компиляторы..

Но это же безумие просто, повторять раз за разом одну и ту же работу, писать сотни раз одну и ту же стандартную библиотеку, наступать на те же грабли - вместо того, чтобы завести один единственно-верный (опенсорсный, разумеется) компилятор. Зачем, ради чего?!

Вот, в посте есть замечательная ссылка на блок члена комитета по стандартизации С - он прямо об этом же пишет:

(краткий пересказ-перевод)

С постоянно продает себя как "простой язык". Но это ложь! На С даже два числа сложить нельзя, не подглядывая в стандарт (ой, а не закралось ли UB?), но менеджменту говорили не это! Им говорили "любой хороший разработчик может сляпать компилятор за пару дней", эту иллюзию поддерживают все нормальные компиляторы (gcc, clang и т.д.), ведь все их оптимизации и проверки - необязательные!

На самом деле стандарт гарантирует так мало, что это просто смешно - но даже это реализовать правильно ужасающе сложно!

Именно поэтому все попытки производителей чипов сделать свой компилятор приводили лишь к забагованным кускам <вырезано цензурой>.

Now now, I say that like the compiler authors are being vindictive. The reality of the matter is that C has repeatedly and perpetually sold itself as being a “simple” language. And I mean…

Is it?

Can’t add 2 numbers together in C without consulting the holy standard about whether or not some UB’s been tripped, let alone with a well-defined way to figure out how to stop it. We recently just had to reinforce a Defect Report where we stated that “yes, even if a compiler can figure out that your array bounds are, in fact, a constant number, we have to treat the creation and usage of the array like a VLA because the Standard’s constant expression parser isn’t smart enough!”.

C is not a simple language.

That’s not what management gets told, of course. What management hears is the spicy dream, the “any good developer can bang out a C compiler drunk out of their mind on a weekend”. The way the Standard supports that dream is by making all of the good stuff people get used to in GCC, Clang, EDG or whatever else “optional” or “recommended”. What’s actually guaranteed to you by the C Standard is so pathetically miniscule it’s sad (and even that tiny little bit is still complex!). It’s why every person who ran off to “write their own C compiler” did a miserable job, why every embedded chipset thought they could roll their own C compilers and ended up with a bug-ridden mess. It’s why there are so many #ifdef __SUN_OS and // TODO: workaround, please removes that end up becoming permanent fixtures for 17 years.

Насчет vendor-specific компиляторов - это ведь реально так, даже ARM сливает свой armcc и вместо него переходит на форк clang'a, потому что сделать свой нормальный компилятор для своей же архитектуры оказалось слишком сложно!

Простите, что-то у меня пригорело слишком сильно..

Да, так вот. Я реально не вижу никаких причин активно поддерживать множество компиляторов для одного языка, это только распыляет усилия людей и создает проблемы переносимости на ровном месте, поэтому мне очень интересно, почему вы считаете, что это хорошо :)

Ну сложно не сложно, Интел бросил свой ICC в пользу Clang по одной простой причине - Clang ковыряют тысячи бесплатных разработчиков и миллионы тестировщиков. И если стандарты на С++ будут продолжать печь с той же скоростью, то чтобы обеспечить полный охват Интелу надо будет продавать свое процессорное подразделение и нанимать больше программистов компилятора. А не потому что запил архитектуры сложен...

Утрированно конечно, но в целом описывает проблему новых стандартов в целом в отрасли.

По-моему это тоже самое другими словами :) Сложно => дорого и/или долго => экономически нецелесообразно.

А зачем писать целиком компилятор, включая парсер, все эти IR, MIR, borrow checker и даже пролого-подобный солвер (https://github.com/rust-lang/chalk), если можно портировать LLVM?

А почему именно это свойство профитное? Я бы предпочел чтобы студент 6 курса смог за то же время написать формализатор на каком-нибудь Coq и мог гарантировать, что конкретная имплементация компилятора гарантирует такой-то набор гарантий пускай без все тех же оптимизаций. Такое по крайней мере будет относительно безопасно использовать.

Приоритетное свойство - простота системного языка. Из неё следует очень много, в том числе и возможность написать 22 компилятора.

Скажите, а вы железо производите или гадаете за других? Я там кидал ссылку на презентацию от реального разработчика железа, так он почему-то едва ли не молится на существующую открытую инфраструктуру и говорит, что начинать новые низкоуровневые проекты на Си — самоубийство. И это чувак, который уже лет двадцать пять программирует на этом самом Си.

Писать компиляторы ради того чтобы писать компиляторы не звучит как здравая цель. В прошлом веке конечно был бум на такое, потому что архитектуры тогдашних устройств отличались, но каждый компилятор имел кучу своих проблем, как с UB так и с производительностью, не говоря уже о специфичных багах, которые могли окуклить устройства из-за нюансов компилятора. Да и за безопасность в то время особо не думали. Ну подумаешь можно всю память компьютера прочитать вместе с паролями. Хакеров еще толком нет, худшее что могло быть — залетный фрик (phreak) который взял себе бесплатный доступ к телефонной линии. Сейчас же мир страдает от того что половина устройств в сети торчит со вшитыми дефолтными паролями, получить доступ к которому плёвое дело. Есть огромный спрос на секурность и надёжность и самописный компилятор едва ли поможет с решением этих проблем в 99.99 процентах случаев.
Вторая причина почему простота системного языка имела значение в прошлые года — нишевость программистов. Проще научить чему-нибудь простому и понятному, чтобы бизнес мог делать ХХП и грести баблишко. Риски от ошибок были относительно маленькими, да и вариантов получше не существовало в принципе. Так что простота софта и разработки это скорее исторический этап, нежели какое-то неотъемлемое свойство. Простота написания Hello world не гарантирует примерно ничего, кроме собственно вывода Hello world. Так что велик шанс что стоит пересмотреть приоритеты.

В целом, требование второго компилятора вполне обосновано,

А зачем?

Ну это же системный язык, на котором можно писать OS для любой экзотической архитектуры, для которой нет хорошо отлаженной системы llvm. Например, для Эльбруса. ;-)

Потому что люди положили все яйца в корзину Watcom C, и где он теперь? А если бы он был единственным компилятором, то и вся кодовая база превратилась бы в стремительно устаревающую тыкву.

Требование более чем разумное.

Но в итоге сейчас всё скатывается в лучшем случае в дуополию gcc/llvm, а все остальное звучит как заявка на выстрел в ногу уже через пять-десять лет.

Ну, как минимум, есть MS и LCC, из того, что я могу назвать.

Если бы он был единственным компилятором, то, вероятно, не умер бы.


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

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

Смотрели и даже используют. Компиляторов С++ достаточно много.

Можно примеры двух альтернативных ненаколенных компиляторов C++, скажем, в 2010-м под линукс?

Я не уверен насчёт OpenWatcom, но ICC точно был.

ICC был, но я его вообще ни разу нигде не видел в бою.


Де-факто gcc альтернатив не было.

но я его вообще ни разу нигде не видел в бою

Я видел. В 2000 он крешил ядро операционки на темплейтах - сам компилятор,kernel panic при запуске компиляции от юзера 8). Где то в 2004-2007 давал очень неплохую SIMD оптимизацию, раскладывал циклы в 4xfloat32, выигрывая у gcc до 40 процентов. На немного более нетривиальном коде с ветвлениями слегка медленнее чем gcc. Потом AMD немного поучаствовала в GCC и он стал побыстрее. А так было время что часть кода у нас собиралась под icc , а часть под gcc.

В известной ветке на IXBT про системы команд процессоров народ наоборот, ничего никогда с помощью gcc в PROD не компилирует. :-) Мир большой.

Можно примеры двух

1 . g++ -GNU

2 . icc -Intel

А за пределами x86 жизни нет?

А за пределами x86 жизни нет?

??? icc -это от вендора . Естественно что интел пишет для интеля. Для ARM - Arm Compiler for Linux in Arm Allinea Studio : https://developer.arm.com/tools-and-software/server-and-hpc/compile/arm-compiler-for-linux

Не альтернативы:

gcc

clang

Альтернативы:

intel C++ compiler

AOCC

arm compiler

cl (MS)

А помните был такой IAR embedded compiler?)

Не под линукс и только под arm, но все же)))

Да, и на совместимость с новыми фичами сишки или плюсов я плевки от народа слышал постоянно.

А что, IAR тоже свой компилятор дропнули?

Intel C++ (ICC) уже упомянули. Ещё был open64, причём в двух инкарнациях — одна сильно подточенная AMD и одна полунезависимая.
Я тогда работал на HPC тематику (хоть и вскользь) и мы его там пробовали. На каких-то математических пакетах он давал код быстрее и GCC, и (тогда ещё только начинавшегося) Clang, и ICC.

Можно поспорить, что Rust переживёт некоторые из таких компаний :)

Некоторые - конечно. Но кто-то будет на нём (или другом языке) писать код с сроком сопровождения 50+ лет (условная электростанция или метро). И их соображения вполне разумны.

И поэтому код условной электростанции будет написан один раз на %vendorname% версии С, который собирается единственным компилятором от производителя использованных микроконтроллеров, который однажды засертифицировал конкретную версию, а потом скончался через 25 лет.


:)


Но требование разумное, я его вполне понимаю.

А вы точно уверены, что некая АСУТП система сопровождается 50 лет? Насколько я знаю, к тому же самому технологическому оборудованию подключают новые шкафы, написанные на другом языке, стандартном на момент внедрения. Срок службы автоматики - 20 лет.

Это просто часть кластера болезненных точек, не причина, разумеется, но единственность ghc сильно связана с:

Производительность ghc - это единственный компилятор, поэтому невозможно сделать быстрый компилятор Хаскеля. (быстрый компилятор почти Хаскеля возможен - это cocl, компилятор Клина - 0.2 сек полная сборка страниц 10 текста, использующих стандартные библиотеки на Core 2 Duo T9500).

Портируемость на всякое экзотическое - это единственный компилятор, поэтому увы и ах.

Простота разворачивания - это единственный компилятор, поэтому об "./configure; make world opt -j 40" забудьте.

Удаление невзлетевших фич, вроде backpack - это единственный компилятор.

Эксперименты с разными подходами, например в духе MLton/Stalin - забудьте. Хотя в случае ленивых языков это могло бы быть крайне интересно.

Где встраиваемый Хаскель, который может занять место Lua? GHC - единственный компилятор.

-----------------------
Разумеется, часть вещей реализована в том же Клине или Ocaml, которые тоже единственные компиляторы. Но к ним я могу тоже набросать массу претензий.

Производительность ghc — это единственный компилятор, поэтому невозможно сделать быстрый компилятор Хаскеля. (быстрый компилятор почти Хаскеля возможен — это cocl, компилятор Клина — 0.2 сек полная сборка страниц 10 текста, использующих стандартные библиотеки на Core 2 Duo T9500).

Тормозят всякие продвинутые фичи в системе типов. Что-нибудь тупое вроде haskell98 компилируется весьма быстро — модуль с 250 функциями вида foo_k n = n + k у меня собрался за 0.8 секунд, пустой модуль — за 0.6 секунд (там линковка отчего-то долго занимает).


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

А, по-вашему, легче портировать имеющийся компилятор, для этого предназначенный, или написать с нуля под новую платформу?


Простота разворачивания — это единственный компилятор, поэтому об "./configure; make world opt -j 40" забудьте.

Ну это увы, да, согласен.


Удаление невзлетевших фич, вроде backpack — это единственный компилятор.

Почему? Фичи вполне выпиливаются и меняются, иногда со сломом обратной совместимости. Просто оказывается, что выпиливать надо не так много фич (а то, что надо — оказывается, что проще сделать новый язык с нуля и назвать его, например, идрисом).


Эксперименты с разными подходами, например в духе MLton/Stalin — забудьте. Хотя в случае ленивых языков это могло бы быть крайне интересно.

Есть огромная куча экспериментальных форков, а лёгкость написания плагинов означает, например, что вы можете под тот же блокчейн (cardano) писать код на более-менее обычном хаскеле, а потом отдельный плагин сгенерирует код для блокчейна. Отдельный компилятор не нужен.

А, по-вашему, легче портировать имеющийся компилятор, для этого предназначенный, или написать с нуля под новую платформу?

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

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

а то, что надо — оказывается, что проще сделать новый язык с нуля и назвать его, например, идрисом).

Кмк, Хаскель давно надо было "заморозить". И работать с новыми языками.

Отдельный компилятор не нужен.

Это означает, кстати, болезненную зависимость от upstream'а.

Кмк, Хаскель давно надо было "заморозить". И работать с новыми языками.

Смотря для чего. Для ресёрча — хз. Для промышленного применения — очевидно, нет, там, вопреки стереотипам, уже достаточно кода написано.


Это означает, кстати, болезненную зависимость от upstream'а.

Так в чём болезненность?

Так в чём болезненность?

И это вопрос сейчас, после того, как SPJ ушёл? :-)

Я за политикой слежу мало, а в чём вопрос?

Тормозят всякие продвинутые фичи в системе типов.

Ну нет. Компиляция сгенерированного кода для одной и той же xsd схемы на Ocaml PPX и на Template Haskell отрабатывают за секунду и 30 минут соответственно. Причём что там, что там отображения с одних и тех же алгебраических типов. Просто огромного кол-ва.

Ну и компиляция строк 1000 за 0.6 секунд - это тоже перебор. Я прекрасно знаю, что это именно проблема культуры группы вокруг Ghc - те же Clean/Ocaml собирают такое мгновенно.

Просто Ocaml'щики маниакально оптимизируют компилятор и экосистему для скорости (к той же dune масса претензий, но не скорость). А люди вокруг Ghc забивают - stack на собранном проекте секунду что-то внутри себя думает. Хотя make и dune в той же ситуации отрабатывают мгновенно.

Компиляция сгенерированного кода для одной и той же xsd схемы на Ocaml PPX и на Template Haskell отрабатывают за секунду и 30 минут соответственно. Причём что там, что там отображения с одних и тех же алгебраических типов. Просто огромного кол-ва.

Так в TH небось обмазывание всякими дженериками потом и прочим в deriving, в отличие от окамля. Вот это уже будет тормозить, да, но дженерики — это уже продвинутая фича.


Я прекрасно знаю, что это именно проблема культуры группы вокруг Ghc — те же Clean/Ocaml собирают такое мгновенно.

Это проблема приоритетов. Запилить новые фишки в систему типов интереснее и, я бы сказал, важнее, чем улучшить скорость компиляции. Но и последнее не забывают и над этим работают, и скорость компиляции в TODO-списке всяких важных людей и организаций.


И я бы сказал, что фишки в типах действительно важнее. В конце концов, есть репл, есть LSP, скорость компиляции в процессе разработки не особо важна, на самом деле.


А люди вокруг Ghc забивают — stack на собранном проекте секунду что-то внутри себя думает. Хотя make и dune в той же ситуации отрабатывают мгновенно.

stack делает далеко не то же, что make.

Вот это уже будет тормозить, да, но дженерики — это уже продвинутая фича.

Клин с дженериками не тормозит.

В конце концов, есть репл, есть LSP, скорость компиляции в процессе разработки не особо важна, на самом деле.

До REPL тоже надо добраться. В вышеупомянутом случае я ждал эти 10 минут/пол часа.

stack делает далеко не то же, что make.

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

Это проблема приоритетов.

Это в первую очередь проблема единого компилятора. Было бы их два, второй мог быть промышленного качества и не тормозить.

Клин с дженериками не тормозит.

Я что-то сомневаюсь, что вы под дженериками понимаете одно и то же. Дэдфуд под ними понимает вот это.

Понял, был неправ, беру свои слова обратно.

Клин с дженериками не тормозит.

Забавно, не знал, что в клине есть дженерики. Теперь мне уже интересно, что там за магия по сравнению с ghc'шной. Ну, просто когда над одним компилятором работает куча людей, в том числе на зарплате, заинтересованных в его качестве, и он оказывается медленнее другого, существенно менее популярного языка, на аналогичных по фичам исходниках — это как-то странно.


До REPL тоже надо добраться. В вышеупомянутом случае я ждал эти 10 минут/пол часа.

Это точно не была первая сборка проекта, когда там зависимости подтягиваются?


У меня какое-то серьёзное ожидание бывает только в тех проектах, где надо серьёзно обмазываться типами (правда, таких проектов большинство, увы).


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

Ну, запустил на каком-то своём проекте холостой time stack build --fast (я его без оптимизаций собираю, они там не нужны) — 0.45 секунд. Запустил с --verbose — получил


такое
Version 2.7.3, Git revision 7927a3aec32e2b2e5e4fb5be76d0d50eddcc197f x86_64 hpack-0.34.4
2021-11-30 14:04:03.051650: [debug] Checking for project config at: /home/d34df00d/Programming/necogda/stack.yaml
2021-11-30 14:04:03.051781: [debug] Loading project config file stack.yaml
2021-11-30 14:04:03.055321: [debug] (SQL) SELECT COUNT(*) FROM "last_performed" WHERE ("action"=?) AND ("timestamp">=?); [PersistInt64 1,PersistUTCTime 2021-11-29 20:04:03.055300791 UTC]
2021-11-30 14:04:03.055690: [debug] Using package location completions from a lock file
2021-11-30 14:04:03.057504: [debug] Loaded snapshot from Pantry database.
2021-11-30 14:04:03.236940: [debug] RawSnapshotLayer < тут 250 килобайт текста с текущими пакетами в снепшоте >
2021-11-30 14:04:03.290311: [debug] Running hpack on /home/d34df00d/Programming/necogda/package.yaml
2021-11-30 14:04:03.294792: [debug] hpack output unchanged in /home/d34df00d/Programming/necogda/necogda.cabal
2021-11-30 14:04:03.296550: [debug] Asking for a supported GHC version
2021-11-30 14:04:03.297149: [debug] Installed tools: 
 - ghc-tinfo6-8.10.3
 - ghc-tinfo6-8.10.7
 - ghc-tinfo6-8.10.5
 - ghc-tinfo6-8.10.4
 - ghc-tinfo6-8.8.4
 - ghc-tinfo6-8.10.6
 - ghc-tinfo6-8.10.2
 - ghc-tinfo6-8.8.3
 - ghc-tinfo6-8.6.5
2021-11-30 14:04:03.297392: [debug] Run process: /sbin/ldconfig -p
2021-11-30 14:04:03.298620: [debug] Process finished in 1ms: /sbin/ldconfig -p
2021-11-30 14:04:03.299264: [debug] Did not find shared library libtinfo.so.5
2021-11-30 14:04:03.299299: [debug] Found shared library libtinfo.so.6 in 'ldconfig -p' output
2021-11-30 14:04:03.299339: [debug] Found shared library libncursesw.so.6 in 'ldconfig -p' output
2021-11-30 14:04:03.299378: [debug] Found shared library libgmp.so.10 in 'ldconfig -p' output
2021-11-30 14:04:03.299435: [debug] Did not find shared library libgmp.so.3
2021-11-30 14:04:03.299459: [debug] Potential GHC builds: tinfo6, ncurses6
2021-11-30 14:04:03.299496: [debug] Found already installed GHC builds: tinfo6
2021-11-30 14:04:03.299602: [debug] (SQL) SELECT "id","actual_version","arch","ghc_path","ghc_size","ghc_modified","ghc_pkg_path","runghc_path","haddock_path","cabal_version","global_db","global_db_cache_size","global_db_cache_modified","info","global_dump" FROM "compiler_cache" WHERE "ghc_path"=?; [PersistText "/home/d34df00d/.stack/programs/x86_64-linux/ghc-tinfo6-8.10.7/bin/ghc-8.10.7"]
2021-11-30 14:04:03.362765: [debug] Loaded compiler information from cache
2021-11-30 14:04:03.362887: [debug] Asking for a supported GHC version
2021-11-30 14:04:03.363125: [debug] Resolving package entries
2021-11-30 14:04:03.363172: [debug] Parsing the targets
2021-11-30 14:04:03.365413: [debug] Checking flags
2021-11-30 14:04:03.365455: [debug] SourceMap constructed
2021-11-30 14:04:03.370510: [debug] Starting to execute command inside EnvConfig
2021-11-30 14:04:03.373375: [debug] Finding out which packages are already installed
2021-11-30 14:04:03.373466: [debug] Run process: /home/d34df00d/.stack/programs/x86_64-linux/ghc-tinfo6-8.10.7/bin/ghc-pkg-8.10.7 --global --no-user-package-db dump --expand-pkgroot
2021-11-30 14:04:03.406968: [debug] Process finished in 33ms: /home/d34df00d/.stack/programs/x86_64-linux/ghc-tinfo6-8.10.7/bin/ghc-pkg-8.10.7 --global --no-user-package-db dump --expand-pkgroot
2021-11-30 14:04:03.416051: [debug] Run process: /home/d34df00d/.stack/programs/x86_64-linux/ghc-tinfo6-8.10.7/bin/ghc-pkg-8.10.7 --user --no-user-package-db --package-db /home/d34df00d/.stack/snapshots/x86_64-linux-tinfo6/13fa040522c82079f8fcb58f0649cdd5905ee24cc49d2e88e8f1a2a03c8fcec9/8.10.7/pkgdb dump --expand-pkgroot
2021-11-30 14:04:03.494927: [debug] Process finished in 79ms: /home/d34df00d/.stack/programs/x86_64-linux/ghc-tinfo6-8.10.7/bin/ghc-pkg-8.10.7 --user --no-user-package-db --package-db /home/d34df00d/.stack/snapshots/x86_64-linux-tinfo6/13fa040522c82079f8fcb58f0649cdd5905ee24cc49d2e88e8f1a2a03c8fcec9/8.10.7/pkgdb dump --expand-pkgroot
2021-11-30 14:04:03.503268: [debug] Run process: /home/d34df00d/.stack/programs/x86_64-linux/ghc-tinfo6-8.10.7/bin/ghc-pkg-8.10.7 --user --no-user-package-db --package-db /home/d34df00d/Programming/necogda/.stack-work/install/x86_64-linux-tinfo6/13fa040522c82079f8fcb58f0649cdd5905ee24cc49d2e88e8f1a2a03c8fcec9/8.10.7/pkgdb dump --expand-pkgroot
2021-11-30 14:04:03.522352: [debug] Process finished in 19ms: /home/d34df00d/.stack/programs/x86_64-linux/ghc-tinfo6-8.10.7/bin/ghc-pkg-8.10.7 --user --no-user-package-db --package-db /home/d34df00d/Programming/necogda/.stack-work/install/x86_64-linux-tinfo6/13fa040522c82079f8fcb58f0649cdd5905ee24cc49d2e88e8f1a2a03c8fcec9/8.10.7/pkgdb dump --expand-pkgroot
2021-11-30 14:04:03.522887: [debug] Constructing the build plan
2021-11-30 14:04:03.525400: [debug] (SQL) SELECT "id","directory","type","pkg_src","active","path_env_var","haddock" FROM "config_cache" WHERE "directory"=? AND "type"=?; [PersistText "/home/d34df00d/Programming/necogda/.stack-work/install/x86_64-linux-tinfo6/13fa040522c82079f8fcb58f0649cdd5905ee24cc49d2e88e8f1a2a03c8fcec9/8.10.7/",PersistText "lib:necogda-0.1.0.0-DOEEJaDfef8BQb8WOzmppB"]
2021-11-30 14:04:03.525768: [debug] (SQL) SELECT "id", "config_cache_id", "index", "option" FROM "config_cache_dir_option" WHERE ("config_cache_id"=?) ORDER BY "index"; [PersistInt64 7]
2021-11-30 14:04:03.525934: [debug] (SQL) SELECT "id", "config_cache_id", "index", "option" FROM "config_cache_no_dir_option" WHERE ("config_cache_id"=?) ORDER BY "index"; [PersistInt64 7]
2021-11-30 14:04:03.526115: [debug] (SQL) SELECT "id", "config_cache_id", "ghc_pkg_id" FROM "config_cache_dep" WHERE ("config_cache_id"=?); [PersistInt64 7]
2021-11-30 14:04:03.526483: [debug] (SQL) SELECT "id", "config_cache_id", "component" FROM "config_cache_component" WHERE ("config_cache_id"=?); [PersistInt64 7]
2021-11-30 14:04:03.526877: [debug] Start: getPackageFiles /home/d34df00d/Programming/necogda/necogda.cabal
2021-11-30 14:04:03.539345: [debug] Finished in 12ms: getPackageFiles /home/d34df00d/Programming/necogda/necogda.cabal
2021-11-30 14:04:03.543139: [debug] Checking if we are going to build multiple executables with the same name
2021-11-30 14:04:03.543184: [debug] Executing the build plan

То есть, стэку ещё надо выяснить, что там надо собирать.


При этом зря вы так про make и dune — make может работать произвольно долго. Сгенерированный cmake'ом вхолостую на одном моём проекте занимает 8 секунд (с -j12, в одном потоке будет медленнее). На прошлой работе была своя наркоманская билдсистема, генерировавшая мейкфайлы, там тоже оно собиралось не за секунду. Короче, make тормозит (не тормозил бы — не сделали бы ninja).


С dune я работал ровно один раз, и мне откровенно лень настраивать окружение для окамля снова, но у меня не было впечатления, что dune на собранном проекте отрабатывает моментально.Это в первую очередь проблема единого компилятора. Было бы их два, второй мог быть промышленного качества и не тормозить.


Это в первую очередь проблема единого компилятора. Было бы их два, второй мог быть промышленного качества и не тормозить.

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

То есть, стэку ещё надо выяснить, что там надо собирать.

Очевидно, это можно было как-то оптимизировать по скорости. Было бы желание. Но у сообщества вокруг Ghc этого желания нет.

Без nfs сейчас dune отрабатывает за

А так — кто мешает потратить силы на то, чтобы запилить это же в первый?

Это шутка?

Ну, просто когда над одним компилятором работает куча людей, в том числе на зарплате, заинтересованных в его качестве, и он оказывается медленнее другого, существенно менее популярного языка, на аналогичных по фичам исходниках — это как-то странно.

Фичи, разумеется, не аналогичные, далеко не аналогичные. Но конкретно на сборках быстрее.

Очевидно, это можно было как-то оптимизировать по скорости. Было бы желание. Но у сообщества вокруг Ghc этого желания нет.

Вообще-то есть, и stack несколько лет назад работал сильно медленнее, чем сейчас. А по сравнению с олдовым cabal, который на любой чих задумывается на пару минут со словами «Resolving dependencies…», он вообще молниеносный.


Без nfs сейчас dune отрабатывает за

Так и не понял, за сколько :]


Это шутка?

Нет. Что проще — запилить компилятор, включая тайпчекер, или запилить только конкретные улучшения по производительности в уже готовый и оттестированный компилятор?


Кстати, сколько компиляторов у clean?

Так и не понял, за сколько :]

Core2 Duo T9500, Linux 64-bit; 0.14 сек первый запуск на ppx (3 файла), 0.04 сек - последующие. Проект собран. А я - лох, конечно.

Кстати, сколько компиляторов у clean?

Я же уже писал, что один. Есть ещё где-то компилятор eClean,но он закрыт и точно без генериков и динамики. Это один из недостатков Клина, а их вообще немало.

Но Клин показывает, что можно сделать быстрый компилятор Хаскеле-подобного языка, выдающий более-менее сносный код. Точно также, как Caml Light показывал, что можно сделать хороший компилятор для языка CAML.

Хотелось бы ещё иметь аналог MLton для Клина или Хаскеля. Но очевидно, что такой компилятор не должен быть единственным.

Я же уже писал, что один.

Ну вот видите, наличие одного компилятора не помешало клину иметь быструю скорость сборки.


Олсо, компиляторов хаскеля когда-то было дофига (и сейчас они всё ещё технически есть, но именно что технически). Просто оказалось, что одного ghc достаточно.

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

Я уверен, что какой-нибудь аналог Stalin'а смог бы выжать из Хаскеля больше за счёт хорошего понимания того, где там ленивость нужна, а где - нет.

К слову об отсутствующем желании, ковырял тут поддержку ghc 9.2 для hls — наткнулся на такое, тут чувак очень сильно ускорил скорость HLS. Что, в принципе, разумно, фокусироваться на language server'е куда разумнее, чем на самой сборке, на удобство и удовольствие от разработки оно влияет куда больше.


Кстати, чувак на зарплате у фейсбука. Походу они там не только антиспам в команде Марлоу пилят.

Так Linux был далеко не монопольной OS.

A Watcom C был open-source? Если вдруг (хотя это очень-очень-очень маловероятно) развитие раста повернет куда-то не туда, не проще ли форкнуть уже существующий отлаженный компилятор, чем писать собственный? Хотя я все равно не понимаю, зачем вам иметь зоопарк компиляторов, чтобы потом мучится и писать проекты, которые должны будут компилироваться под всеми из них и в итоге не использовать ни один из них на полную катушку, зато иметь лес затычек багов то одного, то другого.

Watcom C не был opensource, но в 2003 году он стал OpenWatcom, некоторое время даже развивался. Последний стабильный релиз от 2010 года.
НЛО прилетело и опубликовало эту надпись здесь
А Вы сами ещё не устали от перманентных обновлений языков и необходимости обновлять кодовую базу по временным веяниям?

Зависит от языка, конечно, но в среднем не устал. Напротив, нередко с нетерпением жду релиза новых версий компиляторов с новыми фичами.


Плевать на ухудшающийся функционал и производительность

Производительность тоже растёт. Я как-то просто пересобрал код компилятором после примерно двух лет его развития, и получил прибавку процентов в 20-30 в скорости выполнения.


Новые оптимизации компилятора завозят, оптимизации рантайма завозят, и так далее.

НЛО прилетело и опубликовало эту надпись здесь

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

НЛО прилетело и опубликовало эту надпись здесь

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

> Если не видите необходимости не пользуйте новые плюшки

Это некорректный подход. Обновление может быть вызвано совершенно посторонним фактором: например, для нового целевого устройства нужно новое ядро, дистрибутивы с ним содержат новые версии binutils и прочего обрамления, а старый компилятор с ними уже несовместим. Так бы компилятор никто не трогал, он устраивает, но компилятор сейчас не живёт один на голом железе.

> Все просто же

Нет.
Это некорректный подход. Обновление может быть вызвано совершенно посторонним фактором: например, для нового целевого устройства нужно новое ядро, дистрибутивы с ним содержат новые версии binutils и прочего обрамления, а старый компилятор с ними уже несовместим. Так бы компилятор никто не трогал, он устраивает, но компилятор сейчас не живёт один на голом железе.

Желание собирать под новое целевое устройство и есть "видеть необходимость в обновлении"

> Желание собирать под новое целевое устройство и есть «видеть необходимость в обновлении»

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

Сочувствую промышленникам, их любой мельчайший вопрос разрывает тут пополам.

Нет. PL/I с нами на веки.

"Кресты" вот в области стандартов и обновлений очень хороши. У них море недостатков, но вот конкретно стандарты, обратная совместимость на уровне.

> но вот конкретно стандарты, обратная совместимость на уровне.

Хорошая шутка, спасибо.

Можно начинать смотреть отсюда (всю ветку, там много весёлых гитик).

Или на cppreference.com отменённые совсем в каких-то ревизиях возможности (например, auto_ptr в C++17 уже нет — ну да, GCC рисует для совместимости).

Я сформулировал таки требование, которое у меня есть - простая для реализации стабильная (на ближайшие 5-10 лет) спецификация языка и ядра системной библиотеки. Если это системный язык, то, кмк, это вполне обосновано.

Из этого следует возможность этих 22 компиляторов и прочего. Завязанность на единственный llvm, приведёт к тому, что модульная независимая система превратится в кубик-рубик-монолит. Это закроет возможность развития.

У языка есть поддержка стабильных редакций. Условно говоря, в 2021 вы как писали на Rust-2018, так и пишите, и даже линковаться с либами на rust-2021 можете.

Вот llvm вопрос открытый. Уж очень удобно иметь единый бэкэнд.

Вот llvm вопрос открытый. Уж очень удобно иметь единый бэкэнд.

В инженерии всё всегда имеет свою цену. Если мы/вы думаем, что что-то бесплатно, это повод её поискать.

У языка есть поддержка стабильных редакций. Условно говоря, в 2021 вы как писали на Rust-2018

Можете называть это эстетическим чувством, но мне не нравится, когда экосистема превращается в кубик-рубик-монолит. С единым компилятором, большим кол-вом пакетов в cargo, и llvm оно неизбежно превратится в нечто такое необозримое и неизменное.

В инженерии всё всегда имеет свою цену. Если мы/вы думаем, что что-то бесплатно, это повод её поискать.

И какая цена отказа от того, во что вложены миллионы человеко-часов? :)

Это создает некоторую конкуренцию, что лучше чем монополия.

Я сильно сомневаюсь, что простому системному язычку нужно 5 (пять) видов макросов

Как вы их столько насчитали? В Rust есть только две разновидности макросов: декларативные и процедурные.

Декларативные двух видов (macro_rules! и нестабильный macro), процедурные - трёх (derive, attribute, function-like).

С точки зрения писателя макроса — два с половиной вида, с точки зрения пользователя — один (function-like, а как именно реализован — неважно).

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

Вот именно. Си намного сложнее Раста, не в плане синтаксиса, а в понимании того, как на самом деле будет работать программа

>сложность языка имеет свойство перекладываться на сложность программ

  1. В расте очень много сахара, который частично компенсирует сложность bc.

  2. Зависит от масштаба. Мелкая утилита/библиотека на расте будет чуть сложнее. Зато на крупном проекте будет меньше граблей и головной боли с отладкой сегфолтов.

Ключевое слово — кажется. Раст просто в язык добавляет явно некоторые концепции (вроде времени жизни объекта) которые и в Си и в Сипипи существуют, но на уровне "в голове у разработчика". Шаг же от динамики (в голове у разработчика) к статике (записано в типах) всегда же должен приветстоваться, тем более в таких важных вещах как системное ПО.

Ну, справедливости ради, самое близкое это всё же режим Better C в Dlang. Там часть самострелов в ногу устранено. Но главный плюс это то, что перетекать на этот компилятор можно начать плавно с уже имеющейся кодобазой.

Имеющаяся codebase, которая основна на цементировании UB, это, скорее, минус. Собственно, поговорка, что Rust - это C++ из которого убрали С.

Чисто ради интереса. А как оно происходит в Си?
Программа ождидает что здесь по адресу лежит положительное число. Пришло отрицательное. Код не упал и продолжил работать с отрицательным числом.

Как это работает?

Если это энкапсулированное число (т.е. код его не использует), то просто оставляет как есть. Если использует, но в виде "просто данных" (например, делает +1), то из -1 становится 0. Если это смещение к указателю, то UB. Что будет, то будет, а CVE не миновать.

(Это в отсутствие явной проверки в коде)

> Программа ождидает что здесь по адресу лежит положительное число. Пришло отрицательное. Код не упал и продолжил работать с отрицательным числом.

А общего рецепта не будет, всё зависит от конкретного кода.

Ну например вы пишете

for (int i = 0; i < n_elems; ++i) {
  что-то делаем
}


будет пустой цикл.

А если вы сделаете

for (int i = 0; i != n_elems; ++i) {
  что-то делаем
}


то оно пойдёт выполняться по всем положительным, заврапится и на отрицательные числа (4 миллиарда проходов не хотите?)

А если компилятор опознает, что там отрицательное — то может вообще выкинуть цикл как неисполнимый. Вопрос, дадут ли ему это опознать — зависит от погоды на Юпитере и фазы лун Альдебарана (точнее, от всего входного кода и версии компилятора).

Но специфика оптимизации тут только в этом выкидывании, код и так был некорректный. В идеале надо было бы какой-нибудь assert на предусловие n_elems >= 0… хотя <= и так помогает неплохо. Поэтому при любой возможности тут лучше делать такие проверки.

(GCC, что характерно, обычно превращает такой цикл в сначала проверку что n_elems >= 1, а затем уже в теле цикла меняет <= на !=. Вот удобнее ему так. Но имеет право. А вот если бы совсем выкидывал проверку — был бы неправ.)

cc @amarao@vanxant

Я не, видимо, не правильно задал вопрос что его смысл потерялся.

Я немного писал код на С в студенчестве. У меня есть идеи, как себя поведет компилятор.

Вопрос именно в контексте аргументов Торвальдса. Как пишут драйверы, что они устойчевы в повреждению памяти? Весь мой опыт (10 лет, 3 основных и десяток вспомогательных языков) - говорит что оно все равно упадет. Что даже, если каким-то чудом, драйвер продолжит работу - вероятность поломки/потери чего-то ценного огромна.

Можно, например, написать какой-то глобальный цикл (или демона де-факто) на 10 строк. Он никогда принципиально не упадет. И любую логику делать внутри цикла и трая. Тогда ошибки перехватываются и *формально* драйвер продолжает работать. Но этот паттерн применим к любому языку.

Я уже молчу про контраргумент в дргом сообщении - из С тоже можно вызвать аналог паники в любом месте.

> Вопрос именно в контексте аргументов Торвальдса. Как пишут драйверы, что они устойчевы в повреждению памяти?

Они не устойчивы. Но в Linux есть защита, что если драйвер повредил только собственные структуры, то ядро хотя бы успеет сказать «мяу» и скинуть лог проблемы. Срабатывает не всегда, но достаточно часто, чтобы собирать трейсы, логи и прочие данные, по которым можно понять, что случилось.
А в остальном — повезёт или не повезёт.
Проблема с Rust, насколько я вижу, в интеграции его рантайма. Пока что есть с этим проблемы — в отличие от C, которому в варианте ядра Linux достаточно было настроить стек на кусок RAM, argc+argv и какой-то простейший malloc, дальше он уже работал.
Но, думаю, их решат без особых затяжек.

Если я всё ещё не на то отвечаю — переформулируйте.

Нет, теперь понятно, спасибо.

Но опять же получается что это не проблема Раста - а лок линукса на си

Нет, нет никакого лока. Ядро стартует когда нет ничего, кроме непрерывного куска памяти с кодом и процессора. В этом режиме очень хочется не отвлекаться на всякие магические процедуры, которые непонятно когда запускаются и сколько времени тратят. А вот когда ядро стартануло, запустило все дрова и инициализировало вверенное оборудование, то тогда можно и свистелки-перделки запускать.

"Лок" не всмысле управления памятью и процессами. А в смысле кода захардкодженного на определенное поведение определенной реализации. Которое, как я понимаю, еще и не является частью стандарта.

memset - это стандартная функция. Она есть в стандарте. Более того, сам Линукс к подобным функция не привязан. Более того, если не ошибаюсь, там вообще стандартная библиотека в ядре отсутствует.

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

Мне (и возможно не только) очень интересно за что минус. Потому что аргументация в комментарии мне понятна (хотя я все еще не согласен с Торвальдсом).

Но я не сишник, чтобы самому знать что в нем не верно.

Тут кто-то очень щедро минусы расставляет. У меня почти все комментарии с минусами тут. :-)

Ну скорее не на C, а на конкретные ABI его реализации. Но C тут тем хорош, что его рантайм имеет очень ограниченные требования — фактически, только обеспечение стеком, правила передачи аргументов/результата и явно вызванные функции библиотеки. И под конкретный ABI на конкретной платформе можно уже и подстроить заточку. Уже с C++ так легко не получилось (хотя в причинах я не уверен — можно было бы уточнить в режиме без исключений и RTTI).

Вы не поняли. Ядро это приложение не "пользовательского пространста", где аварийное завершение работы, в общем, не является катастрофой. Как само ядро, так и его модули должны иметь возможность продолжать работу даже при "обстоятельствах непреодолимой силы", до тех пор, пока позволяет железо, а неотключеамое состояние паники совершенно не вписывается в сценарий использования.

Но ведь такие обстоятельства существуют и для C, например UB. Почему вызов паники из C-кода ядра - хорошо, наличие UB в C разработчики ядра терпеть готовы, а панику рантайма Rust - нет?

Причины для паники могут быть очень разными.
Как тут пишут - Раст паникует если не может выделить память.

Если же драйвер в ядре не может выделить память - он просто возвращает ошибку и все работает дальше.

Но на самом деле проблема с паниками - это не самое страшное. Я тут недавно читал цикл постов про различия в моделях памяти между растом и ядром. Там все куда сложнее. Хотя и менее очевидно.

Эти штуки из разных плоскостей. Undefined Behavior не означает аварийное завершение работы, тем более в пространстве ядра, а является формальным описанием области значений, при нарушении инварианта, которые будут отличаться от ожидаемых. Например, если Вы нарушаете закон, то Вас могут найти правоохранители и наказать, а могут не найти и всё будет ок.

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

А процессору пофиг, он вообще не знает, знаковые у него там числа или беззнаковые (исключая инструкции умножения и деления)

Ну а так, мусор на входе - мусор на выходе.

Потом, правда, ракеты падают и десятичная точка в сумме при банковском переводе сдвигается.

Ракета упала там, где C как раз не виноват :(
Накосячить таким методом можно с любым языком.

Процессору пофиг. А вот компилятору нет. Сегодня ты компилмруешь для процессора с int=int32, а завтра int=int64, и твоя кривая проверка работать перестанет. Поэтому разработчики стандарта языка и компиляторов делают такие вещи. По мне, если ты делаешь какую-то дичь, которая не соответствует стандарту языка, ты обязан обложить ее проверками на архитектуру системы и написать кучу комментариев, почему ты это сделал, и как оно вообще работает. По-хорошему, компилятор должен ругаться, когда выкидывает часть кода. Ну, ненормально это в 99% случаев.

твоя кривая проверка работать перестанет

Прикол в том, что в Си, когда я последний раз в него смотрел, нет стандартного способа сделать проверку переполнения. В отдельных компиляторах есть свои методы типа __builtin_add_overflow_p, но не во всех, и главное в стандарте языка их нет. Крутитесь как хотите.

При том что спор, тащем-та, о трёхстрочных функциях. Допустим, у нас есть

int a, int b;

Для проверки на то, не было ли переполнения при выполнении условной операции ⊕, достаточно написать

(long)(a ⊕ b) == ((long) a) ⊕ ((long) b)
> Для проверки на то, не было ли переполнения при выполнении условной операции ⊕, достаточно написать

> (long)(a ⊕ b) == ((long) a) ⊕ ((long) b)

А теперь повторите этот фокус, например, с 64-битным long при отсутствии поддержки 128-битного типа.

А потом вспомните цену поддержки 64-битного long long на 32-битной платформе и, соответственно, цену такой проверки, по сравнению с более близкой к возможностям машины (которые обычно делают проще).

Заметьте, я сказал написать (на Си), а не скомпилировать под конкретный процессор. В комитете не совсем идиоты сидят, наверное, что столько лет не пускают это в стандарт.

> Заметьте, я сказал написать (на Си), а не скомпилировать под конкретный процессор.

Отлично, и как вы обеспечите этот фокус на long значениях, если вдвое более широкого типа данных просто не дают (по стандарту)?

> В комитете не совсем идиоты сидят, наверное, что столько лет не пускают это в стандарт.

В комитете сами по себе — безусловно, не идиоты. А люди, защищающие бизнес-интересы конкретного представителя (чаще всего — компиляторов) так, как эти интересы понимают владельцы их фирм. И эти интересы не совпадают в подавляющем большинстве случаев.

Отлично, и как вы обеспечите этот фокус на long значениях, если вдвое более широкого типа данных просто не дают (по стандарту)?

Все реально используемые процессоры умеют в произвольное удлинение целых. Хоть с флагом переноса, хоть с трёхместными инструкциями, хоть как. Ну просто потому что без этого тот же финансовый софт не запустишь. Бэкэнд компилятора должен в такое уметь, даже если на его фронте таких типов нет.

> Все реально используемые процессоры умеют в произвольное удлинение целых.

1. Это сильно дороже.

2. Вы сказали «все реально используемые процессоры», но не сказали, что это обеспечено библиотекой. Это значит, что тот, кто заботится о проблеме, должен сам писать такой код? Ещё и продираясь через то, что некоторые фишки процессора (типа флагов CF, OF) недоступны в C и их функциональность надо эмулировать?

Ладно, со сложением или вычитанием — там достаточно просто написать что-то типа
if ((b > 0 && a > INT_MAX - b)
 || (b < 0 && a < INT_MIN -b))
{
 ... 
}


а с умножением как? Приведите тут (хотя бы для себя), как это будет выглядеть в коде. Можно в отдельную функцию.

> Бэкэнд компилятора должен в такое уметь, даже если на его фронте таких типов нет.

Вы сами сказали в своём предыдущем комментарии:

> Заметьте, я сказал написать (на Си)

Какой бэкэнд? Какой фронтэнд? Вы или крестик снимите, или трусы наденьте ©, или мы говорим про язык, или про свойства и возможности конкретных компиляторов.

Воу-воу, палехчи. Я, похоже, в отличие от вас, читал исходники gcc.

там достаточно просто написать что-то типа

Нет, недостаточно, в вашем подходе нужно рассмотреть все варианты, ну например a = b = INT_MIN. Вы забываете, что инструкция сравнения это "вычитание без запоминания результата". Если в процессоре нет флагов, как в RISC-V, то ваш код не взлетит (а если флаги есть, то он не нужен).

а с умножением как?

умножение двух интов сразу даёт long (и гарантированно в этот long влезает что для знаковых, что для беззнаковых). Тут даже делать особо ничего не требуется, просто не упаковывать long обратно в int.

> Воу-воу, палехчи. Я, похоже, в отличие от вас, читал исходники gcc.

Значит, вы утверждаете, что я не читал их.
Это попросту неправда, и продолжение дискуссии с вами будет только тогда, когда вы извинитесь за подобные предположения в публичной дискуссии.
Не вижу смысла продолжать по сути, пока вы не в состоянии это делать со своей стороны.

(Не вспоминаю пока, зачем тут вообще нужны исходники gcc к данной дискуссии. Но это мы обсудим после ваших извинений.)

вы утверждаете, что я не читал их.

Слово "похоже" вам ни на что не намекает?

Ну раз читали, освежите память. Исходники gcc вот: https://github.com/gcc-mirror/gcc/blob/16e2427f50c208dfe07d07f18009969502c25dc8/gcc/internal-fn.c

начиная с 686 строки.

Конкретно для сложения и вычитания int-ы и вообще signed типы достаточно расширять до соответствующего unsigned (что само по себе CWE-195, но компилятору можно :)

Мой пример на проверку до собственно сложения. Вы сказали:

> Нет, недостаточно, в вашем подходе нужно рассмотреть все варианты, ну например a = b = INT_MIN.

OK, смотрим. b == INT_MIN даёт ветку b < 0. INT_MIN — b == 0. a == INT_MIN, значит, < 0. Проверка сработала (вторая ветка), и мы ушли в обработку переполнения. К чему были ваши претензии? Вы можете показать, с какими именно аргументами для сложения такая проверка не сработает?

Ну а что с умножением сильно сложнее — я говорил открытым текстом.

И вот это вот:

> Если в процессоре нет флагов, как в RISC-V, то ваш код не взлетит (а если флаги есть, то он не нужен).

Почему это он не взлетит, если он полностью корректен?

> Вы забываете, что инструкция сравнения это «вычитание без запоминания результата».

Не забываю. А проверки такого рода корректны и выполняются и на машинах без флагов условий. Да, они могут быть дороже, чем явный вызов профильного интринсика, но результат они дают — и они используются именно в таком виде в соответствующих врапперах.
Можете сами взять эмулятор того же RISC-V и проверить все ключевые случаи.

> Исходники gcc вот:

Обработка __builtin_{add,sub}_overflow? Ну хорошо, и при чём тут это к моему описанию, как можно обойтись без такого интринсика для конкретной операции?

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

> Конкретно для сложения и вычитания int-ы и вообще signed типы достаточно расширять до соответствующего unsigned (что само по себе CWE-195, но компилятору можно :)

1. Это не расширение. Просто расширить signed до unsigned нельзя: -1 уже ни во что не преобразуется. Можно переинтерпретировать битовую последовательность, что они и делают, насколько я понял (код сильно путаный).
2. Там же на строках 724-726 написано дословно то же, что я предлагал для сложения. Вы это читали?
3. И повторю в который раз, что фишки конкретного компилятора тут не относятся к стандарту аж никак.

Но, попробуйте сделать ассемблерный выхлоп __builtin_mul_overflow() на самых распространённых архитектурах. Вы получите разные варианты игр с получением старшей части «косого» умножения средствами процессора, но не процессоро-независимыми средствами. И даже понятно, почему :)

UPD: скосил нетехническую часть, так лучше для долгой истории. Всё остальное осталось в личке.
Нет, недостаточно, в вашем подходе нужно рассмотреть все варианты, ну например a = b = INT_MIN.

Посмотрите на код ещё раз — этот вариант там обрабатывается.

Да, посмотрел ещё раз - этот код работает. Хотя и использует 2 ветвления вместо одного.

умножение двух интов сразу даёт long (и гарантированно в этот long влезает что для знаковых, что для беззнаковых).
А разве лонг не может быть иногда той же длины, что инт?

Речь про процессоры, а не про языки программирования. Если вам нужно сложить два числа разрядностью 32 бита на 8-ми битном Z80, например, то вы делает это так:

LD  A,(DE)
ADD A,(HL)
LD  (DE),A
INC DE
INC HL
LD  A,(DE)
ADC A,(HL)
LD  (DE),A
INC DE
INC HL
LD  A,(DE)
ADC A,(HL)
LD  (DE),A
INC DE
INC HL
LD  A,(DE)
ADC A,(HL)
LD  (DE),A

Как не трудно заметить, сложность тут чисто линейная, чуть дороже, чем сложение 4-х 8-ми битовых чисел. И никакие библиотеки тут не нужны. А вот на языках высокого уровня сделать это значительно сложней, так как ни один язык, что я знаю, не предоставляет информацию о переполнении.

Речь именно про различие процессоров и языка программирования Си.


Отлично, вы написали код сложения двух "длинных" чисел на ассемблере. А теперь попробуйте повторить тот же самый способ сложения на Си.

Согласен, это только на ассемблере можно так просто сделать. На Си будет чуть сложней.

int32_t num1[8];
int32_t num2[8];
...
int64_t t = 0;
for (int i = 0; i < 8; i++) {
   t += int64_t(num1[i]) + num2[i];
   num1[i] = int32_t(t);
   t >>= 32; //result: -1, 0 or 1
}
if (t != 0) {} //overflow

Да, это несколько дороже... Возможно, компилятор при оптимизации сможет что-то сделать, но не уверен.

А теперь сложите не 8 по 32 разряда, а 4 по 64.

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

ЗЫ: Я знаю, как будет выглядеть код, если пытаться складывать 64-х битные числа.

Вот-вот, "на процессоре" можно, как правило, складывать числа максимальной разрядности, а в языке Си — на 1 шаг меньше.


И после этого кто-то ещё пытается утверждать что язык Си к железу близок!

ни один язык, что я знаю, не предоставляет информацию о переполнении.

FYI, в питоне целые сразу произвольной длины "из коробки". Типа как строки. Поэтому для вычислений там все юзают numpy и прочие написанные на С либы:)

Я вот не знаю Питона :-)

Поэтому для вычислений там все юзают numpy и прочие написанные на С либы:)

Не поэтому :)

> Речь про процессоры, а не про языки программирования.

Процессоры ой разные бывают. Вот смотрим на GMP код для MIPS, со строки 64 — метод может быть повторен 1:1 с использованием беззнаковых целых на каждом шагу, типа такого:

void sum(unsigned *a, unsigned *b, unsigned *result, size_t length) {
  unsigned carry = 0;
  for (int i = 0; i < length; ++i) {
    unsigned tmp1 = b[i] + carry;
    unsigned carry1 = tmp1 < b[i];
    unsigned r = a[i] + tmp1;
    unsigned carry2 = r < a[i];
    result[i] = r;
    carry = carry1 | carry2;
  }
}


Проще никак — флагов нету, всё съели. RISC-V — точно так же.

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

Увы, придётся повторить тот же метод. Но это сработает.
Для знаковых — сравнить знаки аргументов со знаком результата, тоже легко.

Несколько неформальным тут может быть побитовая конверсия между знаковым и беззнаковым одинаковой ширины, но это сейчас реально всеми допускается, а с учётом обязательного дополнительного кода в C++20 (и ожидаемом таком же в C) — проблемы не составит.
А вот на языках высокого уровня сделать это значительно сложней, так как ни один язык, что я знаю, не предоставляет информацию о переполнении.

__builtin_add_overflow есть в gcc и clang. Да, этой фичи нет в стандарте, но она есть в реальном мире.

Я не настоящий сварщик, но в Intel Forntran есть возможность получать ошибку при целом переполнении:

severe(165): Program Exception - integer overflow

FOR$IOS_PGM_INTOVF. During an arithmetic operation, an integer value exceeded the largest representable value for that data type. See Data Representation Overview for ranges for INTEGER types.

А вот на языках высокого уровня сделать это значительно сложней, так как ни один язык, что я знаю, не предоставляет информацию о переполнении.

Rust предоставляет.

Тогда понятно, почему продвигают Rust для разработки ядра.

Цена 64-битнго сложения на 32-битной машине равна двум 32-битным сложениям: сложение и сложение с переносом. Ничего тут страшного нет. Другое дело, что неправильно так делать для проверки, с учетом того, что сам процессор, обычно, знает, когда произошло переполнение.

Согласен с тем, что в Си нет проверки на переполнение. Было бы очень здорово, если бы оно было в виде int add_int(int *a, int b, int с) { ... *a += b + c; ... } , где возвращаемое значение 0 - если все в порядке, -1 если переполнение в отрицательную сторону, 1 - в положительную. В этом случае очень удобно бы было делать операции с многоразрядными числами.

> Было бы очень здорово, если бы оно было в виде int add_int(int *a, int b, int с) {… *a += b + c;… }

Почти то же (без знака переполнения) есть в GCC, Clang и вслед за ними ICC. Осталось протолкнуть в стандарт :)
Знак можно вычислить по другим признакам (например, если вообще произошло переполнение, то в ваших терминах b!=0 и знак b соответствует направлению переполнения). Этого достаточно.

Вот бы ещё у раста с портабельностью (в т.ч. на "старое" железо) было получше...

А ещё со временем сборки. Как самого тулчейна, так и программ на нём...

А ещё с весом конечных бинарников...

INB4: "в 21 веке считать ресурсы".

Размер бинарников во многом зависит от runtime'а (который предоставляет много сервиса, типа разворачивания стека при паниках и т.д.). Рантайм в расте опциональный, и люди вполне пишут под embedded без него.

Вторая проблема - generic'и, которые сильно раздувают код (хотя и обещают работать быстрее). Возможное решение - использование динамической диспетчеризации (чуть медленее, зато компактнее).

В целом, я не видел, чтобы rust генерировал код существенно больше, чем Си (для этого надо смотреть на размер кода функций, а не итогового бинаря, потому что, повторю, опциональный рантайм толстоват).

А вот с поддержкой странных систем... Это да.

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

У меня знакомый пишет под старое железо с DOS и микроконтроллеры — все отлично с портабельностью у раста. Не си конечно, но вполне достойно


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

А zig?

На мой взгляд более "прямой" заменой Си является Zig (https://ziglang.org/), хотя он и менее известен, чем Rust.

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

Насколько я вижу по текущему описанию языка, выравнивание - часть типа указателя, указатели на отдельные переменные и на массивы просто так не смешиваются, арифметика указателей для простых указателей не допустима и т.п. Т.е. практически все потенциально небезопасные операции требуют как минимум явной переинтерпретации указателей через целые числа или другие "силовые" преобразования.

Но может я плохо понимаю, какие моменты Вы имеете ввиду ...

https://ziglang.org/documentation/master/#Pointers

https://ziglang.org/documentation/master/#Alignment

В GCC/Clang это уже частично решили через __builtin_${op}_overflow(). Пока только в избранных местах, без левого сдвига, но это уже хоть какая-то поддержка (можно написать хотя бы переносимый между процессорами враппер).

Теперь ждём ещё 30 лет для пробивания этого в стандарт.

Я не настолько глубоко в gcc, чтобы понять о чём речь. Грубо говоря, если я сделаю int(max_int)+1 - оно такое отловит? А int(max_int/2)*2? А int (min_int)*int(min_int)?

> Я не настолько глубоко в gcc, чтобы понять о чём речь. Грубо говоря, если я сделаю int(max_int)+1 — оно такое отловит?

Да, отловит. Если вы сделаете, например,

int a = INT_MAX;
int c;
int ovf = __builtin_add_overflow(a, 1, &c);


ovf будет установлено в 1 — случилось переполнение.

> А int(max_int/2)*2? А int (min_int)*int(min_int)?

Точно так же, через __builtin_mul_overflow().

И, так как все три параметра могут быть разного типа, можно ловить неудачную попытку сужения значения:

int32_t a = 999;
int8_t b;
int ovf = __builtin_add_overflow(a, 1, &b);


поставит 1 — потому что 999 не помещается в int8_t.

Эти же интринсики есть в Clang. В GCC есть ещё _p вариант, который позволяет проверить, например, что значение влезло в битовое поле (Clang это себе ещё не перенёс).

Спасибо.

Не совсем. Дело в том что с точки зрения стандарта есть вещи, которые вы видимо имеете в виду под выстрелом в ногу -- провоцирующие UB. Когда разработчики компиляторов дошли до того, что стали трактовать UB как запрещёнку, они начали ломать существующий код, который был написан не столько с опорой на стандарт, сколько на по факту наблюдаемое поведение -- и такого кода весьма много. В общем-то правы тут разработчики компиляторов, вот только жить от этого не легче -- и вся эта ситуация в целом показывает, чем плох Си -- это высокоуровневый язык, но мимикрирующий под низкий уровень, причём не самым умелым образом, ведь сложился он больше исторически (читайте: слеплен на коленке и допилен по ходу дела). При этом если писать с использованием реально низкоуровневых вещей, в отрыве от стандарта, разработчики компиляторов имеют полное право сломать такой код при очередном обновлении (естественно можно применить ассемблерные вставки -- но тогда и пишем мы не на Си, а на ассемблере). Надеюсь теперь стало понятней. =)

Я понял как решить проблему с UB для знакового! Надо внести в стандарт поведение при переполнении и UB исчезнет, переполение станет предсказуемым!

Именно так. =)

И какое именно поведение будем вносить?

То, что для беззнаковых производится _молчаливое_ усечение (wrapping, truncating) до младших бит (и это жёстко в стандарте) — это тоже не лучший вариант. Да, проверку на переполнение в беззнаковых операциях делать чуть легче, но всё равно про неё надо явно помнить.

Возможности компиляторов позволяют сейчас управлять желаемым поведением: где ставить проверки, где усечение, а где, если автор кода уверен, и полагание на отсутствие переполнения, как сейчас для знаковых. Нужна только общая воля.
Честно говоря, проблема в принципе не очень понятна.
производится _молчаливое_ усечение (wrapping, truncating) до младших бит (и это жёстко в стандарте) — это тоже не лучший вариант
Это как раз хорошо и логично и часто используется на малопроизводительных железках с высокими требованиями к реалтайму (экономим память и такты на обработку счетчиков в циклических процессах типа генераторов ШИМ, сигнала развертки и тд). При чем, дело тут больше даже не в стандарте, а в том, что оно почти всегда так работает на уровне процессора (если не обрабатывать флаг переноса, если он есть вообще на платформе). То есть скорее просто протечка абстракций.

Разумеется, при это иподразумевается, что разработчик знает платформу, под которую пишет.
> То есть скорее просто протечка абстракций.

Так в том и дело, что абстракции нижнего уровня становятся неадекватными и вредными на верхних уровнях.
В идеале, конечно, надо было бы иметь какой-то «безразмерный» int, который везде, где компилятор способен опознать реальный диапазон значений, сокращается до удобного машинного типа. Но если мы не можем себе это позволить, что делать в случае переполнения, то есть несоответсвии сгенерированного результата ожидаемому? Как и при любой ошибке программирования, jIMHO надо генерировать ошибку явно и вышибать выполнение (или просто ставить флаг, если явно попросили, или механизм немедленной реакции не работает).
(А если компилятор уверен, что переполнения не будет — он выкинет проверку, это законно и желательно.)

Ну а «на малопроизводительных железках с высокими требованиями к реалтайму» всё равно особая среда, которую можно явно пометить.
В целом, я согласен с каждым пунктом.
Но с другой стороны, мне кажется, что это решается некими coding policy. Либо можно вообще не писать «верхние» уровни на си.

Да, по сути верно. Единственная загвоздка тут в том, что знаковое переполнение, ровно как и многие другие случаи неопределенного поведения, большинством программистов считалось правомерным. В этой позиции есть смысл, если думать о Си как языке низкого уровня. В действительности это не так. Отсюда и огромное количество сломанного кода, и требование откатить изменения компилятора.

В Java знаковое переполнение это определённое поведение. В C - неопределённое. Значит ли это по-вашему, что в этом аспекте язык C является языком более высокого уровня, нежели Java? На мой взгляд всё с точностью до наоборот. Язык Java даёт гарантии программистам, пусть даже за счёт теоретической производительности на каких-нибудь мифических платформах, где это знаковое переполнение придётся имитировать. Язык C этих гарантий не даёт как раз для того, чтобы иметь возможность генерировать более производительный код на этих самых платформах с нетрадиционным представлением знаковых чисел.

> чтобы иметь возможность генерировать более производительный код на этих самых платформах с нетрадиционным представлением знаковых чисел.

Этой проблемы никогда не было и сейчас нет: оптимизации используются потому, что нашлась лазейка их разрешить, а не ради мифических странных платформ.
Отсутствие этих оптимизаций для операций с беззнаковыми это чётко показывает.

Немного даже иронично, что человечество, строя систему на базе такой строго предсказуемой вещи, как машина Тьюринга, в итоге дошло до такой магии в коде :)

А вот этот комментарий сделал мой день :-)

Я этого не говорил да и полагаю, что они оба просто высокоуровневые. Ни один из них не более или не менее высокоуровневый, чем другой, так как нормальных критериев я придумать не могу. Да и какой смысл? Вообще я имел в виду немного другое - большое количество программистов предполагают, что операции в Си будут имеют ровно такое же поведение, как и соответствующие им инструкции на целевой архитектуре. Т.е. если вы скомпилировали код под x86, то битовый сдвиг на размер переменной должен оставить ее нетронутой. В теории и на практике это так не работает. Поэтому Си - это не язык низкого уровня. То, как в стандарте языка определяется битовый сдвиг или знаковое переполнение не влияет на то, станет он от этого более высокого или низкого уровня (если только прямо не сказано, какую инструкцию должен использовать компилятор при трансляции). Да, неопределенное поведение позволяет совершать более агрессивные оптимизации, но всему есть предел. Си просто переполнен UB, приличная часть которого вообще имеет мало смысла. Конкретно проверять знаковое переполнение перед каждой потенциально опасной операцией гораздо неудобнее, чем это делать после.

А как переносимо проверять переполнение после? Например, если на какой-то платформе переполнение вызывает прерывание и аварийное завершение программы?
В каком-то языке (не помню каком) так и было определено поведение при переполнении, и на x86 компилятор инструкции into вставлял после арифметических операций.
> В каком-то языке (не помню каком) так и было определено поведение при переполнении, и на x86 компилятор инструкции into вставлял после арифметических операций.

TurboPascal при соответствующем режиме компиляции (директива компилятора {$Q+}). Можно было выключать в определённых местах.

Если бы в стандарте языка было указано, что вся целочисленная арифметика явным образом использует дополнительный код, то для тех архитектур, которые ее не поддерживают, пришлось бы имитировать такое поведение. Впрочем, тут нет ничего чрезвычайно страшного - компиляторам языка Си и без этого приходится программно реализовывать те операции, которые целевые архитектуры также не поддерживают. Так, например, для тех машин, которые не содержат инструкций для целочисленного деления и не только, gcc использует специальную библиотеку libgcc. Подробнее можно почитать здесь:

https://gcc.gnu.org/onlinedocs/gccint/Libgcc.html

> разработчики научили компилятор предотвращать выстрелы в ногу и убрали из него саму возможность выстрелить себе в ногу

Нет, всё строго наоборот, насколько я понял вашу метафору.

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

Разработчики компилятора говорят, что предохранитель утяжеляет конструкцию, и если пользователь не может предусмотреть все ситуации, то он сам виноват, а падать с лошади не надо. (Скрытым намёком идёт, что кто хочет безопасности — пусть берёт пистолеты от Ada.)

В результате пистолет стреляет даже когда лошадь просто пугается лая собаки из-за забора и делает рывок.

Вообще-то, возможность выстрелить себе в ногу лежит в основе философии Си и вообще Unix. "Unix was not designed to stop its users from doing stupid things, as that would also stop them from doing clever things." (Doug Gwyn)

НЛО прилетело и опубликовало эту надпись здесь
> Вообще-то, возможность выстрелить себе в ногу лежит в основе философии Си и вообще Unix.

Возможность намеренно выстрелить себе в ногу. А не неявно и случайно. Это принципиальная разница.

Вы можете написать rm -rf / и оно удалит все файлы (если от рута). Но надо было его вызвать с -r и -f. Просто «rm /» такого не даёт, и с какого-то раннего момента по одной из этих опций — тоже (причём Bell V6 давало это по rm -r, а System III и ранние BSD, насколько помню, уже нет). «rmdir» тоже, оно удаляет только пустой каталог.

И это принципиально. Обсуждаемые тут фишки это неожиданный выстрел в ногу от действий, которые такого не предполагали.

Я так понимаю, неожиданный выстрел в ногу получился не от того, что Си допускает неопределённое поведение, о котором все кому надо давно знают, а от того, что конкретный мейнтейнер конкретного компилятора начал усердно реализовывать стандарты. Правильно Линус сказал: когда текст стандарта противоречит реальности - он является обычным куском туалетной бумаги. Стандарты должны помогать, а не мешать. Кстати, это происходит в естественных языках: когда неправильное употребление становится всеобщим, оно становится стандартом :)

Мне вот интересно стало, если Линусу не нравится реализация GCC, то где можно увидеть ядро, собранное альтернативным компилятором? И легко ли это сделать в принципе?

Проект, на котором я сейчас работаю, успешно собирается на С++ компиляторах от 3-х вендоров. Этот проект бесконечно мал, по сравнению с ядром Линукса, но и ресурсы на его разработку также бесконечно малы.

Вопрос: если новый компилятор GCC так не устраивает Торвальдса, то почему он ест этот кактус?

Ну вообще-то прямо сейчас ведутся работы для возможности компиляции ядра линукса также и на clang.

Да, действительно, только что clang-13 без проблем собрал ядро 5.15.6

Видимо clang стал еще более совместим с gcc и/или из ядра таки выпилили gcc-only код.

Просто это хреновая философия. У вас же перила на лестнице например стоят и двери у лифта закрываются — зачем? Нормальный человек ведь в шахту не упадет. Надо было специально подойти к двери (-r) и шагнуть вперед (-f), любое одно действие из этих не дают такого результата.


Кому от этого легче только.

> Нормальный человек ведь в шахту не упадет. Надо было специально подойти к двери (-r) и шагнуть вперед (-f)

Нет, -r это означает перелезть через перила, если они есть, а -f — проигнорировать отсутствие лифта. Такая аналогия сильно точнее вашей (полного соответствия, естественно, не будет, но и не требуется).

> Просто это хреновая философия.

При таких интерпретациях — да.

> Кому от этого легче только.

Тому, кто таки обращает внимание на то, что делает.

Сейчас вон мода в UX отменять запросы подтверждений — мол, их никто не читает и всегда жмёт «да»… но мне они реально помогают, иногда остановиться «перед краем» как раз подумав, что тут может быть что-то не так.
Нет, -r это означает перелезть через перила, если они есть, а -f — проигнорировать отсутствие лифта. Такая аналогия сильно точнее вашей (полного соответствия, естественно, не будет, но и не требуется).

То есть -r это что-то плохое чем никто не пользуется? Или это нормальный флаг без которого папочку не удалить если в ней что-то есть?




В реальности же на щитке кроме "не влезай убьет" ещё всегда замок висит. И если человек влез и погиб то виноват будет не он, а тот кто замок сломал. У вас же почему-то "ты че дурак не прочитал что написано" — ну ок

> То есть -r это что-то плохое чем никто не пользуется?

Это флаг, который явно говорит «удаляй каталог несмотря на наличие содержимого», и если он указан — то это значит преодолеть соответствующую защиту.

> Или это нормальный флаг без которого папочку не удалить если в ней что-то есть?

Если вы уверены, что надо удалить с содержимым — да. Но надо вначале стать уверенным (или слишком уверенным).
Для удаления пустого каталога отдельно есть rmdir, и я его регулярно зову в случае, если хочу получить эффект «непустое не удалять!»

> И если человек влез и погиб то виноват будет не он, а тот кто замок сломал.

В этой аналогии, как вы представляете себе, чтобы один сломал замок, а другой влез?
Админ ходит по системам и по ночам пишет `alias rm='rm -rf'`, или как?
Я, наоборот, часто вижу `alias rm='rm -i'` как защитное средство, и поддерживаю (в интерактивном режиме).

> У вас же почему-то «ты че дурак не прочитал что написано» — ну ок

И опять же хромая аналогия. Пусть есть на двери обычная ручка, которая чтобы войти (это как вообще вызвать rm), а есть особая, которую ещё надо нажать несмотря на красный цвет и предупреждение. Кто захочет это делать просто так? Ну да, может, 0.1% захочет. Ну так и файлы себе постоянно удаляют даже с предупреждением.
Цель всех этих средств — не тотальная защита от всей глупости, это невозможно — полный дурак или самоубийца всегда найдёт метод — а защитить от ненамеренных ошибок. А для них опций типа -r, -f достаточно (при прочих равных).

Неправильно.

Нет, не правильно.

Суд по листингу, который привел felix-gcc, раньше assert(a + 100 > a) прерывало программу в случае переполнения (что в общем довольно логично — отрицательное число явно не больше положительного), а после очередного обновления — перестало… Типа на основании того, что такое поведение является «неопределенным», а потому — нарушением стандарта и не хрен такой код выводить в прод… ;)

В вашей аналогии люди знали, что можно случайно выстрелить в ногу, сооружали кастомные предохранители, но в очередной версии кольта компилятора эти предохранители перестали работать… НА основании того, что в инструкции по эксплуатации черном по белому написано: «ЗАПРЕЩЕНО стрелять в ногу!»

Простите, что пользуюсь Вашим комментарием, чтобы выразить своё мнение. В своё оправдание скажу, что темы схожи. Если я правильно понял статью, Си должен умереть потому что:

  1. стандарт языка - фуфло;

  2. разработчик (ОДИН!) средства разработки (ОДНОГО!) этого не понимает и отказывается использовать СУЩЕСТВУЮЩУЮ ОПЦИЮ по умолчанию;

  3. в качестве замены, как системного языка, появился юный Rust.

Я всё верно понял? Т.е., вопрос не к выразительным средствам языка, не к тому, что символ "*" в Си без контекста не понять (что есть правда)?

Я не защищаю Си и не обвиняю Rust. Я лишь хочу указать на сомнительность аргументации и использование цитат "больших программистов" в своих целях. Как пример последнего, отсылаю к цитате из переписки Торвальдса, который говорит (ИМХО) верно: если документ мешает писать, то к чёрту документ! Из цитаты Торвальдса "Это то, почему мы используем "-fwrapv", "-fno-strict-aliasing" и другие флаги." я делаю вывод, что у него претензии-таки не к Си. Хао! А теперь минусуем. :)

В примере с check_password есть ошибка: поскольку буфер pwd — внешний, выкидывать memset для него компилятор не может (по крайней мере, до тех пор пока не заинлайнит тело функции на уровень выше). Да и изменение памяти по указателю на константу является ошибкой само по себе, независимо от оптимизаций.


Вот какой вызов на самом деле не помешал бы и какой компилятор имеет полное право выкинуть — это очистка буфера real_pwd.

Да, вы совершенно верно отметили, это опечатка. Имелся в виду real_pwd. Спасибо большое, исправлено!


int main()
{
    int x;
    int y;
    int *p = &x + 1;
    int *q = &y;
    printf("%p %p %d\n", (void *) p, (void *) q, p == q);
    return 0;
}

Небольшая поправка, должно быть

int *p = &x - 1;

Бывает по-разному, можно написать -1, можно +1, у меня при использовании clang работает один вариант, а при gcc -- другой.

На самом деле можно использовать оба варианта, и вне зависимости от реального расположения переменных на стеке обе проверки на равенство вернут ложь.

    int *p1 = &x + 1;
    int *p2 = &x - 1;
    int *q = &y;
    printf("%p %p %p\n", (void *) p1, (void *) p2, (void *) q);
    printf("%d %d\n", p1 == q, p2 == q); /* 0 0 /*
обе проверки на равенство вернут ложь.
YMMV:
Два компилятора (слишком старых?) -- три мнения ^_^
❯ gcc  p_2.c && ./a.out
0x7ffc001efefc 0x7ffc001efef4 0x7ffc001efefc
1 0

❯ gcc -O2  p_2.c && ./a.out
0x7ffc7ef73664 0x7ffc7ef7365c 0x7ffc7ef73664
0 0

❯ clang  p_2.c && ./a.out
0x7ffccdefd07c 0x7ffccdefd074 0x7ffccdefd074
0 1

❯ clang -O2  p_2.c && ./a.out
0x7ffc1afeabdc 0x7ffc1afeabd4 0x7ffc1afeabdc
1 0

❯ gcc --version
gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0

❯ clang --version
clang version 6.0.0-1ubuntu2 (tags/RELEASE_600/final)

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

Хорошая сводка тезисов на эту тему, положил в закладки. Ещё бы хорошо добавить ссылки на другие примеры неожиданных оптимизаций и их последствий, типа такого или такого (вообще их штук 20, наверно), блог regehrʼа, аналогичные ресурсы.

Но, с другой стороны, мне кажется, что именно смерти C (и C++) призывать тут не стоит. А вот что стоит (и об этом говорил много раз) — предлагать формализацию допустимых оптимизаций и обхода таких граблей.
Сейчас это полностью implementation-defined (отдано на откуп компиляторам). Но уже есть механизм атрибутов и прагм. Чтобы устранить возможные проблемы, надо ввести в стандарт регулирование этих возможностей, например:

#pragma STDC optimize strict_aliasing off

и до конца блока (исходного файла, если на верхнем уровне) стоит запрет. Или:
#pragma STDC optimize("Debug") strict_aliasing off


отключено при сборке в Debug-режиме (требуется передача режима команде компиляции).

Аналогично и для переполнения, проверки на null, и прочих диверсий.

Ну и вариант типа

c = [[int_arith(wrapping)]] (a + b);


для атрибутной пометки.

Для ~95% кода достаточным будет режим с минимумом оптимизаций. Только определённые участки (hot/critical path) заслуживают повышенного уровня.

Дополнительно замечания по переполнению: 1) начиная с GCC5 есть проверки типа __builtin_${op}_overflow[_p], частично они есть и у Clang, все проблемные места можно уже пометить и явно проверить. 2) так как в C нет исключений, лучше всего делать проверку переполнений при синтаксисе типа a+b через thread-local переменную, аналогично errno.

Главное — таки продавить это в стандарт (не верю ещё лет ближайших 20).

Спасибо!

Отчасти это и правда решение -- насколько знаю Линукс это активно использует, обвешивая весь свой код сложной системой сборки, где указывается какие правила где применить.

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

Более того, думаю критически неудобно, что по сути в таком случае мы мыслим на многих языках сразу -- не считая необходимости постоянно осекаться и спрашивать "а включил ли я все необходимые свойства сборки?". Кажется что ассемблер в связке с ещё одним языком тут будет банально проще.

Ну и конечно "должен умереть" это преувеличение -- никому он ничего не должен, на нём вполне можно писать рабочий код, а если бы мог стать лучше -- это было бы прекрасно. Но кажется что этот язык по сути свернул куда-то не туда и шансов на улучшение у него мало. Но всё может быть, успехов вам! =)

> что всё же набор всех возможных правил оптимизаций уже невероятно увесистый, и он только продолжает расти

Не нужно такое делать на все оптимизации, мне кажется. Нужно на основные (все — тоже вряд ли получится) случаи UdB в стандарте, которые могут возникать именно из-за ограничений человеческого разума. Два первых кандидата — как раз переполнения и алиасинг. Разыменование пустого указателя — возможно, следующее, но его статический анализатор способен (обычно) опознать анализом кода (хотя был пример неожиданного для человека проявления, сейчас не найду ссылки). В общем случае, да, пересмотреть набор всех случаев (в C++ их несколько сотен, в C должно быть поменьше).

> не считая необходимости постоянно осекаться и спрашивать «а включил ли я все необходимые свойства сборки?»

Полиси кода со стандартной шапкой в каждом исходном файле решает это. Потом — и умолчания компилятора (хотя на это вряд ли пойдут).

> Но кажется что этот язык по сути свернул куда-то не туда и шансов на улучшение у него мало. Но всё может быть, успехов вам! =)

Вряд ли я лично буду этим заниматься, но всё равно спасибо :)

на нём вполне можно писать рабочий код

Только очень сложно. Локально работающий - да. Код, которому можно доверять - неа.

Спасибо большое за ссылки и комментарий! К сожалению, в текст одной статьи невозможно уместить все сразу, тем более, что бездна UB неисчерпаема. Идея была в том, чтобы провести неискушенного в тонкостях стандарта читателя от достаточно тривиальных ошибок до гораздо более изощренных и опасных случаев неопределенного поведения. Безусловно, говоря о том, что Си должен умереть, я сам не надеюсь на то, что это произойдет. Смысл в том, что вокруг этого языка существует огромное количество серьезных заблуждений, и вызваны они отнюдь не только непрофессионализмом тех, кто на нем пишет. И учитывая историю Си, и то, как написан стандарт, неудивительно, что ошибки неопределенного поведения стали обыденностью. Я сам долгое время считал, что Си - это своего рода кроссплатформенный ассемблер. Потому я полагаю, что концепция языка, который создает столь опасную иллюзию , является по меньшей мере неудачной. И в том случае, если Си остается с нами надолго (а у меня нет оснований считать иначе), очень важно, чтобы программисты четко осознавали то, какой именно инструмент они используют.

Но это очевидным образом не уменьшает сложность языка, так как новый С с прагмами содержит старый С как строгое подмножество. Проще ли на нем писать? ХЗ, даже с прагмами в стандарте нужно учитывать возможные UB.

Короче, я пессимистичнее смотрю.

Я вот считаю, что тут нужно призывать именно к смерти Си. Для низкоуровневого языка он делает слишком много предположений о том, как нам жить. А для современного высокоуровневого языка он не дает достаточной изоляции от платформы.

Очень интересная и конструктивная статья. Сложно не согласиться с Вами, особенно принимая во внимание, что

Зачастую на отладку и тестирование программ уходит больше времени, чем на проектирование и написание самого кода.

И данная проблема, действительно, имеет место быть. Что зачастую отталкивает начинающих программистов в изучении Си.

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

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

Спасибо, мы старались! =)

Автор привел в тексте огромное количество примеров о которых знает каждый новичек в Си. Спасибо, перебрали немного граблей новичков.

Си это не язык в вакууме -- это текст который транслируется в бинарный вид (LLVM или в машинный) так, как описано в стандарте. Остальное является неопределенным поведением, зависящим от твоей платформы, фазы луны, версии компилятора или стандарта.

Гарантии не даются не по причине "мы хотим натворить ЗЛО", а потому, что никто не может их дать. Ты пишешь стандарт сегодня, а завтра intel выпустит очередной i7 в котором переполнение int будет работать по другому, а ты завязался на логику, которая является платформа-зависимой. Это абсолютно адекватно, а автор приводит срачи 20 летней давности.

На тему поломки старого кода -- у Си есть 2 пути: умереть или стать лучше. Си выбрал вариант похоронить старый код, и сделать обычные случаи применения на 10% быстрей. Переезд на новый стандарт для старого кода означает ускорение его работы, и перепись особо кривых мест. Ребята которые используют грязные хаки за границами стандарта прекрасно понимают "что они делают" и почему они это делают. Спойлер: ради скорости.

У староверов, есть выход: отвязаться от платформы либо привязаться к конкретной версии стандарта и компилятора.

 Благодаря нему у нас есть ядро Linux и тысячи уязвимостей в нём же в придачу.

Благодаря Си Linux самая популярная серверная ОС. А еще мы не пишем на ASM для МК в основном благодаря стандарту.

Тысяча уязвимостей не потому что Си, а потому, что ядро Linux это не школьный проект на 100 строк, а огромная штука, написанная человеком. Человеки склонны ошибаться.

В погоне за эффективностью разработчики компиляторов забыли о том, для чего на самом деле нужен язык Си. Он инструмент программиста, а плохим инструментом нельзя написать хорошую программу.

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

Особенно меня радует истории про "-fstrict-aliasing", который при включении хоронит твою производительность, т.к. нельзя теперь применять жесткие оптимизации зато работает "не стандартный код". Если ты лично решил, что умней компилятора и оптимизатора, ты можешь собрать свою 1 функцию в отдельном файле с "-fstrict-aliasing", проблема высосана из пальца.

Впрочем, и этот шедевр черной магии не проходит проверку стандартом - похоже, что создатель культовых игр Джон Кармак совсем не разбирается в языке Си

Кармак приводит дробное число в бинарный вид, домешивает константу, возвращает в дробный вид. Это изящный хак, очень завязанный на уверенность, что float в его системе будет выглядеть именно так, а не иначе. Этот код абсолютный unsafe, на уровне ассемблерной вставки в код. Он не может быть пропущен стандартом на уровне идеи. Программисту, стоит указать, что этот код оптимизировать не нужно, и бац проблемы не существует.

> Автор привел в тексте огромное количество примеров о которых знает каждый новичек в Си.

Курсы для новичков про это обычно не рассказывают. Затем и нужны статьи, чтобы потом объяснять, что реально здесь водится целая стая голодных драконов.

> Гарантии не даются не по причине «мы хотим натворить ЗЛО», а потому, что никто не может их дать. Ты пишешь стандарт сегодня, а завтра intel выпустит очередной i7 в котором переполнение int будет работать по другому, а ты завязался на логику, которая является платформа-зависимой.

Нет и ещё раз нет.
Во-первых, вы почему-то принципиально думаете только о варианте, когда переполнение замалчивается. Да, -fwrapv делает это. Но нормальная реализация должна давать оповещение, а не замалчивание. Для C++ этим было бы throw. Для C такого нет, но в стандартной библиотеке есть errno, которое уже показывает пример, что thread-local переменная может быть использована для оповещения об ошибке. Или не thread-local, а указанная контекстным атрибутом.

Во-вторых, если вы не в курсе, C++20 уже постановил, что кроме дополнительного кода варианта нет. C в следующем стандарте, вероятно, последует за ним. Вероятность отката от этого уже нулевая. Так что предположения про «а завтра intel выпустит очередной i7 в котором переполнение int будет работать по другому» это не более чем голые фантазии.

А так — C изначально завязан на кучу вещей: например,
1) само по себе двоичное (не троичное, не какое-то другое) представление
2) битовое представление числа в обычном счислении (а не в каком-нибудь коде Грея)
3) ячейки-«байты» не менее чем по 8 бит
4) память с последовательной нумерацией ячеек (не может быть, что в массиве от 1000 до 2000 — 1001 пропущено, а 1002 есть)

и 100500 других признаков типовых современных платформ. И что, кто-то их собрался отменять?

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

Проблема в том, что оптимизации давно уже действуют за пределами обычного представления автора кода. Например, переполнение может возникнуть в результате умножения на константу, определённую в заголовочном файле другой библиотеки. И никто не скажет, что оно случилось, если не сделать явные проверки по каждому чиху, который даже и не предполагаешь.

А именно такая механическая помощь человеку — задача компилятора. Если надо проверять переполнение, это должен делать компилятор, а не человек. Человек должен думать над задачами для человека. Сейчас же компилятор только мешает, и каждая такая новая оптимизация мешает чем дальше, тем резче и неожиданнее.

> Если ты лично решил, что умней компилятора и оптимизатора, ты можешь собрать свою 1 функцию в отдельном файле с "-fstrict-aliasing", проблема высосана из пальца.

Нет, не высосана, потому что 1) требуется отдельный файл (include-only уже не годится, инлайнинг не сработает), 2) опции компиляторо-зависимы и присутствуют не везде.

Но нормальная реализация должна давать оповещение, а не замалчивание. 

Идеальная нормальная реализация требует доказательства отсутствия переполнения во время компиляции, а не рантайм-проверок.

> Идеальная нормальная реализация требует доказательства отсутствия переполнения во время компиляции,

Это более продвинутый вариант, безусловно. Но доводить C до такого состояния вряд ли получится и вряд ли имеет столь высокий смысл.
Так то да, но тут еще есть проблема уже написанной кодовой базы, которая по определению не идеальна… ( Получается где-то такая проверка использовалась и выдавала ошибку, а после какого-то момента перестала…

Наверное плохо, что код был написан так. Наверное автор кода не прав в том смысле, что использовал в коде неопределенное поведение относительно которого было отдельное предупреждение. Но плохо, что поведение компилятора изменилось в моменте, а кодовая база осталась прежней… Это реально чревато проблемами в самых неожиданных местах. Причем «тихими» проблемами. Было бы лучше, если бы переполнение стало везде приводить к ошибке…

Да и… Если честно я не понял логики разработчика… Почему при сравнении отрицательного числа с положительным оно внезапно не меньше?! Компилятор не производит вычислений? Типа видит что слева Х+константа, а справа просто Х и без вычисления понимает что справа слева больше, так что ли? "Хороший тамада и конкурсы интересные"(с)

1) само по себе двоичное (не троичное, не какое-то другое) представление
2) битовое представление числа в обычном счислении (а не в каком-нибудь коде Грея)
3) ячейки-«байты» не менее чем по 8 бит

Это точно проблема языка, а не существующего железа? Для последнего пункта есть bit fields.

4) память с последовательной нумерацией ячеек (не может быть, что в массиве от 1000 до 2000 — 1001 пропущено, а 1002 есть)

Ты уверен, что в Си есть массивы? Точно знаком с языком?

и 100500 других признаков типовых современных платформ

Особенно код грея (который не используется для хранения и работы с данными в памяти) и троичное счисление.

> Это точно проблема языка, а не существующего железа? Для последнего пункта есть bit fields.

Вы вообще смотрите, на что отвечаете? Я говорю, что это требование языка к среде реализации кода: память состоит из сущностей, именуемых «байт» и имеющих не менее 8 бит. При чём тут битовые поля?
Если машина, например, с 6-битными байтами (как было типовым решением в 1950-60-х), то «байт» C будет состоять из двух байтов этой машины.

> Ты уверен, что в Си есть массивы? Точно знаком с языком?

Какие возражения против сказанного? Вы не в курсе, что массивы располагаются в памяти в последовательных адресах?

> Особенно код грея (который не используется для хранения и работы с данными в памяти) и троичное счисление.

В чём возражение? Или вы не попытались понять тезис?

Если машина, например, с 6-битными байтами (как было типовым решением в 1950-60-х)

А напомни, когда разработан Си и сколько таких машин было с того времени? Не было надобности - нет реализации.

Какие возражения против сказанного?

Утверждение, что в Си есть массивы.

Вы не в курсе, что массивы располагаются в памяти в последовательных адресах?

Элементы массивов? В каком языке?

В чём возражение?

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

> А напомни, когда разработан Си и сколько таких машин было с того времени? Не было надобности — нет реализации.

Поищите «comp.lang.c FAQ» с примерами странных архитектур.
Машины с 18- и 36-битными словами дожили до начала 90-х. C на них был.
Скажете, это всё легаси? Собственные разработки Cray имели 32-битные слова без возможности байтовой адресации, на них и CHAR_BIT сишный был равен 32. А это уже конец 70-х, начало 80-х.
Ранние Alpha могли адресовать память только словами, байты там эмулировались (до появления BWX extensions), это 1995.
Ну ладно про всякие PDP-1 и Cray вы могли не знать, но как мимо вас прошла Alpha?

> Утверждение, что в Си есть массивы.

Я честно не хочу влазить в режим language lawyerʼа плохого пошиба, поэтому скажу так: если я вижу определение типа `int a[1000];`, то я вместе со 100500 других источников, включая учебники C, называю это массивом, как бы это кому-то ни не нравилось.

> В требовании абсурдной ерунды, которой ни в одном языке нет за ненадобностью.

Вы перепутали знак: я как раз объяснял, что такого требования нет, а есть, наоборот, требование отсутствия подобных особенностей. Разберитесь, кто это требовал (может, и вы), с него и спрашивайте.

Машины с 18- и 36-битными словами дожили до начала 90-х. C на них был.

Не, ты мне реализацию шестибитных байтов обоснуй. В большую размерность всё элементарно засовывается. Ещё интересно как это сочетается с твоей уверенностью в нереальности систем с другой размерностью int.

Собственные разработки Cray имели 32-битные слова без возможности байтовой адресации, на них и CHAR_BIT сишный был равен 32.

И сейчас такие системы есть, но в твоей реальности они не существуют.

Я честно не хочу влазить в режим language lawyerʼа плохого пошиба, поэтому скажу так: если я вижу определение типа int a[1000];, то я вместе со 100500 других источников, включая учебники C, называю это массивом, как бы это кому-то ни не нравилось.

Называй как хочешь, устройство языка это не меняет.

И к чему эта простыня?

Да, оно у меня в закладках. Проблема в том, что C одновременно и такой высокоуровневый язык, как там описывают, и низкоуровневый, и вот это сочетание даёт как минимум заметную часть текущих проблем. С C++ ещё хуже — сохраняя всё это, он добавляет ещё несколько слоёв вплоть до отдельного compile-time языка темплетов. Понимать и поддерживать всю эту кашу можно только думая на нескольких уровнях.
Исходная же статья частично поднимает вопрос — а как всё это получилось? Абьюзинг всех изначальных UdB только часть этой проблемы (хотя и самая очевидная).
> Не, ты мне реализацию шестибитных байтов обоснуй.

Ну я тут не знаю… попробуйте историю вычислительной техники почитать, что ли? Там много интересного есть. Например, что в 50-60-х 6-битные байты были большей нормой, чем 8-битные. Что при проектировании знаменитой System 360 большинство голосов начальства были за 6-битность, и то, что Gene Amdahl отстоял 8-битность, было великим подвигом с его стороны (всупереч голосам про бессмысленную трату ресурсов), что на него ещё несколько лет шипели, пока окончательно не утвердилось преимущество 8-битки. Что PDP-1 была в 1958, а знаменитая PDP-11 аж в 1970, а в промежутке между ними было множество моделей со всякими словами по 18 бит. Что линия PDP в заметной мере разрабатывалась под нужды армии (PDP, в отличие от S/360, были мобильными! в переводе с армейского — могли переезжать на грузовиках), а там своя специфика. Ну и прочее…

> И сейчас такие системы есть, но в твоей реальности они не существуют.

В моей реальности _под которую пишу_ я и под которую пишет 99% тут присутствующих — да, не существуют.
За пределами её есть 8-битные процессоры, есть секвенсоры, есть много чего, но это другой слой.

> Называй как хочешь, устройство языка это не меняет.

Верно. И в этом устройстве массивы есть, но вы об этом не хотите знать, судя по данному ответу.

18-битная и другая странная арифметика до сих пор встречается в DSP, AFAIK. Да, программируют их обычно не на С, но как явление их упомянуть надо.

> 18-битная и другая странная арифметика до сих пор встречается в DSP, AFAIK. Да, программируют их обычно не на С, но как явление их упомянуть надо.

Ну некоторые DSP вроде и на C программируют (с интринсиками везде где можно). Но для таких, где 18 бит, да, это уже кажется вряд ли — не по факту, а по тому, что современным программистам непривычно.

А так — я могу и на amd64 применить int16_t, int18_t и всё такое, если их предоставит компилятор. Правда, все операции с ним будут получать автоматический integer promotion до int32_t (равному int), но при укладке в переменные урежется обратно (и это ещё одна проблема на подумать).

LLVM в этом смысле сильно более современен — и появляются языки, которые начинают это напрямую использовать (Rust, Swift...): integer promotion нет, операции выполняются в естественном размере типов. Более того, int может быть шириной любой от 1 до 2**23 бит (не представляю себе, зачем такая безумная ширина, но сложение-вычитание-умножение делается тривиально). Используя это, уже можно выполнить адекватную укладку на размеры необходимого. Если мы знаем, что получили определение типа

type Temperature = new Integer(-90...99); // даже в Оймяконе ниже не бывало


то дальше компилятор получит полную возможность укладывать его в i7 и делать операции в соответствующем пространстве (и избавляться от явных проверок, где не надо). Помощь человека в виде «а int8_t достаточно или нет?» тут уже не нужна.

Ещё это полезно в плане укладки данных в битовые поля (там, где сейчас ширина поля отделена от типа, на самом деле лучше подходит явное указание в виде int<3>, uint<11> — это автоматически позволит и проверять границы при укладке в значение такого типа). Сейчас в GCC чтобы проверить влезание данных в битовое поле пишется что-то вроде `__builtin_add_overflow_p(val, 0, dest_field)` (это проверка без собственно размещения), это даже Clang ещё не скопировал.

LLVM даже проверки влезания в такой размер делает без проблем — при переносе на реальную платформу типа x86 используя хранилище достаточной ширины (32, 64 бита) и добавляя проверки на границы.

Но чем более высокоуровневое программирование нужно, тем ценнее возможность проверки границ и указания множества значений (диапазонами или как-то иначе), а не просто какой-то [u]int${N}_t.

Вопрос производительной и/или тонковывернутой арифметики сильно шире вопросов ширины арифметики как таковой, имхо. Там очень много тонкостей семантики, какие-то из которых важны для корректности результата, а какие-то для производительности. И на уровне языка системного программирования и тем более llvm asm эти тонкости теряются.

> Вопрос производительной и/или тонковывернутой арифметики сильно шире вопросов ширины арифметики как таковой, имхо.

«Тонковывернутой»… это о чём? Какие-то особые операции типа ДПФ в целых числах? Ну если их на C не описать, то по крайней мере интринсики уже способны их задать, а дальше дело компилятора, что он и как вызовет.

> И на уровне языка системного программирования и тем более llvm asm эти тонкости теряются.

На уровне LLVM IR таки, после укладки на дополнительный код и с дополнительными всякими nuw/nsw, оно достаточно неплохо отражается — можно восстановить и что это было вначале. Диапазоны, да, не сохраняются, но это там уже, наверно, и не нужно. Хотя тут могу и пропустить что-то важное.

Не только и даже не столько, хоть и близко. Операции типа DFT пишут специально обученные люди, обложившись мануалами, и пишут как правило под конкретное железо и один раз. Поэтому там вполне можно писать непосредственно в асме, если уж надо (см библиотеки типа openblas, где внутренние циклы натурально написаны на асме целевой платформы).

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

Если нужна производительность, то в качестве примера можно рассмотреть агрегацию значений сложной в вычислении функции на целочисленной сетке, причем распределение значений на этой сетке имеют нетривиальную симметрию и про некоторые диапазоны заранее известно, что их обходить не нужно. (см wikipedia://polytope+modell)

В любом случае, речь идет о том, что вопрос оптимизации и/или корректности вычислений часто оказывается завязан на неочевидную и нетривиальную информацию о входных данных, которые в модели вычислений на уровне С/llvm asm не отображается. Некоторые современные компиляторы достаточно умны, чтобы её из кода реконструировать (напр. gcc/graphite) но то такое.

Конечно, массивы в Си есть, заблуждение утверждать обратное, основываясь на одном лишь сходстве работы с указателями и массивами: стоит положить массив в структуру, и оказывается, что его можно передавать и возвращать из функции как одно целое, как кусок памяти определённого размера, чем и является массив.

Конечно, массивы в Си есть

Наверняка для них размерность и тип задаются, да? Или исключительно указатель на начало даже без типа?

стоит положить массив в структуру

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

В Си нет массивов, они введены в синтаксис добавлением смещения к указателю.

> Наверняка для них размерность и тип задаются, да? Или исключительно указатель на начало даже без типа?

Ага, вы таки перепутали хрен с пальцем.

Когда написано: `int a[1000];` — это массив.

Более того, sizeof(a) выдаст в этом случае 4000, а не 4 (считая, что int — 4 байта).

А то, что в ряде случаев указание на массив превращается в указатель на первый элемент (индекс 0), не устраняет факт наличия такого типа.

Когда написано: int a[1000]; — это массив.

А когда после используешь a или *a - это указатель. Сам ты хрен с пальцем.

Более того, sizeof(a) выдаст в этом случае 4000, а не 4 (считая, что int — 4 байта).

У тебя вызов sizeof() заменится константой на этапе компиляции.

Очень удобно критиковать язык, не зная его.

Особенно код грея (который не используется для хранения и работы с данными в памяти) и троичное счисление.

Таки машины на троичных кодах были.

Были, не спорю. Но для них не нужен был язык Си.

Дополню ответ @netch80с которым согласен. Вы описываете Си как язык высокого уровня, и конечно тогда он имеет право давать любые гарантии и любую модель исполнения. Вот только как язык высокого уровня он плох и даёт сложные для понимания и контроля гарантии (если для вас это не так -- поздравляю, вы в сотни раз умнее меня =) ) -- и тогда вопрос -- а зачем он нужен? Ведь когда мы пишем программы мы хотим что бы они работали -- а в силу сложности Си гарантировать это ... сложно. И при этом не важно насколько быстро они работают -- если они работают с ошибками, неправильно -- то они просто не работают.