Комментарии 643
Переполнение знакового - это такая бездна, в которую даже приближаться не хочется.
Если бы проблема была только в переполнении знакового... Но да, уже это весьма неприятно. =)
Си должен умереть, да, и пока что 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 в новые функции выделения памяти не очень понятно.
Не совсем так.
Именно так.
Я сильно сомневаюсь, что простому системному язычку нужно 5 (пять) видов макросов. И никакой язык не может избавить от всех ошибок. Это просто невозможно.
Учтите, что для языка - замены С нужна возможность писать компиляторы по любому чиху для чего угодно. Это, в сущности, должен быть уровень магистрской.
А если язык сложен как Rust, написать второй компилятор очень тяжело.
В целом, требование второго компилятора вполне обосновано, и я его много раз слышал в контексте "коммититься в язык".
В целом, вот, люди пытаются. https://github.com/thepowersgang/mrustc
Нет, "я требую" не второго компилятора, а двадцать второго.
То есть, для замены текущего С, как lingua franca современных ЯВУ, должна быть спецификация, компилятор по которой усердный студент 6 курса реализует ну за пол года. Без оптимизаций, разумеется, без хитрых проверок, но вполне рабочую. Стандартной библиотеки тоже несколько реализаций.
Ну и, раз можно помечтать, набор стандартных тестов к компилятору.
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 спортировать?
А ведь Эльбрус - это мощнейшая контора с гос. финансированием, долгими традициями и т.д. А что могут авторы какого-то исследовательского процессора?
че-то ору....
А чем вообще ценность такого упражнения, кроме занятости студента?
Возможность разработки какой-то новой экосистемы, с новыми процессорами, новыми шинами и т.д.
Иначе мы завязнем в болоте x86. О, блин.
Справедливости ради, переписывание кода на расте, всё-же, может потребоваться.
Вообще-то банально — для этого нужно банкирование памяти :) (которое и так очень часто есть на таких контроллерах, но не в таких масштабах).
Но в здравом уме и твёрдой памяти, конечно, никто так делать не будет (превышение в 64 раза, как на старших PDP-11, вроде был максимум достигнутого), поэтому это замечание чисто вскользь.
Не, я про использование, к примеру, 64-битных примитивов на 32-битных платформах и тому подобное.
А почему, собственно, без gcc ? А если я потребую у Вас Rust без llvm ?
А почему надо писать для новой экосистемы весь компилятор а не только бекэнд?
Весь раст прячет за абстракциями llvm, так что порт кодогенерации из llvm IR в спарковский байткод вполне решит проблему. Расту останется только указать необходимый бэкэнд. Есть неофициальная поддержка и 16-битных досов и под амигу помнится что-то делали. Это уже не говоря про зоопарк embed микроконтроллеров, часть из которых потом в спутниках вокруг земли вертится.
И вы начинаете зависеть от llvm. Нет, это путь в никуда для небольшой группы.
Кроме того, системный язык на то и системный, чтобы быть близок к текущему железу => у него должен быть достаточно простой компилятор.
Насколько должна быть небольшой группа, чтобы ей понадобился отдельный компилятор под собственное не мейнстримное железо и при этом страдало бы от зависимости от llvm? Звучит как мифический персонаж из африки у которого из девайсов только телефон с парой мегабайт памяти и ллвм туда просто не влезет. Опять же встаёт вопрос каков шанс, что такая мифическая железка нужна в массовых количествах и будет иметь хоть какие-то требования к безопасности и отказоустойчивости? Мне кажется более вероятным что кто-то будет писать сразу на ассемблере для такого, чем пытаться изобразить ещё один компилятор Си.
Просто исследовательская группа в НИИ/каком-нибудь университете. Мультиклет тот же самый. Или вот - https://www.cs.utexas.edu/~trips/
Утверждение про писанину на собственном ассемблере все ещё валидно. Мультиклет тоже поставляет llvm-based компиляторы.
Нужды trisp, вероятно, оправданы, только кому сейчас нужен тот trisp? Выглядит так что они так и не перешли путь в никуда, даже с собственным компилятором. Который был нужен по сути только для выжимания скорости в академических условиях, какие в нем баги и уязвимости одним только исследователям известно.
И как много кода сейчас пишется или, прости господи, компилируется в режиме С89?
Много чего для видео. FFMpeg, например. До сих пор ругается на смешанные объявления переменных и код.
Вопрос интересный. Я, с одной стороны, понимаю мотивацию требовать простой для реализации язык. Но, с другой стороны, вы же этого не требуете от ОС? Времена, когда вы могли нашкрябать аналог MS-DOS за пол-года 6 курса давно прошли, и чем дальше, тем сложнее, однако, никто не выкидывает новые компьютеры на том основании, что студент под новую умную сетевуху драйвера уже не осилит написать. Аналогично с компиляторами. Альтернативные (условно простые) реализации компилятора - да, требование охватности компилятора неподготовленным мозгом - нет, это произвольное требование.
Альтернативную библиотеку к расту уже написали/пишут (насколько оно применимо не смотрел): https://github.com/eloraiby/alt-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 remove
s 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, и где он теперь? А если бы он был единственным компилятором, то и вся кодовая база превратилась бы в стремительно устаревающую тыкву.
Требование более чем разумное.
Компании, которые выдвигают требованием наличие хотя бы двух независимых реализаций перед адоптацией языка, на Хаскель даже не смотрят.
Смотрели и даже используют. Компиляторов С++ достаточно много.
Я не уверен насчёт OpenWatcom, но ICC точно был.
но я его вообще ни разу нигде не видел в бою
Я видел. В 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 жизни нет?
??? 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, но все же)))
Я тогда работал на HPC тематику (хоть и вскользь) и мы его там пробовали. На каких-то математических пакетах он давал код быстрее и GCC, и (тогда ещё только начинавшегося) Clang, и ICC.
Некоторые - конечно. Но кто-то будет на нём (или другом языке) писать код с сроком сопровождения 50+ лет (условная электростанция или метро). И их соображения вполне разумны.
Это просто часть кластера болезненных точек, не причина, разумеется, но единственность ghc сильно связана с:
Производительность ghc - это единственный компилятор, поэтому невозможно сделать быстрый компилятор Хаскеля. (быстрый компилятор почти Хаскеля возможен - это cocl, компилятор Клина - 0.2 сек полная сборка страниц 10 текста, использующих стандартные библиотеки на Core 2 Duo T9500).
Портируемость на всякое экзотическое - это единственный компилятор, поэтому увы и ах.
Простота разворачивания - это единственный компилятор, поэтому об "./configure; make world opt -j 40" забудьте.
Удаление невзлетевших фич, вроде backpack - это единственный компилятор.
Эксперименты с разными подходами, например в духе MLton/Stalin - забудьте. Хотя в случае ленивых языков это могло бы быть крайне интересно.
Где встраиваемый Хаскель, который может занять место Lua? GHC - единственный компилятор.
-----------------------
Разумеется, часть вещей реализована в том же Клине или Ocaml, которые тоже единственные компиляторы. Но к ним я могу тоже набросать массу претензий.
А, по-вашему, легче портировать имеющийся компилятор, для этого предназначенный, или написать с нуля под новую платформу?
Я же говорю, что это не причина, а просто часть узла проблем. С моей точки зрения, для промышленного (не экспериментального) компилятора спецификация лучше единственной реализации.
В частности потому, что изменения промышленного языка очень часто болезненны.
а то, что надо — оказывается, что проще сделать новый язык с нуля и назвать его, например, идрисом).
Кмк, Хаскель давно надо было "заморозить". И работать с новыми языками.
Отдельный компилятор не нужен.
Это означает, кстати, болезненную зависимость от upstream'а.
Тормозят всякие продвинутые фичи в системе типов.
Ну нет. Компиляция сгенерированного кода для одной и той же xsd схемы на Ocaml PPX и на Template Haskell отрабатывают за секунду и 30 минут соответственно. Причём что там, что там отображения с одних и тех же алгебраических типов. Просто огромного кол-ва.
Ну и компиляция строк 1000 за 0.6 секунд - это тоже перебор. Я прекрасно знаю, что это именно проблема культуры группы вокруг Ghc - те же Clean/Ocaml собирают такое мгновенно.
Просто Ocaml'щики маниакально оптимизируют компилятор и экосистему для скорости (к той же dune масса претензий, но не скорость). А люди вокруг Ghc забивают - stack на собранном проекте секунду что-то внутри себя думает. Хотя make и dune в той же ситуации отрабатывают мгновенно.
Вот это уже будет тормозить, да, но дженерики — это уже продвинутая фича.
Клин с дженериками не тормозит.
В конце концов, есть репл, есть LSP, скорость компиляции в процессе разработки не особо важна, на самом деле.
До REPL тоже надо добраться. В вышеупомянутом случае я ждал эти 10 минут/пол часа.
stack делает далеко не то же, что make.
Вот к этому и претензии. Конкретно в случае stack build, когда всё собрано, он должен проверить даты и выйти. А он занимается ещё какой-то фигнёй.
Это проблема приоритетов.
Это в первую очередь проблема единого компилятора. Было бы их два, второй мог быть промышленного качества и не тормозить.
Клин с дженериками не тормозит.
Я что-то сомневаюсь, что вы под дженериками понимаете одно и то же. Дэдфуд под ними понимает вот это.
Зря. Теперь сравните вот с этим - https://clean.cs.ru.nl/download/html_report/CleanRep.2.2_9.htm#_Toc311798067
То есть, стэку ещё надо выяснить, что там надо собирать.
Очевидно, это можно было как-то оптимизировать по скорости. Было бы желание. Но у сообщества вокруг Ghc этого желания нет.
Без nfs сейчас dune отрабатывает за
А так — кто мешает потратить силы на то, чтобы запилить это же в первый?
Это шутка?
Ну, просто когда над одним компилятором работает куча людей, в том числе на зарплате, заинтересованных в его качестве, и он оказывается медленнее другого, существенно менее популярного языка, на аналогичных по фичам исходниках — это как-то странно.
Фичи, разумеется, не аналогичные, далеко не аналогичные. Но конкретно на сборках быстрее.
Так и не понял, за сколько :]
Core2 Duo T9500, Linux 64-bit; 0.14 сек первый запуск на ppx (3 файла), 0.04 сек - последующие. Проект собран. А я - лох, конечно.
Кстати, сколько компиляторов у clean?
Я же уже писал, что один. Есть ещё где-то компилятор eClean,но он закрыт и точно без генериков и динамики. Это один из недостатков Клина, а их вообще немало.
Но Клин показывает, что можно сделать быстрый компилятор Хаскеле-подобного языка, выдающий более-менее сносный код. Точно также, как Caml Light показывал, что можно сделать хороший компилятор для языка CAML.
Хотелось бы ещё иметь аналог MLton для Клина или Хаскеля. Но очевидно, что такой компилятор не должен быть единственным.
Зато помешало иметь много другого. Нельзя одним компилятором охватить всё. Зачастую к ним предъявляются совершенно противоположные требования. В частности скорость и оптимизация на уровне всей программы.
Я уверен, что какой-нибудь аналог Stalin'а смог бы выжать из Хаскеля больше за счёт хорошего понимания того, где там ленивость нужна, а где - нет.
Так Linux был далеко не монопольной OS.
A Watcom C был open-source? Если вдруг (хотя это очень-очень-очень маловероятно) развитие раста повернет куда-то не туда, не проще ли форкнуть уже существующий отлаженный компилятор, чем писать собственный? Хотя я все равно не понимаю, зачем вам иметь зоопарк компиляторов, чтобы потом мучится и писать проекты, которые должны будут компилироваться под всеми из них и в итоге не использовать ни один из них на полную катушку, зато иметь лес затычек багов то одного, то другого.
Вас под дулом пистолета заставляют обновляться? Если не видите необходимости не пользуйте новые плюшки, видите — используйте. Все просто же
Это некорректный подход. Обновление может быть вызвано совершенно посторонним фактором: например, для нового целевого устройства нужно новое ядро, дистрибутивы с ним содержат новые версии 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 есть только две разновидности макросов: декларативные и процедурные.
>сложность языка имеет свойство перекладываться на сложность программ
В расте очень много сахара, который частично компенсирует сложность bc.
Зависит от масштаба. Мелкая утилита/библиотека на расте будет чуть сложнее. Зато на крупном проекте будет меньше граблей и головной боли с отладкой сегфолтов.
Ключевое слово — кажется. Раст просто в язык добавляет явно некоторые концепции (вроде времени жизни объекта) которые и в Си и в Сипипи существуют, но на уровне "в голове у разработчика". Шаг же от динамики (в голове у разработчика) к статике (записано в типах) всегда же должен приветстоваться, тем более в таких важных вещах как системное ПО.
Если это энкапсулированное число (т.е. код его не использует), то просто оставляет как есть. Если использует, но в виде "просто данных" (например, делает +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, а затем уже в теле цикла меняет <= на !=. Вот удобнее ему так. Но имеет право. А вот если бы совсем выкидывал проверку — был бы неправ.)
Они не устойчивы. Но в Linux есть защита, что если драйвер повредил только собственные структуры, то ядро хотя бы успеет сказать «мяу» и скинуть лог проблемы. Срабатывает не всегда, но достаточно часто, чтобы собирать трейсы, логи и прочие данные, по которым можно понять, что случилось.
А в остальном — повезёт или не повезёт.
Проблема с Rust, насколько я вижу, в интеграции его рантайма. Пока что есть с этим проблемы — в отличие от C, которому в варианте ядра Linux достаточно было настроить стек на кусок RAM, argc+argv и какой-то простейший malloc, дальше он уже работал.
Но, думаю, их решат без особых затяжек.
Если я всё ещё не на то отвечаю — переформулируйте.
Нет, нет никакого лока. Ядро стартует когда нет ничего, кроме непрерывного куска памяти с кодом и процессора. В этом режиме очень хочется не отвлекаться на всякие магические процедуры, которые непонятно когда запускаются и сколько времени тратят. А вот когда ядро стартануло, запустило все дрова и инициализировало вверенное оборудование, то тогда можно и свистелки-перделки запускать.
memset - это стандартная функция. Она есть в стандарте. Более того, сам Линукс к подобным функция не привязан. Более того, если не ошибаюсь, там вообще стандартная библиотека в ядре отсутствует.
А причина, по которой не хотят переносить ядро на другой язык, кроме того, что придется все переписывать, заключается в том, что другие языки требуют для работы программы поддержку со стороны стандартной библиотеки (runtime), а вот программа на Си не требует - все конструкции языка будут компилироваться даже без библиотеки.
Вы не поняли. Ядро это приложение не "пользовательского пространста", где аварийное завершение работы, в общем, не является катастрофой. Как само ядро, так и его модули должны иметь возможность продолжать работу даже при "обстоятельствах непреодолимой силы", до тех пор, пока позволяет железо, а неотключеамое состояние паники совершенно не вписывается в сценарий использования.
Но ведь такие обстоятельства существуют и для C, например UB. Почему вызов паники из C-кода ядра - хорошо, наличие UB в C разработчики ядра терпеть готовы, а панику рантайма Rust - нет?
Причины для паники могут быть очень разными.
Как тут пишут - Раст паникует если не может выделить память.
Если же драйвер в ядре не может выделить память - он просто возвращает ошибку и все работает дальше.
Но на самом деле проблема с паниками - это не самое страшное. Я тут недавно читал цикл постов про различия в моделях памяти между растом и ядром. Там все куда сложнее. Хотя и менее очевидно.
Эти штуки из разных плоскостей. Undefined Behavior не означает аварийное завершение работы, тем более в пространстве ядра, а является формальным описанием области значений, при нарушении инварианта, которые будут отличаться от ожидаемых. Например, если Вы нарушаете закон, то Вас могут найти правоохранители и наказать, а могут не найти и всё будет ок.
Вообще, даже в высокоуровневых языках, при нарушении инварианта, можно получить неожидаемое поведение и это не означает, что все языки нужно отменить.
А процессору пофиг, он вообще не знает, знаковые у него там числа или беззнаковые (исключая инструкции умножения и деления)
Ну а так, мусор на входе - мусор на выходе.
Потом, правда, ракеты падают и десятичная точка в сумме при банковском переводе сдвигается.
Накосячить таким методом можно с любым языком.
Процессору пофиг. А вот компилятору нет. Сегодня ты компилмруешь для процессора с 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 вот: 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: скосил нетехническую часть, так лучше для долгой истории. Всё остальное осталось в личке.
Как в С определить знаковый 24х битный тип данных для переменной int24_t?
Если аппаратной поддержки такого типа нет, то структура с битовым полем или int32_t внутри (структура специально чтобы избежать случайного использования стандартных операторов с нестандартным типом). В C++ мы сделали бы отдельный класс для этих целей, перегрузив арифметику, но в C вместо операторов добавим функции вида int24_t add(int24_t a, int24_t b), которые внутри будут работать с int32_t и обрабатывать ситуации, когда результат вычисления выходит за границы int24_t.
Нет, недостаточно, в вашем подходе нужно рассмотреть все варианты, ну например a = b = INT_MIN.
Посмотрите на код ещё раз — этот вариант там обрабатывается.
умножение двух интов сразу даёт 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-х битные числа.
ни один язык, что я знаю, не предоставляет информацию о переполнении.
FYI, в питоне целые сразу произвольной длины "из коробки". Типа как строки. Поэтому для вычислений там все юзают 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 предоставляет.
Цена 64-битнго сложения на 32-битной машине равна двум 32-битным сложениям: сложение и сложение с переносом. Ничего тут страшного нет. Другое дело, что неправильно так делать для проверки, с учетом того, что сам процессор, обычно, знает, когда произошло переполнение.
Согласен с тем, что в Си нет проверки на переполнение. Было бы очень здорово, если бы оно было в виде int add_int(int *a, int b, int с) { ... *a += b + c; ... } , где возвращаемое значение 0 - если все в порядке, -1 если переполнение в отрицательную сторону, 1 - в положительную. В этом случае очень удобно бы было делать операции с многоразрядными числами.
Почти то же (без знака переполнения) есть в GCC, Clang и вслед за ними ICC. Осталось протолкнуть в стандарт :)
Знак можно вычислить по другим признакам (например, если вообще произошло переполнение, то в ваших терминах b!=0 и знак b соответствует направлению переполнения). Этого достаточно.
Вот бы ещё у раста с портабельностью (в т.ч. на "старое" железо) было получше...
А ещё со временем сборки. Как самого тулчейна, так и программ на нём...
А ещё с весом конечных бинарников...
INB4: "в 21 веке считать ресурсы".
Размер бинарников во многом зависит от runtime'а (который предоставляет много сервиса, типа разворачивания стека при паниках и т.д.). Рантайм в расте опциональный, и люди вполне пишут под embedded без него.
Вторая проблема - generic'и, которые сильно раздувают код (хотя и обещают работать быстрее). Возможное решение - использование динамической диспетчеризации (чуть медленее, зато компактнее).
В целом, я не видел, чтобы rust генерировал код существенно больше, чем Си (для этого надо смотреть на размер кода функций, а не итогового бинаря, потому что, повторю, опциональный рантайм толстоват).
А вот с поддержкой странных систем... Это да.
С другой стороны, там иногда такая поддержка Си, что попытка там собрать хоть что-то большое вызывает страх и ужас.
У меня знакомый пишет под старое железо с DOS и микроконтроллеры — все отлично с портабельностью у раста. Не си конечно, но вполне достойно
Время сборки — в релизе с lto да, печально, есть куда улучшать. В остальном — вполне можно жить
На мой взгляд более "прямой" заменой Си является Zig (https://ziglang.org/), хотя он и менее известен, чем Rust.
Насколько я вижу по текущему описанию языка, выравнивание - часть типа указателя, указатели на отдельные переменные и на массивы просто так не смешиваются, арифметика указателей для простых указателей не допустима и т.п. Т.е. практически все потенциально небезопасные операции требуют как минимум явной переинтерпретации указателей через целые числа или другие "силовые" преобразования.
Но может я плохо понимаю, какие моменты Вы имеете ввиду ...
Теперь ждём ещё 30 лет для пробивания этого в стандарт.
Я не настолько глубоко в gcc, чтобы понять о чём речь. Грубо говоря, если я сделаю int(max_int)+1 - оно такое отловит? А int(max_int/2)*2? А int (min_int)*int(min_int)?
Да, отловит. Если вы сделаете, например,
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 надо генерировать ошибку явно и вышибать выполнение (или просто ставить флаг, если явно попросили, или механизм немедленной реакции не работает).
(А если компилятор уверен, что переполнения не будет — он выкинет проверку, это законно и желательно.)
Ну а «на малопроизводительных железках с высокими требованиями к реалтайму» всё равно особая среда, которую можно явно пометить.
Да, по сути верно. Единственная загвоздка тут в том, что знаковое переполнение, ровно как и многие другие случаи неопределенного поведения, большинством программистов считалось правомерным. В этой позиции есть смысл, если думать о Си как языке низкого уровня. В действительности это не так. Отсюда и огромное количество сломанного кода, и требование откатить изменения компилятора.
Этой проблемы никогда не было и сейчас нет: оптимизации используются потому, что нашлась лазейка их разрешить, а не ради мифических странных платформ.
Отсутствие этих оптимизаций для операций с беззнаковыми это чётко показывает.
Немного даже иронично, что человечество, строя систему на базе такой строго предсказуемой вещи, как машина Тьюринга, в итоге дошло до такой магии в коде :)
Я этого не говорил да и полагаю, что они оба просто высокоуровневые. Ни один из них не более или не менее высокоуровневый, чем другой, так как нормальных критериев я придумать не могу. Да и какой смысл? Вообще я имел в виду немного другое - большое количество программистов предполагают, что операции в Си будут имеют ровно такое же поведение, как и соответствующие им инструкции на целевой архитектуре. Т.е. если вы скомпилировали код под x86, то битовый сдвиг на размер переменной должен оставить ее нетронутой. В теории и на практике это так не работает. Поэтому Си - это не язык низкого уровня. То, как в стандарте языка определяется битовый сдвиг или знаковое переполнение не влияет на то, станет он от этого более высокого или низкого уровня (если только прямо не сказано, какую инструкцию должен использовать компилятор при трансляции). Да, неопределенное поведение позволяет совершать более агрессивные оптимизации, но всему есть предел. Си просто переполнен UB, приличная часть которого вообще имеет мало смысла. Конкретно проверять знаковое переполнение перед каждой потенциально опасной операцией гораздо неудобнее, чем это делать после.
В каком-то языке (не помню каком) так и было определено поведение при переполнении, и на x86 компилятор инструкции into вставлял после арифметических операций.
TurboPascal при соответствующем режиме компиляции (директива компилятора {$Q+}). Можно было выключать в определённых местах.
Если бы в стандарте языка было указано, что вся целочисленная арифметика явным образом использует дополнительный код, то для тех архитектур, которые ее не поддерживают, пришлось бы имитировать такое поведение. Впрочем, тут нет ничего чрезвычайно страшного - компиляторам языка Си и без этого приходится программно реализовывать те операции, которые целевые архитектуры также не поддерживают. Так, например, для тех машин, которые не содержат инструкций для целочисленного деления и не только, gcc использует специальную библиотеку libgcc. Подробнее можно почитать здесь:
Нет, всё строго наоборот, насколько я понял вашу метафору.
Пользователи хотят иметь возможность сделать предохранитель к пистолету, потому что контролировать все действия и все ситуации невозможно — чтобы он не выстрелил, например, при падении с лошади. (Самые продвинутые согласны на убирание предохранителя, но только по своему явному желанию — заменить стандартизованной заглушкой.)
Разработчики компилятора говорят, что предохранитель утяжеляет конструкцию, и если пользователь не может предусмотреть все ситуации, то он сам виноват, а падать с лошади не надо. (Скрытым намёком идёт, что кто хочет безопасности — пусть берёт пистолеты от 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)
Есть и противоположная точка зрения - https://dreamsongs.com/WorseIsBetter.html
Возможность намеренно выстрелить себе в ногу. А не неявно и случайно. Это принципиальная разница.
Вы можете написать rm -rf / и оно удалит все файлы (если от рута). Но надо было его вызвать с -r и -f. Просто «rm /» такого не даёт, и с какого-то раннего момента по одной из этих опций — тоже (причём Bell V6 давало это по rm -r, а System III и ранние BSD, насколько помню, уже нет). «rmdir» тоже, оно удаляет только пустой каталог.
И это принципиально. Обсуждаемые тут фишки это неожиданный выстрел в ногу от действий, которые такого не предполагали.
Я так понимаю, неожиданный выстрел в ногу получился не от того, что Си допускает неопределённое поведение, о котором все кому надо давно знают, а от того, что конкретный мейнтейнер конкретного компилятора начал усердно реализовывать стандарты. Правильно Линус сказал: когда текст стандарта противоречит реальности - он является обычным куском туалетной бумаги. Стандарты должны помогать, а не мешать. Кстати, это происходит в естественных языках: когда неправильное употребление становится всеобщим, оно становится стандартом :)
Мне вот интересно стало, если Линусу не нравится реализация GCC, то где можно увидеть ядро, собранное альтернативным компилятором? И легко ли это сделать в принципе?
Проект, на котором я сейчас работаю, успешно собирается на С++ компиляторах от 3-х вендоров. Этот проект бесконечно мал, по сравнению с ядром Линукса, но и ресурсы на его разработку также бесконечно малы.
Вопрос: если новый компилятор GCC так не устраивает Торвальдса, то почему он ест этот кактус?
Ну вообще-то прямо сейчас ведутся работы для возможности компиляции ядра линукса также и на clang.
Не то что "ведутся работы для возможности", а Андроид собирают clang-ом! https://source.android.com/setup/build/building-kernels#customize-build
Ну, как минимум, два альтернативных компилятора умеют:
https://www.kernel.org/doc/html/latest/kbuild/llvm.html
https://www.linuxjournal.com/content/linuxdna-supercharges-linux-intel-cc-compiler
Причём, icc - аж с 2009 года!
Просто это хреновая философия. У вас же перила на лестнице например стоят и двери у лифта закрываются — зачем? Нормальный человек ведь в шахту не упадет. Надо было специально подойти к двери (-r) и шагнуть вперед (-f), любое одно действие из этих не дают такого результата.
Кому от этого легче только.
Нет, -r это означает перелезть через перила, если они есть, а -f — проигнорировать отсутствие лифта. Такая аналогия сильно точнее вашей (полного соответствия, естественно, не будет, но и не требуется).
> Просто это хреновая философия.
При таких интерпретациях — да.
> Кому от этого легче только.
Тому, кто таки обращает внимание на то, что делает.
Сейчас вон мода в UX отменять запросы подтверждений — мол, их никто не читает и всегда жмёт «да»… но мне они реально помогают, иногда остановиться «перед краем» как раз подумав, что тут может быть что-то не так.
Нет, -r это означает перелезть через перила, если они есть, а -f — проигнорировать отсутствие лифта. Такая аналогия сильно точнее вашей (полного соответствия, естественно, не будет, но и не требуется).
То есть -r это что-то плохое чем никто не пользуется? Или это нормальный флаг без которого папочку не удалить если в ней что-то есть?
В реальности же на щитке кроме "не влезай убьет" ещё всегда замок висит. И если человек влез и погиб то виноват будет не он, а тот кто замок сломал. У вас же почему-то "ты че дурак не прочитал что написано" — ну ок
Это флаг, который явно говорит «удаляй каталог несмотря на наличие содержимого», и если он указан — то это значит преодолеть соответствующую защиту.
> Или это нормальный флаг без которого папочку не удалить если в ней что-то есть?
Если вы уверены, что надо удалить с содержимым — да. Но надо вначале стать уверенным (или слишком уверенным).
Для удаления пустого каталога отдельно есть rmdir, и я его регулярно зову в случае, если хочу получить эффект «непустое не удалять!»
> И если человек влез и погиб то виноват будет не он, а тот кто замок сломал.
В этой аналогии, как вы представляете себе, чтобы один сломал замок, а другой влез?
Админ ходит по системам и по ночам пишет `alias rm='rm -rf'`, или как?
Я, наоборот, часто вижу `alias rm='rm -i'` как защитное средство, и поддерживаю (в интерактивном режиме).
> У вас же почему-то «ты че дурак не прочитал что написано» — ну ок
И опять же хромая аналогия. Пусть есть на двери обычная ручка, которая чтобы войти (это как вообще вызвать rm), а есть особая, которую ещё надо нажать несмотря на красный цвет и предупреждение. Кто захочет это делать просто так? Ну да, может, 0.1% захочет. Ну так и файлы себе постоянно удаляют даже с предупреждением.
Цель всех этих средств — не тотальная защита от всей глупости, это невозможно — полный дурак или самоубийца всегда найдёт метод — а защитить от ненамеренных ошибок. А для них опций типа -r, -f достаточно (при прочих равных).
Неправильно.
Суд по листингу, который привел felix-gcc, раньше assert(a + 100 > a) прерывало программу в случае переполнения (что в общем довольно логично — отрицательное число явно не больше положительного), а после очередного обновления — перестало… Типа на основании того, что такое поведение является «неопределенным», а потому — нарушением стандарта и не хрен такой код выводить в прод… ;)
В вашей аналогии люди знали, что можно случайно выстрелить в ногу, сооружали кастомные предохранители, но в очередной версии
Простите, что пользуюсь Вашим комментарием, чтобы выразить своё мнение. В своё оправдание скажу, что темы схожи. Если я правильно понял статью, Си должен умереть потому что:
стандарт языка - фуфло;
разработчик (ОДИН!) средства разработки (ОДНОГО!) этого не понимает и отказывается использовать СУЩЕСТВУЮЩУЮ ОПЦИЮ по умолчанию;
в качестве замены, как системного языка, появился юный Rust.
Я всё верно понял? Т.е., вопрос не к выразительным средствам языка, не к тому, что символ "*" в Си без контекста не понять (что есть правда)?
Я не защищаю Си и не обвиняю Rust. Я лишь хочу указать на сомнительность аргументации и использование цитат "больших программистов" в своих целях. Как пример последнего, отсылаю к цитате из переписки Торвальдса, который говорит (ИМХО) верно: если документ мешает писать, то к чёрту документ! Из цитаты Торвальдса "Это то, почему мы используем "-fwrapv", "-fno-strict-aliasing" и другие флаги." я делаю вывод, что у него претензии-таки не к Си. Хао! А теперь минусуем. :)
В примере с check_password есть ошибка: поскольку буфер pwd — внешний, выкидывать memset для него компилятор не может (по крайней мере, до тех пор пока не заинлайнит тело функции на уровень выше). Да и изменение памяти по указателю на константу является ошибкой само по себе, независимо от оптимизаций.
Вот какой вызов на самом деле не помешал бы и какой компилятор имеет полное право выкинуть — это очистка буфера 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)
Но, с другой стороны, мне кажется, что именно смерти 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 неисчерпаема. Идея была в том, чтобы провести неискушенного в тонкостях стандарта читателя от достаточно тривиальных ошибок до гораздо более изощренных и опасных случаев неопределенного поведения. Безусловно, говоря о том, что Си должен умереть, я сам не надеюсь на то, что это произойдет. Смысл в том, что вокруг этого языка существует огромное количество серьезных заблуждений, и вызваны они отнюдь не только непрофессионализмом тех, кто на нем пишет. И учитывая историю Си, и то, как написан стандарт, неудивительно, что ошибки неопределенного поведения стали обыденностью. Я сам долгое время считал, что Си - это своего рода кроссплатформенный ассемблер. Потому я полагаю, что концепция языка, который создает столь опасную иллюзию , является по меньшей мере неудачной. И в том случае, если Си остается с нами надолго (а у меня нет оснований считать иначе), очень важно, чтобы программисты четко осознавали то, какой именно инструмент они используют.
Я вот считаю, что тут нужно призывать именно к смерти Си. Для низкоуровневого языка он делает слишком много предположений о том, как нам жить. А для современного высокоуровневого языка он не дает достаточной изоляции от платформы.
Очень интересная и конструктивная статья. Сложно не согласиться с Вами, особенно принимая во внимание, что
Зачастую на отладку и тестирование программ уходит больше времени, чем на проектирование и написание самого кода.
И данная проблема, действительно, имеет место быть. Что зачастую отталкивает начинающих программистов в изучении Си.
Когда начинаешь познавать такую необъятную и весьма трудоемкую вещь как Си изначально интересно, захватывает, но попутно возникает слишком много булыжников и непонятных вопросов. И в конечном итоге приходишь к не самым приятным эмоциям от языка.
Тем более, беря в расчёт, что на данный момент уже существует множество различных языков, надстроек и возможностей в разработке, что Си предстаёшь в особенно невыгодном и отталкивающем виде.
Курсы для новичков про это обычно не рассказывают. Затем и нужны статьи, чтобы потом объяснять, что реально здесь водится целая стая голодных драконов.
> Гарантии не даются не по причине «мы хотим натворить ЗЛО», а потому, что никто не может их дать. Ты пишешь стандарт сегодня, а завтра 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 других признаков типовых современных платформ
Особенно код грея (который не используется для хранения и работы с данными в памяти) и троичное счисление.
Вы вообще смотрите, на что отвечаете? Я говорю, что это требование языка к среде реализации кода: память состоит из сущностей, именуемых «байт» и имеющих не менее 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, называю это массивом, как бы это кому-то ни не нравилось.
Называй как хочешь, устройство языка это не меняет.
И к чему эта простыня?
Исходная же статья частично поднимает вопрос — а как всё это получилось? Абьюзинг всех изначальных 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. Да, программируют их обычно не на С, но как явление их упомянуть надо.
Ну некоторые 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с которым согласен. Вы описываете Си как язык высокого уровня, и конечно тогда он имеет право давать любые гарантии и любую модель исполнения. Вот только как язык высокого уровня он плох и даёт сложные для понимания и контроля гарантии (если для вас это не так -- поздравляю, вы в сотни раз умнее меня =) ) -- и тогда вопрос -- а зачем он нужен? Ведь когда мы пишем программы мы хотим что бы они работали -- а в силу сложности Си гарантировать это ... сложно. И при этом не важно насколько быстро они работают -- если они работают с ошибками, неправильно -- то они просто не работают.
Только вот проблема, что ни один язык не может дать таких же гарантий производительности. Переписал тут недавно небольшую ВМ на rust - замучался выпиливать проверки границ( при помощи того самого страшного unsafe кода ), т.к. с ними было медленее на 10-30% в зависимости от семпла. И даже после этого только приблизился к результату с++
Программисту, стоит указать, что этот код оптимизировать не нужно, и бац проблемы не существует.
Так как, как указать, что код оптимизировать не нужно!?
Благодаря Си Linux самая популярная серверная ОС
Не-а. Я бы даже сказал, что Linux — это великолепная иллюстрация тому факту, что крупные проекты на C писать невозможно. Почему? Да потому что Linux написан не на C, а на C с расширениями языка от GCC — вообще говоря, другой язык.
(самый переносимый язык кстати, один раз написал (если в пределах стандарта) собрал для любой платформы)
Вот с этим "в пределах стандарта" как раз большие проблемы. Приличная долю UB можно обнаружить, только проанализировав текст программы целиком, а у людей мозги маленькие, в которые вся программа целиком не умещается.
Я читаю этот коммент и кажется вы обижаетесь на то, что си несправедливо обругали и говорите, что не язык виноват, а плохие человеки в вообще вон нам язык линукс дал.
Фишка в том, что из двух извечных вопросов важный "что делать", а виноватых искать — последнее дело. Человеки — ошибаются и им нужна помощь машины в виде компилятора чтобы писать сложные программы. И чем больше этой помощи тем лучше. В си из-за особенностей языка такой помощи дождаться тяжело, и изменить это не меняя самого языка не выйдет.
Относится к действительно неопределенному поведению как к неопределенному — разумно
Относится к действительно неопределенному поведению как к определенному — рискованно, если не глупо
Можно попробовать договориться и сделать действительно неопределенное поведение действительно определенным, но в статье, кажется, не о том речь
Неопределенное поведение не делают определенным, как было упомянуто автором статьи, в качестве loopholов, чтобы язык стал действительно портируемым, не привязанным к железу и компилятору, так как некоторые вещи банально выполнены в них по-разному.
Тем не менее, компилятор почему-то выбрал не спустится на уровень своей платформы и не реализовать "неопределенное поведение" определенным именно для этой платформы образом (для чего этот термин изначально и вводился!), а сделать так, как ни одна платформа не делает и молча. Это свинство.
Для таких случаев используются термины unspecified behavior и implementation-specified behavior.
Это свинство.
Разрабатывая программу на стандартном C, разработчик принимает контракт, в рамках которого он не будет писать код, приводящий к неопределённому поведению, а компилятор не будет вести себя неопределённым образом. Эти правила известны разработчику заранее.
Скажите, пожалуйста, сколько людей на планете физически способны писать без UB (опуская вопрос "нужно ли писать без UB", всё-таки UB — это стандарт, а мнения по его адекватности расходятся)? И если ответ "бесконечно мало", то не является ли свинством делать то, что делают современные компиляторы?
Мало. Что, впрочем, справедливо и для многих других классов ошибок — ведь почти никто не считает свинством, что если программист забудет прибавить или отнять единичку, то компилятор сделает не то, чего на самом деле хотел человек.
Чем же тогда контракт "я не буду писать UB-код" хуже контракта "я не буду совершать off-by-one errors" или "я не буду допускать логических ошибок при инвалидации кэша"?
Современные процессоры при обработке данных делают не в точности то, что написано в исполняемом коде, а могут сделать что-нибудь другое, но так, чтобы результат был неотличим от непосредственного исполнения каждой инструкции наивным образом при условии, что программа не содержит data races — и большинство пользователей процессора довольны тем, ведь это позволяет ему работать быстрее, хотя очень мало людей могут никогда не допускать гонок в своих программах. Оптимизирующие компиляторы C делают нечто похожее — они генерируют из исходника такой код, результат работы которого неотличим от интерпретации исходника абстрактной C-машиной при условии, что программа не содержит UB.
ведь почти никто не считает свинством, что если программист забудет прибавить или отнять единичку
Не считают потому, что и прибавить, и отнять единичку — это обе валидные операции в терминах языка. ИИ еще недостаточно умный, чтобы понять, что от него хочет человек.
А вот в описываемых ситуациях одно поведение явно позиционируется, как хорошее, а другое — как плохое, но проще же сидеть на попе ровно и ничего не делать, чтобы дать об этом знать (если явно не попросили заткнуться).
Off-by-one практически всегда ловятся в пределах одного экрана (ну, можно сделать, чтобы не поймались так, но это надо постараться). В случае кэша есть гарантированный метод — не идти на всякие lock-free, а ограничиться мьютексами (или вообще обменом сообщениями): дороже, но надёжно. В случае описанных ошибок C защититься без полного контекста (включая всякие библиотечные функции, которые могут обновляться независимо от основного кода) нереально, и вопрос не в производительности — если бы было только в ней, большинство бы использовало безопасный режим по умолчанию (как я и предлагаю в десятке соседних комментариев).
Хорошие системы предполагают, что кроме известных правил вас предупредят о том, что вы близки к тому, чтобы их нарушить. Пора очнуться от ощущения, что разработчик способен уместить в своей голове всю программу — это не так уже лет 20. Это забота компилятора. Собственно, основная претензия к C/C++ компиляторам именно в том, что они делают преобразования молча. И не говорите мне, что контекст к тому времени уже потерян. Просто и нет попыток его сохранить.
> Эти правила известны разработчику заранее.
Неизвестны. Не принимает. Во всяком случае, большинство интернов и джунов про эти проблемы не подозревает, и их не учат.
Если вы возьмёте типовую книгу «C для начитающих», «Освоение C++» и тому подобное, там или ни слова про это не будет, или будет очень вскользь, даже если это книга от какого-то корифея языка, который десять собак на нём съел.
Можно, конечно, спрятать голову в песок и сказать, что это проблема образования. Но сейчас это проблема всех нас — когда код взрывается.
А как спуститься на уровень платформы, если на 2х разных платформах подразумевается совершенно разное поведение с разными результатами? Тот же пример со знаковым переполнением, понимаю что заезженно и уже неактуально после с++20, но гипотетически на платформе Winux отрицательные числа представлены не не доп кодом, то есть невозможно один и тот же код допускающий ub скомпилить, чтобы он выполнял одну и ту же работы для этой платформы и, например, linux
Говоря об упомянутом в письме ядре Linux, его автор Линус Торвальдс также дал свою оценку strict aliasing в частности и работе комитета в целом.
Когда знаешь, что следующие три минуты чтения пройдут очень весело.
Си умрёт только тогда, когда будут широко доступны всем квантовые компьютеры. Потому что он как кроссплатформенный ассемблер: плотно работает с железом
Зря заминусовали.
Си это Unix поверх машины Тьюринга. Си неулучшаем. С изобретением бульдозера не наступила смерть лопаты, шуроповерт не уничтожил обычную ручную отвертку.
Нельзя что-то улучшить не ухудшив одновременно какие-то старые свойства.
Главная проблема в том что все эти ошибки позволяет совершать аппаратная сущность - процессор.
Требуется радикальная смена парадигмы. Такой вычислитель, который отправит в каменный век Unix.
Хорошая статья. Плюсанул везде, где мог.
От себя скажу лишь одно - умрет. Обязательно умрет. Как умер Кобол, как умер Фортран, как умер Ада, как умер Паскаль... Так устроена жизнь - старички умирают уступая место молодым. Самые легендарные продолжают жить в мифах и легендах. Си уже более чем седовласый старичок. За время его жизни сменилась не одна парадигма программирования и не одно поколение вычислительных машин. Потому даже не сомневаюсь - умрет.
Вопрос ведь не в том умрет ли. И даже не в том когда именно. Вопрос в том, что будет потом. Появится ли тот самый "квантовый компьютер" - единственный и неповторимый, под который получится написать сразу и бесповоротно единственный и непротиворечивый стандарт без костылей в виде неопределенного поведения в бесконечном будущем? Помнится JAVA VM в свое время пыталась стать таким вот "универсальным компьютером"... И не учит ли нас та же JAVA VM тому, что у такого пути есть свои недостатки? Или у нас будет зоопарк архитектур, в которых подсчет бит в виде BE/LE и особенности работы со знаковыми и беззнаковыми числами это минимальное из зол? И кто возьмется написать что-то, что накроет все прошлые и еще живые архитектуры в купе с настоящими и будущими?
У меня нет ответов ни на один из вопросов.
Всё течёт, всё меняется. =)
К сожалению я тут немного пессимистичен, Никита ещё более -- опыт PL/I, например, видимо не был учтён, многие языки сегодня всё так же безразмерно разбухают. Боюсь что с Си будет то же -- он уйдёт, на его место придёт почти такой же, не сделавший выводов из ошибок прошлого. Надеюсь эта статья поможет кому-то в будущем сделать лучше, хотя это и излишне оптимистично. =)
Что касается заданных вами вопросов -- я тоже не имею на них ответов. =)
Сложно гадать о том, что будет. В первую очередь по той простой причине, что приходится залезать на чужой огород, в котором мало что понимаешь.
Мне кажется нас ждет некоторая "семиуровневая модель OSI" применительно к вычислительной технике. Где каждый уровень будет максимально изолирован от соседнего. Ассемблер (в смысле машинные коды) в обозримом будущем скорее всего не исчезнут. Значит нужен будет слой абстракции, который так или иначе приводит их к некому единому формату. В идеале максимально эффективно. Опять же в идеале не противоречиво. Вопрос лишь в том как этого достичь...
В принципе, сейчас наблюдается что-то такое. Абсолютно не ведающие про "безопасность" машинные коды накрываются уровнем ядра операционной системы. Выше libc или ее аналоги WinAPI. Еще выше всякие QT/GTK/MFC/NET. Другое дело что сегодня нет строгой изоляции между этими уровнями...
А может быть действительно удастся создать некий "универсальный стандарт проектирования кода" и нужда в строго разделенных уровнях абстракции просто закончится.
Мне кажется, что первый вариант на сегодня более вероятен. Но много я на него не поставлю. Как, впрочем, и на второй. И на третий, и на десятый, и на сотый... Поживем - увидим, доживем - порадуемся.
И какая же парадигма программирования сменилась? ФП - мёртворождённое, а императивщину никто не отменять не собирается
Фундаментальная несовместимость с физическими императивными вычислителями? Не, я слышал что-то про прототип Лисп-машины, но сейчас-то их нет нигде.
А вне контекста этой статьи (системное программирование) всё у ФП прекрасно, разумеется.
просто, как сделали seL4, взять хаскель и обмазаться верификацией.
Точнее, взять хаскель, написать прототип реализации, верифицировать. Смотря на этот прототип, написать реализацию на подмножестве С для улучшения производительности, верифицировать эту реализацию. См. http://www.sigops.org/s/conferences/sosp/2009/papers/klein-sosp09.pdf
Что-то ваша ссылка на ivorylang.org не работает...
Скажите, насколько просто на Хаскелле реализовать, например, in-place quicksort (in-place обязателен)?
Очень просто - пишете в гугеле haskell in-place quicksort и копируете оттуда. Занимает примерно столько же, сколько in-place код на C, т.к. это непосредственный перевод.
Во-первых, попробуйте найти. Типичные in-place версии требуют O(N) памяти (что сразу убирает половину смысла in-place алгоритма).
Во-вторых, это ведь "например". Далеко не всегда вы сможете скопировать код необходимого вам алгоритма где-нибудь в интернете.
https://hackage.haskell.org/package/array-0.5.4.0/docs/Data-Array-MArray.html
Converts an immutable array (any instance of IArray) into a mutable array (any instance of MArray) by taking a complete copy of it.
https://hackage.haskell.org/package/vector-0.12.3.1/docs/Data-Vector.html
O(n) Yield a mutable copy of the immutable vector.
То есть O(N) по памяти, насколько я понял?
ООП, Реактивное Программирование, Логическое Программирование.
Это видимо сарказм, про мертвые языки? Всё перечисленные очень живы, и даже пресловутый Cobol.
Безусловно. Не все, к сожалению, понимают сарказм. Особенно когда нет соответствующего тега.
Боюсь мой уровень сильно ниже. В основном от reset-вектора до exec("/sbin/init"). А порою даже ниже, чем вектор сброса.
Собственно потому и говорю что сложно рассуждать про чужие огороды. Там все не так, как у меня. Мое "всякие" не содержит негативного оттенка. Оно скорее меня характеризует. Как человека не знакомого с данным конкретным слоем. Каждому свое.
А то как у всех миллиниалов у вас будет очень искажённое восприятие действительности, оторванное от реальности.
Как например слухи о скорой смерти СИ.
Знакомый ник. Мы ж с вами где-то уже пересекались на тропке хабровских комментариев... Занятно все это... Вот я уже миллениал (хотя практически ровесник языку С - 1979-ого года выпуска). Вот я уже распускаю слухи о скорой смерти языка С (основного моего рабочего инструмента). Весело.
Я ж не писал что С скоро умрет. Я писал "не сомневаюсь - рано или поздно умрет". И дальше список условно мертвых языков. Каждый из которых когда-то был универсальным, а потом оказался нишевым. И в своей нише продолжат жить (а в некоторых случаях еще и процветать). С (классический, не приплюснутый) уже давно не универсальный язык. И названные в статье проблемы весьма в немалой степени этому способствовали. Это достаточно очевидно. Как очевидна и связка С с (ранним?) UNIX и вытекающие отсюда проблемы. Беда в том, что по моему только настоящие проектировщики встраиваемых систем понимают что эта сцепка не жесткая, и в принципе не обязательная. Но это не формат комментария... Это тема для статьи. А на нее как водится категорически не хватает времени. А если совсем честно, то и желания. Она все равно у подавляющего числа хабражителей не вызовет ничего, кроме изжоги. Это я не хочу топтать их огород. Они мой завсегда и с удовольствием. Потому доношу тем, с кем работаю. Когда дозревают до понимания.
И ладно когда "молодые и горячие" что-то подобное мне предъявляют. Это нормально. Каждый приходящий ко мне в отдел пытается меня перекричать и сдать в утиль. Пока, правда безрезультатно. Но от человека, который "делал драйвера для первого смартфона с Линукс компании Самсунг Электроникс" подобного явно не ожидаешь.
Лампы... А знаете как легко и здорово объяснять цифровую схемотехнику с помощью обычных реле. Нормально замкнутых, нормально размкнутых, переключающих. Как классно звучит (во всех смыслах этого слова) "сдвиговый регистр на электромеханических реле"? И насколько после этого по другому воспринимается Шеноновская нетленка "Надежные схемы из ненадежных реле"?
А лампы... Нет, спасибо. Меня "теплый ламповый 100Гц гул" с детства достал... Не ощущаю я в нем той могучей аудиофильской силы. И тем более не интересно в плане цифры. Впрочем, многосеточные лампы это сила. Практически не имеющая себе аналогов. Но опять же - сие тема совсем другого разговора.
Си должен умереть