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

Комментарии 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 в новые функции выделения памяти не очень понятно.

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

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

Именно так.

Я сильно сомневаюсь, что простому системному язычку нужно 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 спортировать?

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

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

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

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

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

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

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

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

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

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

Иначе мы завязнем в болоте 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? Выглядит так что они так и не перешли путь в никуда, даже с собственным компилятором. Который был нужен по сути только для выжимания скорости в академических условиях, какие в нем баги и уязвимости одним только исследователям известно.

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

И как много кода сейчас пишется или, прости господи, компилируется в режиме С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 removes that end up becoming permanent fixtures for 17 years.

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

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

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

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

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

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

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

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

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

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

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

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

А зачем?

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

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

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

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

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

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

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

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

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

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

Я не уверен насчёт 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, но все же)))

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Ну нет. Компиляция сгенерированного кода для одной и той же 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, когда всё собрано, он должен проверить даты и выйти. А он занимается ещё какой-то фигнёй.

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

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

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

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

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

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

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

Очевидно, это можно было как-то оптимизировать по скорости. Было бы желание. Но у сообщества вокруг 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? Если вдруг (хотя это очень-очень-очень маловероятно) развитие раста повернет куда-то не туда, не проще ли форкнуть уже существующий отлаженный компилятор, чем писать собственный? Хотя я все равно не понимаю, зачем вам иметь зоопарк компиляторов, чтобы потом мучится и писать проекты, которые должны будут компилироваться под всеми из них и в итоге не использовать ни один из них на полную катушку, зато иметь лес затычек багов то одного, то другого.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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


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

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

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

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

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

Если я всё ещё не на то отвечаю — переформулируйте.
НЛО прилетело и опубликовало эту надпись здесь

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

int a, int b;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

И вот это вот:

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

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

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

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

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

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

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

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

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

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

UPD: скосил нетехническую часть, так лучше для долгой истории. Всё остальное осталось в личке.

Как в С определить знаковый 24х битный тип данных для переменной int24_t?

Если аппаратной поддержки такого типа нет, то структура с битовым полем или int32_t внутри (структура специально чтобы избежать случайного использования стандартных операторов с нестандартным типом). В C++ мы сделали бы отдельный класс для этих целей, перегрузив арифметику, но в C вместо операторов добавим функции вида int24_t add(int24_t a, int24_t b), которые внутри будут работать с int32_t и обрабатывать ситуации, когда результат вычисления выходит за границы int24_t.

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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


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

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

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

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

НЛО прилетело и опубликовало эту надпись здесь
> Речь про процессоры, а не про языки программирования.

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

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


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

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

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

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

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

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

severe(165): Program Exception - integer overflow

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

А zig?

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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


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

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

Спасибо.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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




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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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


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

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

int *p = &x - 1;

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

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

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

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

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

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

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

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

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

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

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

#pragma STDC optimize strict_aliasing off

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


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

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

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

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


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

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

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

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

Спасибо!

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Только вот проблема, что ни один язык не может дать таких же гарантий производительности. Переписал тут недавно небольшую ВМ на rust - замучался выпиливать проверки границ( при помощи того самого страшного unsafe кода ), т.к. с ними было медленее на 10-30% в зависимости от семпла. И даже после этого только приблизился к результату с++

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

Так как, как указать, что код оптимизировать не нужно!?

Зависит от компилятора. Должно быть что-то вида:

#pragma NO_OPTIMIZE_START
...
#pragma NO_OPTIMIZE_END
А не должно зависеть от компилятора, в чём и основной вопрос…

Кому не должно?

История с gcc в статье говорит, что это ничего не гарантирует.

Благодаря Си 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.

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

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


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

Но всеже программы мы пишем для людей, а не для машин. Если программа работает неправильно, то она работает неправильно

> Чем же тогда контракт «я не буду писать UB-код» хуже контракта «я не буду совершать off-by-one errors» или «я не буду допускать логических ошибок при инвалидации кэша»?

Off-by-one практически всегда ловятся в пределах одного экрана (ну, можно сделать, чтобы не поймались так, но это надо постараться). В случае кэша есть гарантированный метод — не идти на всякие lock-free, а ограничиться мьютексами (или вообще обменом сообщениями): дороже, но надёжно. В случае описанных ошибок C защититься без полного контекста (включая всякие библиотечные функции, которые могут обновляться независимо от основного кода) нереально, и вопрос не в производительности — если бы было только в ней, большинство бы использовало безопасный режим по умолчанию (как я и предлагаю в десятке соседних комментариев).

Хорошие системы предполагают, что кроме известных правил вас предупредят о том, что вы близки к тому, чтобы их нарушить. Пора очнуться от ощущения, что разработчик способен уместить в своей голове всю программу — это не так уже лет 20. Это забота компилятора. Собственно, основная претензия к C/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Гц гул" с детства достал... Не ощущаю я в нем той могучей аудиофильской силы. И тем более не интересно в плане цифры. Впрочем, многосеточные лампы это сила. Практически не имеющая себе аналогов. Но опять же - сие тема совсем другого разговора.

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

Ну почему же совсем другого ... 

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

Если убрать предсказание смерти C - получится неплохая статья про UB :)

Спасибо, хотя это скорее крик души, чем предсказание. =)

За использование int в программах на Си - нужно руки/ноги отрывать. А если человек плохо обучается - то ещё и голову.

Что, по-вашему, тут улучшит явное предписание какого-нибудь int32_t?

Везде улучшит. В зависимости от ЦП int может быть размером и 16 и 32 и 64 бита.

Давайте без фантазий о мифических странах. Во всех реальных платформах вокруг меня int == int32_t. Вот я заменил в программе int на int32_t. Каким образом это поможет исчезнуть факту незамеченного переполнения в конкретном месте программы?

Привет от нереальных платформ avr, pic, stm8, rpi4.

> Привет от нереальных платформ avr, pic, stm8, rpi4.

Ни разу не приходилось под такие писать после ~2000 года, потому и указал явно, что «вокруг меня». Но предположим. То есть вы решили на платформах с естественными 16 битами решить проблему расширением разрядности до уровня, поддержка которого уже в разы более громоздкая, потому что длинная арифметика?
А не боитесь, что и этот размер переполнится, только чуть позже?
И не учитываете, что по сравнению с другими проблемами платформы эта конкретная будет минимально заметной?

Ни разу не приходилось под такие писать после ~2000 года, потому и указал явно, что «вокруг меня».

Я уже понял, что ты солипсист.

То есть вы решили на платформах с естественными 16 битами решить проблему расширением разрядности до уровня, поддержка которого уже в разы более громоздкая, потому что длинная арифметика?

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

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

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

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

Скучно.

На практике, конечно, давно придуманы всякие valgrind, санитары, clangd, pvs студии и прочие штуки, которыми надо обмазаться, чтобы написать на с/с++ что-то, что проработает больше дня без падения.

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

То же самое с остальным тулингом вокруг этих языков. В каком-нибудь вскоде официальный плагин для c/c++ довольно ограниченный и заметно хуже того экспириенса, который есть в студии с решарпером. И чтобы этот опыт улучшить надо возиться. В нашей компании ребята попробовали приспособить clangd, но за несколько месяцев НИКТО из плюс-минус сотни разработчиков так и не попробовал. Никому это не интересно, все привыкли страдать и принимать отсутствие IntelliSense как должное. Да, можно пользоваться студией или редакторами от JB, но это в случае если ты пишешь только на с/с++. Мне, например, нравится что в вскоде, как в виме или емаксе можно работать с любым языком.

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

Так что разыменование нулевого указателя это так, мелочи.

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

За все время работы с сишниками у меня сложилось впечатление что им нравится этим заниматься. Сишник/плюсовик лучше потратит несколько лет на рассказы про байки UB, священную борьбу и написание книг/статей на эту тему, вместо того чтобы взять sonar/pvs/etc и прогнать свой говнокод через него. Поговорите с любым отбитым сишником (или почитайте их в твиттере) про раст и сразу все поймете.

Ну не хотят люди жить хорошо, привыкли они так. И это не плохо. Просто так сложилось.

> Сишник/плюсовик лучше потратит несколько лет на рассказы про байки UB, священную борьбу и написание книг/статей на эту тему, вместо того чтобы взять sonar/pvs/etc и прогнать свой говнокод через него. Поговорите с любым отбитым сишником (или почитайте их в твиттере) про раст и сразу все поймете.

Видите ли… сишники/плюсовики подобные средства и так регулярно используют. Проблема в том, что они не дают никакой гарантии — и анализаторы, и санитайзеры ловят, грубо говоря, 99% случаев, а 1% всё равно остаётся. И при мельчайшей смене чего-то в коде их надо напускать заново, потому что от того, что карты разложились чуть иначе, компилятор нашёл новый вариант схалявить.

> Ну не хотят люди жить хорошо, привыкли они так.

Нет «привычки». Есть понимание реальности. Вы же её не понимаете и рассказываете какие-то байки о том, что подсмотрели в замочную щёлку вместо собственного опыта.

> Другая проблема в непортабельности исходников с/с++. Код на питоне, тайпскрипте или расте можно просто взять и запустить/собрать на любой из поддерживаемых платформ и он сразу заработает.

Потому что они непосредственно стыкаются с железом, не? В отличие от прочих названных, где как раз C/C++ обеспечили им независимость и возможность поплёвывать свысока на мучения «холопов»?

Проблема в том, что они не дают никакой гарантии — и анализаторы, и санитайзеры ловят, грубо говоря, 99% случаев, а 1% всё равно остаётся. 

Если человеку нужна 100% гарантия, то он выбрал совершенно не тот язык.

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

Это более чем очевидно. Не думал, что такое нужно объяснять.

Другое дело, что у пользователей современных языков большинство этих проверок выполняется после каждого нажатия на клавишу в редакторе. И начинают работать сразу из коробки после установки одним кликом бесплатного плагина из стора, никогда не разваливаются, не падают и не отжирают 32 ГБ памяти в попытках проиндексировать 100 тысяч строк кода.

Нет «привычки». Есть понимание реальности. Вы же её не понимаете и рассказываете какие-то байки о том, что подсмотрели в замочную щёлку вместо собственного опыта.

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

Потому что они непосредственно стыкаются с железом, не? В отличие от прочих названных, где как раз C/C++ обеспечили им независимость и возможность поплёвывать свысока на мучения «холопов»?

Давайте расскажите как раст не стыкуется с железом)))

Типичная байка, будто на с/с++ только ОС и драйверы пишут.

> Если человеку нужна 100% гарантия, то он выбрал совершенно не тот язык.

А вы думаете, чему посвящён исходный постинг? Мы же тут обсуждаем, что можно было бы сделать, чтобы получить гарантии.

> Это более чем очевидно. Не думал, что такое нужно объяснять.

Ну вот и речь о том, что сделать, чтобы такое перестало быть «очевидным» потому, что перестало быть реальным.

А в расте вам тоже «очевидно», что изменение кода одного модуля может взорвать код другого, безо всякой видимой связи между ними? Тогда зачем тот раст был бы нужен? (заметьте, я не утверждаю)

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

Потому что у них нет таких перекрёстных эффектов? Ну так, в третий раз повторяю, за то и боремся.

> Так расскажите свое понимание, только без перехода на личности.

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

А по сути — уже рассказано, не вижу смысла повторяться: есть анализаторы, есть санитайзеры, есть здравый смысл авторов, и в сумме они… ну почти всё таки покрывают. И вот этот вот недостающий кусочек от «почти» до полного — предмет, в 4-й раз, обсуждения.

> Давайте расскажите как раст не стыкуется с железом)))

Сколько лет C и сколько расту? Сколько кода на C накопилось? И зачем вы мне стали тут приписывать какие-то утверждения, на которые я даже не намекал?

> Типичная байка, будто на с/с++ только ОС и драйверы пишут.

Нет, конечно. А зачем вы это говорите? Говорите тому, кто придумал эту байку (это не я, а упомянули тут её зачем-то вы — сами и придумали?)

На каких холопов поплевывает раст? Гц языки-то ещё ладно, но тут вполне bare metal можно писать.

> На каких холопов поплевывает раст?

Вы про последний абзац? Там про Rust как-то очень вскользь и сомнительно. Утверждать, что на Rust может может сразу скомпилироваться и запуститься то, что на C не завелось, как-то очень странно (вопрос не в отсутствии багов или тому подобном, а именно в переносимости). Это я просто пропустил, извините, но там и так был очень объёмный комментарий. Хотелось бы обоснований и примеров, как такое может получиться, причём не в исключительном случае.

(Наперёд — случаи размеров типов не считаем. И в C есть int${N}_t давно, и случаи, где применяются всякие int, от его размера зависят в редчайших ситуациях.)
НЛО прилетело и опубликовало эту надпись здесь
Вы про последний абзац? Там про Rust как-то очень вскользь и сомнительно. Утверждать, что на Rust может может сразу скомпилироваться и запуститься то, что на C не завелось, как-то очень странно (вопрос не в отсутствии багов или тому подобном, а именно в переносимости).

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


Я сказал лишь то что раст может запускаться на голом железе и не требует никаких холопов для работы. Единственное к чему можно (и иногда это делают) прикопаться это к "вот вы присосались к сишному llvm, ничего сами сделать не можете!!1". Ну тут уже людям объяснять нечего, конечно.

> Я не понимаю откуда тут в комментах люди всегда додумывают так сильно.

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

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

Спасибо, сложил в закладки. Там интересно и что явно сказано, и что не сказано. Например, я в reset_handler не увидел инициализации стека. Вероятно, это обеспечено линкером. Главное, что несколько импортированных символов не потянули за собой половину стандартной библиотеки, как часто бывает в сишных реализациях, не рассчитанных на embedded. Значит, уже есть «achievement unlocked».

> Единственное к чему можно (и иногда это делают) прикопаться это к «вот вы присосались к сишному llvm, ничего сами сделать не можете!!1». Ну тут уже людям объяснять нечего, конечно.

+100. LLVM не сишный, он универсальный, на чём бы ни был написан.
НЛО прилетело и опубликовало эту надпись здесь

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

Тут ещё проблема в том, что pvs даст какие-то гарантии сейчас, а они должны были быть даны 5 лет назад, когда этот код писался. А сейчас уже никто и не вспомнит, как именно надо написать вот эти явно кривые 5 строк кода. И не поломает ли исправление что-то там далеко, в миллионах каталогов от места исправления.

А так - да, когда у нас более "расслабленный" язык, на нём часто легально пишут с провоцированием ошибок. Конкретный пример - функция filter-map в стандартной библиотеке Racket. Она принимает в качестве первого параметра функцию, которая должна выдавать значения разных типов: #f (false : bool), если элемент нужно выбросить, и какой-то 'a, если его нужно отобразить и оставить. Очень удобная эта filter-map, но из-за неё уже нельзя вот так просто брать и требовать у каждой функции тип возвращаемого значения.

И отделять тут козлищ (тех, кто по-ошибке возвращает значения разных типов) от агнцев можно лишь "интуитивно". Это значительно опаснее того же C.

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

Тем, что у вас там внутри скобочек есть тип после стрелочки, а у filter-map - принципиально нету. И ваша функция явно указывает Just/Nothing, а в Racket мы не пишем эти два конструктора.

То есть, выразительные средства языка у Racket такие, что некоторые вещи выглядят двусмысленно по сравнению с Haskell. И ничего тут стат. анализатор Racket не сделает.

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

Некоторые из моих знакомых считают, что статическая типизация убивает креативность. :-) :-) :-)

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

Сишник/плюсовик лучше потратит несколько лет на рассказы про байки UB, священную борьбу и написание книг/статей на эту тему, вместо того чтобы взять sonar/pvs/etc и прогнать свой говнокод через него.

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

Да, можно пользоваться студией или редакторами от JB, но это в случае если ты пишешь только на с/с++. Мне, например, нравится что в вскоде, как в виме или емаксе можно работать с любым языком.

Пишу в Clion от JB. Есть поддержка всего что только нужно. Обычно с C/C++ в проекте есть Python, Bash, Lua, CUDA, Rust, yaml/json/toml/xml/, всякие шейдеры и прочая дичь.

Вероятно Вы мимо JB лишь мимо проходили Ж)

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

Да, конечно, особенно если учесть что под капотом каждой первой либы на питоне все тот же C/C++. Удачи с запуском scipy на любой платформе. Там еще и фортран подтянуть прийдется. Или может LAPACK уже на чистом питоне переписан?

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

1024 Пива?

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

Небольшой демон можно на чем угодно написать, от lua до ассемблера (на хабре точно была пара статей про вебсервера на asm). Если вы позиционируете его как язык для небольших поделок — это одна история, если как язык для серьезных проектов на много лет и много программистов — совершенно другая.
НЛО прилетело и опубликовало эту надпись здесь

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

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

чем каждый день пользуются

Ничем не пользуюсь. Не нужен этот ваш Эксель, позвоню в Майкрософт, чтоб выпиливали.

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

Собрать код на питоне или тайпскрипте? Раст у тебя везде заработает?

на любой из поддерживаемых платформ
Раст у тебя везде заработает?
Как ни странно — да. Конечно, у конкретный крейтов могут зависимости быть от библиотек, которые отсутствуют на какой-то платформе, но это проблема крейтов, а не языка/компилятора; точно так же как если я буду использовать /dev/random в качестве источника случайных значений, то на Windows ну никак не будет работать.

на любой из поддерживаемых платформ

Значит если интерпретатора питона нет на платформе или твой код использует сишную библиотеку, распространяемую в прекомпилированных бинарниках, то не заработает?

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

Так и код на Си везде заработает, если доступны нужные библиотеки и API.

Значит если интерпретатора питона нет на платформе или твой код использует сишную библиотеку, распространяемую в прекомпилированных бинарниках, то не заработает?
Именно так. Как и со всеми другими скриптовыми языками: нет интерпретатора — нет интерпретации.
Так и код на Си везде заработает, если доступны нужные библиотеки и API.
Так и на C код не заработает, если нужных библиотек/API нет, в чём тут недостаток Rust? Пишете код на «чистом» C/Rust — перенесётся без проблем, пишете с библиотеками — уже от них будет зависеть ваша переносимость.
При этом зависимость не только от сторонних библиотек, но и от std. Банальный пример: на всяких микроконтроллерах нет stdio.h, зато есть прямой доступ к регистрам процессора (без ассембреных вставок, просто по адресам), но вы же не скажете, что C — непереносимый язык?

Так и на C код не заработает, если нужных библиотек/API нет, в чём тут недостаток Rust?

Изначальная претензия была к Си.

Банальный пример: на всяких микроконтроллерах нет stdio.h

Вообще-то есть. И переопределением, например, putc() можно использовать printf для вывода в последовательный порт или любой другой интерфейс.

зато есть прямой доступ к регистрам процессора

Указатели на абсолютные адреса использовать можно ("регистры" периферии), именованные регистры общего назначения без ассемблера - нельзя.

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

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

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

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

А Vax assembler ещё лучше))

Нельзя так говорить! Каждый ассемблер еще лучше. ?

разверну вашу мысль, коллега. Если отбросить троллинг, то упомянутый уже здесь Крис Касперски писал что-то вроде «тяжело забивать гвозди, когда молоток непрерывно изменяется в руке».
Когда-то в начале века я только заработал на первый компьютер, опыта не было, и я смог найти работу только программистом в embedded на ассемблерах. Так получилось, что я был предоставлен сам себе и написал на нём все базовое с нуля: многоразрядную арифметику, копирование блоков памяти и все такое. Оказалось что это очень много кода. Потом я уже работал над большими чужими коммерческими проектами и научился писать на макросах, как писали большие пацаны древности (ассемблер превращается во что-то высокоуровневое — фортранообразное).

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

Конечно, ассемблер это зло, но это то к чему нужно стремиться при разработке инструментов — инструмент не изменяется в руке. Всё однозначно понятно. Порог входа (для технарей) тоже очень низкий. То есть у новичка код будет плохой, но он будет работать.

На ассемблерах я писал годами и когда перешел на Си я в голове постоянно прокручивал как эти конструкции выглядят на ассемблере и что происходит в процессоре. И должен признать что испытывал все те же новичковые сложности с синтаксисом ссылок-указателей. Осознал всю вселенную неоднозначностей что скрывает компилятор и стандарт. Хотя казалось бы — почему? Ведь я реально знал контроллеры наизусть до последнего бита-флага, систему команд ассемблеров тоже (8051, AVR).

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

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


Я долгое время сидел на ruSO и отвечал на глупые вопросы начинающих, и могу сказать что там почти нет вопросов относящихся именно к пониманию языка. Большинство проблем растёт либо из непонимания алгоритмов, либо вовсе из неспособности провести декомпозицию задачи. Ещё есть проблемы непонимания некоторых библиотек, но это снова проблемы библиотек, а не языков.

Если отбросить троллинг,

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


Конечно, ассемблер это зло,

Это не факт и совершенно не следует из вашего поста.


На ассемблерах я писал годами и когда перешел на Си я в голове постоянно прокручивал как эти конструкции выглядят на ассемблере и что происходит в процессоре.

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

Если вы разрабатываете драйвер для железки или пишете прошивку микроконтроллера — да, вероятно ассемблер будет лучшим выбором, по крайней мере в некоторых блоках кода.
Но у языков более высокого уровня (С и далее) есть два ключевых преимущества: многое сделано и протестировано за/до вас, а также многие операции, которые делают одно и то же, имеют один и тот же вид.
Пример: вот три цикла.
for (char i = 0; i < 10; i++)
    do_something_char(i);

for (unsigned long long i = 0; i < 1000000000000000; i++)
    do_something_ull(i);

for (float i = 0.0; i < 10; i += 0.01)
    do_something_float(i);

Синтаксис у них идентичный, назначение — тоже: вызвать функцию много раз, передав ей текущее значение цикла, а всё отличие — в типе данных. Эти сишные циклы выглядят одинаково и на x86_64, и на ARM64, и даже на AVR. Внимание, вопрос: зачем писать одно и то же 9ю (3 цикла на 3 архитектурах) разными наборами инструкций на ассемблере, когда есть ровно одна работающая конструкция на C?
Повторюсь: я не говорю про низкоуровыневые коды, я про общий случай (судя по вашим предыдущим комментариям, вы «топите» за ассемблер для всего).
Внимание, вопрос: зачем писать одно и то же 9ю (3 цикла на 3 архитектурах) разными наборами инструкций на ассемблере, когда есть ровно одна работающая конструкция на C?

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

Если вы пишете код только под одну платформу, и точно знаете, что этот код никогда не будет переноситься на другую — да, конечно, ассемблер имеет намного больше смысла…
  • … особенно если нужна критическая производительность
  • … особенно если ваш код никто кроме вас и других товарищей по ассемблеру не читает (ассемблер сильно менее понятен, чем C, а ведь есть и гораздо более выразительные языки)
  • … особенно если вы на 100% уверены, что напишете код не менее производительный и/или компактный, чем современные оптимизирующие компиляторы (а это ой как не факт)

Нет, я не призываю вас отказываться от ассемблера для решения ваших задач, но заявление «ассемблер лучше С» (без специфики, без конкретных задач) слишком смелое.
ассемблер сильно менее понятен, чем C, а ведь есть и гораздо более выразительные языки

Это совершенно не так. Просто ассемблер мало знают и мало пишут на нем. Для меня например Лисп совершенно не понятен. Но это не значит что Лисп менее понятен в принципе. Он только мне непонятен.

Ассемблеров от производителей, например, разных контроллеров много (AVR, MSP430, ARM, PIC, RISC-V, 8051, STM8, Z80, M6800, DSP...) с разной системой команд и способами в целом их использования.
Как возможно безболезненно перенести код программы с одного контроллера в другой используя ассемблер?
(при том ещё и аппаратная составляющая у разных контроллеров предполагает определённые шаги по работе с ней)
Как возможно безболезненно перенести код программы с одного контроллера в другой используя ассемблер?

А кто сказал что должно быть "безболезненно"? Совершенство постигается через боль. Как говорят, через тернии к звездам!


Безболезненно они хотят!


П.С. А еще говорят – глаза боятся, а руки делают. Часто этот процесс намного легче, чем кажется.

Один из лучших «ассемблеров» — это Forth (Форт). ?
У него, даже, и встроенный ассемблер может быть на свой лад.
Для современного железа затраты на call/ret чудовищны, так что для «гражданки» Forth уже не выйдет за нишу клея для загрузчиков и тому подобного (если без перерабатывающего супероптимизирующего компилятора, после которого дух языка, считаем, подменён чем-то другим).

А вот для военного или космического применения Forth вполне вкусен — гарантированный не исправленный никакими подозрительными оптимизациями выхлоп, в сочетании с аналогичными мерами на уровне процессора (например, никаких кэшей памяти), и средствами защиты от сбоев, наверняка будет обязателен, чтобы безопасно лететь хотя бы к Марсу…
Чужие: странная архитектура инопланетных компьютеров (c RTX2010).

А, вот для «гражданки», вообще нет контроллеров с MISC построением,
если не считать экзотичный GA144 на который, кстати, делали
и проект «Си» компилятора — Chlorophyll Language and Compiler. ?

Статья из журнала «Компоненты и Технологии»:
Процессоры GreenArrays — GA144
Хабр статья: GA144: русские спецификации процессоров

P.S. Хотя в кремнии и в России есть сделанные «MISC» кристаллы К1894,
В Минском Интеграле — K1881BE1T (IN16C)
НЛО прилетело и опубликовало эту надпись здесь
А вот в Китае такой пост бы зацензурили!
*простите, не сдержался :)
Думал шутки про Китай будут прямо в первых комментариях.
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;
}

А можно объяснить, почему это вызывает вопросы?

x где-то лежит в памяти
y где-то так-же лежит
указатель p = адрес памяти x + размерность x
указатель q = адрес памяти y
почему p должен быть равным y?

С чего указатели, даже содержащие одни и теже данные, должны указывать на одно и тоже место? В общем случае они всегда не будут равны. Хочешь чтобы были равны — породи их от одного адреса.

Для меня указатель — это просто адрес и размерность — и ничего больше, почему из сравнивать нельзя? Даже если этого адреса не существует? Видимо, я давно стандарты не читал )))

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

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

Просто вы не понимаете что такое указатель.

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

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

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

Программисты на С/С++ ошибочно полагают, что эти языки — низкоуровневые, хотя они ими не являются. https://queue.acm.org/detail.cfm?id=3212479

This is a fairly trivial example, yet a significant proportion of programmers either believe the wrong thing or are not sure. When you introduce pointers, the semantics of C become a lot more confusing. The BCPL model was fairly simple: values are words. Each word is either some data or the address of some data. Memory is a flat array of storage cells indexed by address.

The C model, in contrast, was intended to allow implementation on a variety of targets, including segmented architectures (where a pointer might be a segment ID and an offset) and even garbage-collected virtual machines. The C specification is careful to restrict valid operations on pointers to avoid problems for such systems. The response to Defect Report 2601 included the notion of pointer provenance in the definition of pointer:

"Implementations are permitted to track the origins of a bit pattern and treat those representing an indeterminate value as distinct from those representing a determined value. They may also treat pointers based on different origins as distinct even though they are bitwise identical."

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

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

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

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

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

Аналогично, человек, кладущий в память два int-значения, и вытаскивающий обратно float, зачем-то это делает. Вероятно, у него есть на то хорошая причина, но нельзя же не понимать, что ты суёшься ну в очень уж тёмный угол, в котором законы зыбки. В этом отношении юзер felix-gcc из диалога ни разу не прав: у него есть легальный способ получить нужное ему поведение, но он из принципа делать этого не хочет. Он хочет, чтобы его Ариан упал, а виноваты были разработчики компилятора.

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

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

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

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

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

Теоретически этот аргумент понятен, но решительно неясно, что это означает на практике. Есть алгоритм, который работает известным образом на указанном наборе входных данных. За пределами этого набора он по сути не тестировался и работает бог весь как -- в зависимости от оборудования и фазы Луны.

Felix-gcc прикопался к конкретному виду UB, но на практике ведь UB везде, начиная с хрестоматийных f(++x, ++x), и я не удивлюсь, если даже сами авторы компилятора не знают, как именно выполняется f(++x, ++x) на текущей версии, ибо зачем им это знать? То есть в мире felix-gcc мы должны сначала прогнать миллион тестов на миллионе видов UB, потом закрепить это всё в тысячестраничном документе и следить, чтобы не дай бог новая версия компилятора ничего не сломала.

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

Так проблема в том, что непонятно, с чем совмещать. Юзер пишет, и говорит, что в ревизии x.5 работало так, а в x.6 работает эдак. Мы чешем репу и выясняем, что действительно так, хотя мы даже об этом не подозревали. Окей, поправили. А потом находится другой юзер, который сообщает, что в ревизии x.4 было вообще иначе, и что теперь делать?

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

> Felix-gcc прикопался к конкретному виду UB, но на практике ведь UB везде, начиная с хрестоматийных f(++x, ++x), и я не удивлюсь, если даже сами авторы компилятора не знают, как именно выполняется f(++x, ++x) на текущей версии, ибо зачем им это знать?

Не так:

1. Это не undefined, это unspecified behavior. Разница принципиальная в том, что если UdB позволяет формально вообще всю программу выкинуть и заменить на показ котика, то UsB даёт определённое поведение везде, кроме проблемного места.

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

> То есть в мире felix-gcc мы должны сначала прогнать миллион тестов на миллионе видов UB, потом закрепить это всё в тысячестраничном документе и следить, чтобы не дай бог новая версия компилятора ничего не сломала.

В моей модификации этого мира есть контекстная установка «выполнять все вычисления параметров функций слева направо», которая действует по умолчанию, но, если автор кода уверен, он её отменяет для конкретного блока (исходного файла). Далее сначала пишется корректный код, а затем после его проверки — производится разрешение оптимизаций.

Это не undefined, это unspecified behavior. 

Да, согласен. Но я думаю что в мире felix-gcc разницы нет: если я запустил свой код, и он выдал на выходе, скажем, нуль, то теперь нуль должен быть всегда, при любых обновлениях компилятора, и неважно, UB там или UC.

Какой-то прямо закон Хирама в действии!

Почему нельзя вернуться к Паскалю, доработав его?
1. Исправить очевидные глупости, на которые жаловался Керниган в своей знаменитой статье (Собственно, может быть их уже и выкинули в новых версиях Free Pascal и Delphi/Lazarus, я давно ими не занимался.)
2. Вставить несколько необходимых вещей типа long jumps, которых, собственно, должно быть не так уж и много
3. Чтобы была как минимум опция начинать индексы массивов с 0, как все люди делают.
И вперед, можно будет писать новую операционку.
После Python и даже Java понимаешь, что С это мазохизм. Я понимаю, что раньше время было такое (послевоенная разруха, пережитки сталинизма :-)), но в 21 веке пора жить по принципу "explicit is better than implicit".

Выше уже написали, что уже есть паскалеподобный язык ADA, который был создан на 10 лет позже С на основе задания министерства обороны США для безопасной разработки, а также созданный на его основе язык Spark программы на котором если скомпилировались, то с большой долей вероятности они не буду содержать ошибок вообще.

Минусы этих языков - многословны (как и все паскалеподобные), серьезные инструменты и поддержка - платны, немного медленоватее С/C++ - на тестах , в связи с внутренними проверками ошибок, чуть быстрее java, но в отличие от java не жрет столько памяти и не имеет задержек сборки мусора.

Языки применяются в критических к ошибкам областях: министерстве обороны США, авиастроении, астронавтике, разработке медицинских приборов, электронике.

  1. Многословны (как и все паскалеподобные) - потому что "explicit is better than implicit". Но я согласен, что в Аде с многословностью увлеклись и переборщили. Чем плох Free Pascal? Если не считать отсутствия long jumps и т.д.?

  2. Серьезные инструменты и поддержка платны - потому что ими мало пользуются, в том числе в мире OpenSource. Больше бы использовались - было бы больше и бесплатных. Опять таки - чем несерьезен Free Pascal?

  3. Немного медленоватее С/C++ - вот именно немного ! Когда-то ядро Windows писали на ANSI C, потому что C++ немного медленоватее. Но ведь уже давно не пишут!

Доработанный Паскаль - это Java.

Есть в этом нечто, но Java как таковая для ядра ОС непригодна по ряду причин. Например, классы уместны не всегда и не везде. Такая вещь как StringBuffer, не может не быть медленной по сути своей. И все, где вместо явного освобождения памяти - сборка мусора, не может не быть медленным. Есть еще причины. А если это в Java изменить, то получится обратно Pascal.

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

В статье много лишних эмоций, а по технической части она, честно говоря, довольно банальная. Подобное писали и 10 лет назад, наверняка писали и 20 лет назад (просто лень искать), и, что самое забавное, продолжат писать и через 10 лет, да, возможно, и через 20 лет тоже. Я такие называю "за все хорошее и против всего плохого" или "статья ради срача".

> Подобное писали и 10 лет назад, наверняка писали и 20 лет назад (просто лень искать)

Вот как раз и хочется, чтобы не нужно было дальше такие статьи писать, а вместо этого можно было писать хотя бы «вставьте вот эти 3 строки в начало каждого исходника, и ваши зубы всегда будут белые и пушистые». А ещё лет через 5 — «перестаньте писать эти 3 строки, уже не нужно».

Смерть "C" неизбежна как крах капитализма! Победа коммунизма неизбежна! Даешь Раст на всех платформах включая 8bit AVR! . Rust on Bare metal !!!. Старичков (Торвальд и компания) на пенсию. Офигеваю я от студентов...

Мы вроде такого не писали. =)

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

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

Это потому что сначала компилятор Си писали его пользователи, а потом абстракционисты. Но язык в этом не виноват

„Знаешь загадку про верблюда? Что такое верблюд? Это лошадь, проект которой составлял творческий коллектив.“ — Том Уэйтс

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

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


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

Молодежь нормальная, такой она и должна быть. Тема просто избитая и исхоженная как "смысл жизни" .

Может в плане понимания «лучше», больше будут востребованы и языки конкатенативной направленности как, к примеру, язык Factor.
НЛО прилетело и опубликовало эту надпись здесь

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

В целом с Си не все так плохо. Пишу под микроконтроллеры. Особенных проблем не возникает, тем более что есть масса линтеров.

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

Неплохо для чего? Зачем такой язык может пригодиться? Переписать на нём ядро Линукс со всеми драйверами? Или coreutils? Или user32.dll на нём перепишут? Кто им будет пользоваться и зачем? Работники каких областей создадут вокруг него экосистему?

Что-то мне кажется, что все беды тут от близкого родства с C++. В старые времена, когда деревья были большими, компилятор, встретив кусок кода типа a << sizeof(int)*8 должен был сгенерировать соответствующее число команд сдвига влево, принятых в данной архитектуре. Предполагалось, что программист который это написал — знает что он делает, и зачем.

Однако, когда завезли C++ сначала как «C with classes» по Страуструпу, добавили темплейты, а потом ударились в template meta-programming — оказалось что эта хрень генерирует просто тонны dead code! И шансов на выживание у языка в таком виде нет, несмотря на закон Мура (который в те времена все еще действовал). И вот именно тогда разработчики компиляторов (не будем показывать пальцем на gcc/g++ как на наиболее известную свободную реализацию...) ударились в dead code elimitation — и пошло-поехало… А поскольку поддерживать две совсем разных ветки для C и для C++ дорого — все эти новые веяния пришли и в C. Что дало компилятору совершенно неприличные возможности для интерпретации канонического кода на языке высокого уровня.

С моей точки зрения — в идеальном мире с пони и единорогами, в случае UB компилятор обязан сгенерировать команды, которые максимально точно передадут написанное в исходнике в коды target-платформы. Даже если на некоторых (или даже на всех) платформах из этого получится идиотизм. И да, проблемы этого подхода мне понятны — но я как минимум разрешил бы любые варианты dead code elimination ТОЛЬКО по указанию соответствующих ключей при вызове компилятора. А не наоборот. Хотя и reordering в сочетании с мультипроцессорными архитектурами, out-of-order execution и прочими штуками современных вычислительных систем может давать неожиданные эффекты.

А пока программисту на C/C++ следует просто знать ассемблер. И если не в первую, так во вторую очередь смотреть, что компилятор вам сгенерировал… Ну или вперед на Java. Там, благо среда исполнения тоже более-менее отвязана от аппаратуры, и оно если скомпилировалось, то так же и работать будет.

Я до такой степени согласен, что я бы оптимизации компилятора в С вообще запретил. В крйнем случае, разрешил бы печатать warnings. Если ты ожидаешь от компилятора оптимизации, значит тебе нужен не С, а Java или С#.
Что же касается С++, то вот его я бы убил точно. Ну кому он сейчас нужен? Как замена ассемблера он не годится, а как "С с классами" есть Java и С#, которые и лучше, и проще.

я бы оптимизации компилятора в С вообще запретил

Как хорошо что это не от Вас зависит. Вызывайте с -O0.

Что же касается С++, то вот его я бы убил точно

C++ закуклился и спрятал плюсы под совместимость с "C".

Ответ кроется в тексте стандарта. Чтобы всё-таки дать теоретическую возможность программистам писать низкоуровневые процедуры, а значит непереносимые, было введено ещё одно понятие — неопределённое поведение (undefined behavior, раздел 1.6, "DEFINITIONS OF TERMS"):

...


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

Божественно

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

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

Э-э-э… А что это за компилятор такой?

Мне кажется, это не оптимизация, а чистой воды вредительство. Что значит: «Использовать memset не хочешь ты! yoda.jpg»? Что значит «выкинуть вызов»? Если такая оптимизация входит в какой-нибудь стандартный пакет O1 или O2, пусть такой компилятор лучше выкинет себя на помойку.

Прикол будет, если мне сейчас расскажут, что так ведёт себя, например, VS и «всё, что жил — всё зря».

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

ruomserg Не совсем понял его/вашу мысль. Как связана оптимизация шаблонного шлака с выкидыванием вручную прописанного вызова memset? Компилятор разве не может различить эти две ситуации?

https://godbolt.org/z/n84ej87xP

Вы видите там вызовы memset? Просто нужно знать язык, на котором пишешь и знать, что memset не даёт гарантий безопасного затирания содержимого памяти

Как это связано с шаблонами, которые, якобы, спровоцировали такую агрессивную оптимизацию?

Что касается «memset не даёт гарантий» — да, теперь буду знать, спасибо.

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

a = 5;
a = 15;
a = 7;

И теперь по вашей логике нельзя выбросить первые две строки? Ведь программист написал, значит, знал, что делает.

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

И мне кажется, что для «С» — это правильная модель. Идеология «С» в 80-е годы была именно что «высокоуровневого ассемблера». Предполагалось, что программист достаточно хорошо понимает — что и для чего он пишет — а дело компилятора, просто перевести это в команды процессора. И оно примерно так и работало — и все были более-менее довольны. Проблемы начались в тот момент, когда началась автоматическая кодогенерация. Поскольку это не живой человек — оно свой сгенерированный код оптимизировать вдумчиво не собирается. А нужно было после сборки чтобы оно хоть как-то шевелилось. Ну и всем показалось, что компилятор — самое подходящее средство чтобы им лечиться. Благо тактовые частоты подросли, теория компиляторов подтянулась — и фактически компилятор начал переводить в команды не тот код, который написал программист, а другой код — относительно которого он (компилятор) может доказать его (другого кода) эквивалентность исходному относительно некоторого абстрактного вычислителя.

Для C++, собственно, выхода не было — либо надо выбрасывать темплейтное мета-программирование (и идею SFINAE — инстанциировать тучу специализаций, и из них выбирать наиболее подходящую), либо соглашаться что компилятор будет компилировать не написанный человеком код, а эквивалентный ему над абстрактным вычислителем. Для языка «C», с моей личной точки зрения, это является большой ошибкой. В «C» нет встроенных механизмов генерации больших объемов dead code. Поэтому компилятор должен компилировать в ассемблер ровно то, что написано — возможно применяя некоторые локальные оптимизации, если таковые разрешены флагами при сборке.
НЛО прилетело и опубликовало эту надпись здесь
Я не буду спорить, потому что давно не смотрел во внутренности компиляторов современных. В моем представлении, когда они начинают процесс инстанциирования шаблонов — это может порождать процесс инстанциирования других шаблонов — и они генерируют целый лес в синтаксическом дереве (если вообще используется это представление...). И дальше два варианта — либо если где-то инстанциирование наткнулось на непреодлимую проблему, то откатывать обратно дерево и начинать сначала, либо оставлять куски в надежде что они скоро понадобятся снова. Я подозреваю (это мое ничем не подкрепленное спекулятивное суждение), что для увеличения скорости компиляции — они оставляют в дереве как можно больше того что сумело инстанциироваться, и полагаются на оптимизатор, который вычистит то, что в конце-концов не пригодилось…

Мне кажется, в некотором смысле это игра в терминологию, потому что выше уже в комментариях приводили ссылку на статью "C is not a low-level language" с подзаголовком "Your computer is not a fast PDP-11". Нюанс в том, что ассемблер вашего компьютера совершенно не совпадает к логикой конструкций языка C, в котором, например, существуют отдельные операции A++ и ++A, имеющие смысл именно в контексте того ассемблера. Даже какое-нибудь тривиальное действие вида int a = 5; in b = 15; можно (и нужно?) выполнять тем способом, который можно считать родным для компьютера сейчас, а не в семидесятых годах. И даже в семидесятых компилятор понимал, что если в формуле с делением выгоднее сперва посчитать знаменатель, это и надо сделать. (Что-то помню подобное, но неточно). А отсюда уже один шаг до dead code elimination и прочих вещей, которые мы так любим. Я бы сказал, в текущей статье ещё очень малоагрессивные виды оптимизаций обсуждают.

Ну… я ж не отрицаю свободу компилятора в выборе конкретного набора инструкций, в который нужно пространслировать конкретную строку кода. Однако, в рамках языка «C», если в строке программист что-то написал — обязанностью компилятора являтся сгенерировать команды, которые максимально выражают написанное в машинных кодах. Понятно, что на одной платформе a++ будет атомарным, а на другой, например, нет. Это уже другое — и оно может решаться библиотеками или intrinsic-ами, или варварским asm {} в обрамлении ifdef-ов. Я не вижу причины, по которой в стандарте «C» (в отличие от C++ — там необходимость понятна...) разрешили компилятору генерировать код для программы не той, которую написал программист — а той, которая ей эквивалентна над определенным в стандарте абстрактным вычислителем. В целом, я бы убрал из стандарта понятие «UB» и потребовал чтобы компилятор во всех этих случаях либо выдавал синтаксическую ошибку, либо генерировал команды, выполняющие ровно те действия, которые написаны в исходном файле. И да, на разных платформах эти действия могут приводить к разным результатам.

Возьмем, например, правило pointer aliasing. Мы можем предположить существование вычислительной системы с неоднородной памятью, где память для int-ов и память для float-ов — это две разные памяти (например, адресуемые разными группами регистров). Если принять, что указатель — это смещение относительно начала каждого вида памяти, то понятно что операция копирования значения int * в float * (и наоборот) — не имеет смысла. Они физически не могут указать на одну область памяти. Но это не значит, что компилятор может выкинуть такие присвоения. Он должен либо упасть с синтаксической ошибкой (если способен такое обнаружить) для данной платформы, либо все-таки перенести значение из одного адресного регистра в другой несмотря на то, что семантика «пусть float * указывает на ту же ячейку памяти куда указывал int *» поменялась на «пусть float * указывает на ячейку в банке float-памяти с таким же смещением как была у int *». Скорее всего, эта операция не будет иметь смысла для данной платформы. Возможно, при таком переносе будет происходить усечение разрядности регистра и что угодно еще. Но это будет описано не в стандарте языка, а в спецификации данной вычислительной платформы. И программист, который под нее пишет — будет знать что тут действительно нельзя менятся значениями указателей между int и float. И это — нормально. Но компилятор не должен выкидывать операции на одном только основании, что ему кажется, что эти операции не имеют смысла или ни на что не влияют… По крайней мере, компилятор «C». В этом случае он останется надолго востребован и актуален…
> если в строке программист что-то написал — обязанностью компилятора являтся сгенерировать команды, которые максимально выражают написанное в машинных кодах.

Вопрос в границах этого действия.

Вот… что бы такого совсем простого:)) Решаем квадратное уравнение.
Дискриминант: d = b*b — 4*a*c;
Должен ли этот код реально читать b дважды из памяти (где он там лежит), или таки он не тварь дрожащая, и может сохранить его в регистре?
Умножать на 4 должно быть сделано с конверсией 4 из целого в плавучее каждый раз, или может сразу умножить на 4.0, или применить какой-то FSCALE на 2 двоичных порядка (может быть ещё быстрее)?
x1 = (-b+sqrt(d))/(2*a);
x2 = (-b-sqrt(d))/(2*a);
Надо ли sqrt(d) считать дважды, если мы точно знаем, что sqrt() — чистая функция (то есть не зависит от любых внешних данных)?

z = a*b + b*c — 2*c*a;
и снова, читать переменные каждый раз или через раз?

for (int i = 0; i < 3; i++) {
  v[i] *= x;
}


то же самое.

Пусть вы решили перевалить вопрос на программиста — что нужно, он пометит register.
Теперь: у процессора в варианте A — 4 относительно свободных регистра для этого, в варианте B — 7 регистров. Должен ли программист о всём этом заботиться?

double *a, *b, *c;
for (int i = 0; i < 3; ++i) {
  a[i] = b[i] + c[i];
}


могу ли я прочитать сначала все b, потом все c, сложить и положить в a (это реально векторная операция, можно сэкономить на процессоре), или должен делать всё попунктно по каждому i (это уже вопрос к алиасингу, если мы не знаем, не пересекаются ли области a с b или c)?

> Возьмем, например, правило pointer aliasing.

> Но компилятор не должен выкидывать операции на одном только основании, что ему кажется, что эти операции не имеют смысла или ни на что не влияют…

А где и как провести границу?

Вот мы перемножаем матрицы:

for (int row = 1; row <= N; ++row) {
  for (int col = 1; col <= N; ++col) {
    double sum = 0;
    for (int xi = 1; xi <= N; ++xi) {
      sum += A[row][xi] * B[xi][col];
    }
    sum[row][col] = sum;
  }
}


Имеет ли право компилятор оптимизировать все вычисления адресов, типа, строки A[row] которая на уровне байтов будет &A[1][1]+(row-1)*N*sizeof(double)? А смещение от этой позиции, которое равно (xi-1)*sizeof(double)? А само вычитание 1, если индексация от 1 (может, недействительно для C, но есть языки, для которых важно)?
Эти вопросы встали ещё в конце 1950-х, когда стали делать первые компиляторы.




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

А вот насколько их разрешать — вот то, что сейчас решается компиляторозависимыми опциями.

Но поймите, что пока во всё это не влезли и не начали писать свой компилятор, 99% этих проблем не чувствуется даже опытными программистами.
Я боюсь, что мы сейчас начнем придумывать новый стандарт «C». :-) Но смотрите — моя идея примерно следующая:

1. Арифметические оптимизации разрешены — потому что арифметика является как раз подмножеством «хорошо определенного» вычислителя. Все существующие UB там переводятся в target-specific behavior. То есть компилятор генерирует команды для target-платформы, а будет ли при этом в рантайме аппаратное прерывание, потеря точности, или процессор зависнет — его не волнует.

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

3. Константные выражения — вероятно как в случае с арифметикой, по-умолчанию можно.

В целом, я бы оптимизации начиная с reordering внутри блока начал ограждать красными флажками в виде опций. А dead code elimination — уже даже не флажками, а светящимися шарами пару метров диаметром (как провода ЛЭП в авиации :-)
> Все существующие UB там переводятся в target-specific behavior. То есть компилятор генерирует команды для target-платформы, а будет ли при этом в рантайме аппаратное прерывание, потеря точности, или процессор зависнет — его не волнует.

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

В случае ассемблера я знаю особенности конкретной платформы и выбираю реализацию под неё. Если это что-то с NZVC — я буду проверять флаг V (OF) при подозрении, что что-то не так пойдёт и не поймано раньше. Если что-то совсем без флагов, как RISC-V или MIPS — для него есть свои достаточно простые средства — писал рядом, но это ещё проще описать так: `(b<0) != (sum<a)` (если sum уже подсчитано с усечением). Если System/Z — переход по CC=3 после знакового сложения. Если мне нужно сложение с усечением — я просто вызову команду без детекта переполнения. Главное, на этом этапе я всем полностью управляю.

И такого же управления я хочу на уровне языка повыше. Нужна операция с усечением — вызываю её (вокруг кроме машин с дополнительным кодом сейчас ничего нет, поэтому пойдёт). Нужна с проверкой — вызываю и её. Как минимум — явно вызываю, это уже первый уровень возможности. (Его GCC с компанией наконец-то начали обеспечивать, хоть и громоздко.) Но дальше хочется второго уровня — чтобы это было удобно. Чтобы, например, сделать

int seen_ovf = 0;
[[int_arith(checked_with_flag(seen_ovf))]] {
  ... рабочий код со стандартной записью операций ...
}
if (seen_ovf) { error("всё было зря"); }


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

А если ещё и можно будет описать всякими директивами вместо тупых int32_t что-то типа integer range(0..200000) и компилятор поэтому выкинет большинство проверок как ненужные — ещё лучше.

> Вынос инварианта за цикл — только по отдельной опции.

В большинстве случаев он не влияет. Влияет тогда, когда начинаются проблемы алиасинга. Но, да, запретить (переносимо) такие оптимизации… ну я бы таки попросил. Хотя бы как контрольную меру.

Управление оптимизацией на уровне всего проекта или даже одного исходного файла, которое было нормой в первые десятилетия компиляторостроения, сейчас стало неадекватным. Можно строить зависимости от режима сборки, но управление по функциям и блокам становится критически важным для того, чтобы не впадать в режим «включили -O2 и всё сломалось в пятнадцати местах, и интерференция ошибок такая, что мы нафиг не понимаем, как вообще начинать искать». GCC (>=5) сделал прагмы режима раздельно по функциям, но это 1/20 от желательного.

> А dead code elimination — уже даже не флажками, а светящимися шарами пару метров диаметром (как провода ЛЭП в авиации :-)

Так полезная же штука. Иногда (особенно при раскрытии макр и шаблонов) превращается в экономию на порядки.
Так что с этим я бы не очень спешил. А вот пометить расслабление именно на типовые проблемы типа разыменования вероятно пустого указателя — полезно.
(Кстати, не везде можно исключить конкретное значение указателя, чтобы это был NULL, и не везде 0 надо запрещать. Но это, наоборот, имеет смысл явно контекстно включать для тех небольших блоков особо системного кода, которые таки полезут по адресу 0. Привет от Хоара.)

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

НЛО прилетело и опубликовало эту надпись здесь
Ну так я и не требую от компилятора детектировать UB во всех случаях. Если компилятор не может детектировать UB — он тупо генерирует код, выполняющий то, что написано в файле. То есть если написано int x=0;float *p=&x — то берется, мать его, адрес «x» (чем бы это значение не выражалось в целевой платформе), и записывается бит-за-битом туда, где выделено место под указатель «p». Имеет ли это смысл на целевой платформе, будет ли в этом месте ошибка времени исполнения, зависнет ли после этого процессор — компилятор волновать не должно. Разработчик написал сделать — компилятор перевел. В «С» изначально предполагалось, что разработчик знает, что он делает когда пишет код. В отличие, например, от паскаля…

Насколько я понимаю там 50 штук разных проходов, в которых теряется информация о том, что же изначально было написано. А проходы эти в таком кол-ве потому, что С не является языком низкого уровня для современных десктопов - https://queue.acm.org/detail.cfm?id=3212479

А ещё есть прекрасная статья по-поводу UB https://www.complang.tuwien.ac.at/kps2015/proceedings/KPS_2015_submission_29.pdf

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

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

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

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

Если в memset передаётся адрес, который потом не читается, то конечно этот вызов будет соптимизирован.

В Си это невозможно предсказать.

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

У volatile свободная реализация, он ничего не гарантирует.

А если включена IPO, то можно и за пределами одной функции.

Нет, если у тебя не выделяется/очищается память при этом через malloc/free. Указателю ведь можно задать абсолютный адрес, который можно использовать за пределами функции.

Я же сказал "пределы функции, где выделен". Про какой абсолютный указатель речь?

Т.е. говорил про ситуацию

void foo() {
  int* ptr = malloc(42);
	// code using ptr
  memset(ptr, 0, 42);
  // more code not using ptr
  free(ptr);
}

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

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

Но мне это всё равно кажется глубоко неправильным.

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

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

Комментарий к тому, что даже при нормальном завершении есть риск утечки данных

Я бы даже сказал, что он есть всегда. Но все равно, какая-то несвязная беседа получилась… ,)

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

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

Во первых я могу просканировать память рутом. Или отследить странички процесса перед выходом. Рутом или родительским процессом. Программа то моя , а не чужая, все обращения к ядру можно перехватить отладчиком.И даже если и чужая. Жидкий азот не понадобится.

И как это поможет выявить несогласованность кеша и физической памяти?
P.S. На всякий случай. Минусую ваши комментарии не я, мне правда интересно.

Элементарно. Пассворд останется в памяти. Ядро освободит страничку без коммита кеша при освобождении ресурсов процесса на его завершении

> Ядро освободит страничку без коммита кеша

В типовых современных архитектурах ядро просто не может управлять этим. Кэш процессора сбрасывается сам по себе, или можно ускорить этот сброс, но не отменить его.
Я не уверен, что так можно (отменить сброс кеша в память).
(я всегда буду обновлять комментарии)

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

Можете привести инструкцию какого-нибудь процессора, при помощи которой ядро может отменить запись процессором в write-back память из кэша?

INVD — Invalidate Internal Caches

Invalidates (flushes) the processor’s internal caches and issues a special-function bus cycle that directs external caches to also flush themselves. Data held in internal caches is not written back to main memory.

Use this instruction with care. Data cached internally and not written back to main memory will be lost.

Хм. Интересно!
Насколько я понимаю, эта инструкция используется в очень специфических случаях (например, при использовании части кеш-памяти в качестве адресуемой физической памяти).
НЛО прилетело и опубликовало эту надпись здесь

Последствия вызова

А я и не спорю. Но никто не гарантирует что в новом процессоре не будет добавлено других иструкций с кешем. У меня просили пример. Я привел. Кроме того пусть падает, пассворд то уже меня. А вообще вот:

Unfortunately, for architectures that do not manage their cache or Translation Lookaside Buffer (TLB) automatically, hooks for machine dependent have to be explicitly left in the code for when the TLB and CPU caches need to be altered and flushed even if they are null operations on some architectures like the x86. These hooks are discussed further in Section 3.8.

https://www.kernel.org/doc/gorman/html/understand/understand006.html

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

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

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

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

Современные процессоры позволяют программисту ошибаться миллиарды раз в секунду. :)

Вы не поверите сколько и каких делают проверок, например в медицинской аппаратуре, в приличных фирмах

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

Или вот это int x; int *p = &x + 1; это ж совсем за гранью, такое писать! Они ждали каких-то гарантий расположения переменных (а то и их существования) на стеке? Да ещё и с арифметикой указателей поверх этого? Безумие.

Вот такой ассерт: assert(a + 100 > a); где a+100 это уже чёрт-те какое значение. Очень странной выглядит жалоба, что нельзя сравнить чёрт-те какое значение с другим значением, и на выходе не получить чёрт-те что.

Я ещё понимаю Торвальдса и Кармака, у них список платформ ограничен и известен зараннее, им можно полагаться на то что int* и double* можно свалить в одну кучу, и не получить по рукам (да и то не всегда). А у комитета такой роскоши нет, им надо рассчитывать на потенциально любой компилер и платформу, даже те гипотетические, которых ещё нет.

где a+100 это уже чёрт-те какое значение.

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


Или вот это int x; int *p = &x + 1; это ж совсем за гранью, такое писать! Они ждали каких-то гарантий расположения переменных (а то и их существования) на стеке?

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


Вот уже надеяться, что &x + 1 укажет на y наверное странно… если только в стандарте не прописано, что переменные кладутся в стек в порядке их объявления. А то, что y тоже будет на стеке следует из того, что берется ее адрес.

неопределенное поведение задумывалось как возможность написать программу под специфическую платформу

Нет, для этого есть implementation-defined behaviour и unspecified behaviour.

изощренный способ сказать "не пишите так"

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

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

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

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

Отлично — что мешает завтра появиться платформе, на которой «add a,b» вышибает нафиг, если carry flag поставился бы?

Только почему-то прогресс идёт в обратном этому направлении, в ARM и RISC-V деление на 0 проглатывается молча. Странные люди, наверно…
И платформ с тритами вместо битов никто не пишет…
А в C++20 вообще дополнительный код канонизировали как единственно возможный. Наверно, не рассчитывают, что завтра Intel скажет «всё, нафиг» и перейдёт на 1-complement…
[/sarcasm]

Вспоминается статья Криса Касперски "Языки, которые мы потеряли". Тут статья немного о другом, но в целом получилось "На этот раз мы теряем и Си".

Если упрощенно (кому лень читать, но как по мне... в общем, прямо-таки рекомендую прочесть её), ту статью можно пересказать таким набором тезисов:

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

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

  3. Настоящие языки метапрограммирования, поддерживающие СМК (самомодификацию кода) на официальном уровне - а именно Лисп и Форт - по сути забыты (отдельное уточнение: здесь не соглашусь с Крисом - и тот, и другой язык жив, но назвать их существование достойным на фоне однотипных потомков языка C во главе "плюсами", а с учетом последних стандартов и описанных в статье фокусов с компиляторами уже и обыкновенного C, всё же язык не повернётся)

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

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

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

Если и сравнивать "по Страуструпу", тогда уж уместнее это делать так: языки делятся на те, на которых людей всё равно заставят работать, и те, на которых они могли бы; в такой интерпретации данное суждение действительно неоспоримо.

Кобол -- это немного другое. Кобол был изначально создан как нишевый язык для бизнес-приложений, и вам надо было днём с фонарём искать людей, которым он нравился даже тогда. Соответственно, в наши дни предприняты большие усилия по продвижению .NET/Java в качестве "дефолтного" языка для таких случаев.

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

Сейчас мы наблюдаем совершенно иной процесс: "маленький и приятный" язык, который изначально создавался в пику чему-то монструозному тоже превращается в монструозность, см. Python. А если не превращается, то остаётся нишевым. Можно посмотреть, как растёт с годами объём документации.

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

Заметьте, мы тут обсуждаем оптимизации gcc, которые все как раз направлены на рост производительности итогового приложения. Расширения стандарта C++ (move semantics, в частности) тоже про более эффективный код. Так что мы видим попытки сделать софт не толще и медленее, а ровно наоборот. Нередко скорость достигается ценой разбухания, но это отдельный вопрос.

Ну сам кобол может и помер, но потомки живы ru.wikipedia.org/wiki/ABAP/4
НЛО прилетело и опубликовало эту надпись здесь

Если gcc нетривиальным образом обновился, то он поменяет своё поведение. Иначе зачем его вообще обновлять?

Каким образом разработчики gcc должны отличать "хорошие" изменения поведения от "плохих"?

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

Я сначала подумал, что это какой-то азиатский язык, а потом как вспомнил...

Одним из главных аргументов наезда на си является то, что он не так хорошо заточен для оптимизации как предлагаемая альтернатива.

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

Но зато оптимайзер сэкономил сто байт памяти переменных.

По моему цель не оправдывает средства.

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

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

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

Полезная статья! (для меня)
Показало, ещё одну возможную проблему в моём коде. Хотя, я уже сталкивался с "strict aliasing" . Исправлял код, не вникая в суть проблемы. Теперь отнесусь более внимательно к стандарту. (и снова, долго и упорно буду проверять свой код)
А PVS-Studio обнаруживает подобные ошибки? (наверно да, но хочется узнать точно)

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

Подозреваю, что если "сервер", то основную часть времени он сидит в ядре (epoll или что там у вас). Эффективно перекладывать байты с диска/БД в пакеты можно и на PHP, и на node.js =)

float Q_rsqrt( float number )
{
	long i;
	float x2, y;
	const float threehalfs = 1.5F;

	x2 = number * 0.5F;
	y  = number;
	i  = * ( long * ) &y;                       // evil floating point bit level hacking
	i  = 0x5f3759df - ( i >> 1 );               // what the fuck? 
	y  = * ( float * ) &i;
	y  = y * ( threehalfs - ( x2 * y * y ) );   // 1st iteration
//	y  = y * ( threehalfs - ( x2 * y * y ) );   // 2nd iteration, this can be removed

	return y;
}

Если бы не C то в Doom бы мы не поиграли на Intel386

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

Современный компилятор GNU C++ можно заменить разве что на LLVM. И обе эти программы написаны на с и обеспечивают широкую поддержку современных платформ . Так что если вы хотите избавиться от с, а вам следует предложить ЯП, пригодный для написания компилятора. Но поскольку в этом направлении никто не двигается, то очевидно все новые языки будут лишь сокращать область применения с, но заменить его полностью не смогут. до тех пор, пока не будет реализован набор компиляторов, написанных на них с достаточным охватом современных платформ

> вам следует предложить ЯП, пригодный для написания компилятора. Но поскольку в этом направлении никто не двигается

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

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

В данном случае это факт инерции — уже есть две мощнейших реализации на C/C++ и переписывать их на нечто новое никто просто так не будет.

Вот и получается, на текущем уровне развития технологий с незаменим.

> Вот и получается, на текущем уровне развития технологий с незаменим.

Если «уровнем развития технологий» называть таковой чисто из-за лени, то да, согласен. Но это не значит, что C/C++ не могут уйти в свою специфическую нишу «для компиляторов», где доживут ещё лет 20. Всё может — как только появится полноценная замена, поддержанная кем-то толстым.

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

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

Потенциально пригоден - возможно.

НЛО прилетело и опубликовало эту надпись здесь
Компиляторы на C пишут потому что а) так исторически сложилось и б) потенциально можно добиться очень высокой производительности.
Кстати, а вы можете назвать хотя бы один язык, который был бы принципиально непригоден для написания компилятора, учитывая что языки Тьюринг-полные, а значит на них можно написать эквивалентную GCC/Clang логику?
Я даже не очень сильно удивлюсь, если лет через 5 в новостях будет «игрок собрал компилятор подмножества C в Minecraft»

Компиляторы давно пишут на чём попало. Это просто праздник какой-то . Особенно в последние пару лет популярна идея source-to-source compiler. Из ruby в javascript (opalrb.com)), ещё WASM кое где пропагандируют. Я не знаю, может быть в будущем появятся операционные системы, написанные на интерпретируемом языка.

Разногласия у нас только в пункте б)

 б) потенциально можно добиться очень высокой производительности.

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

Ключевая часть этого тулчейна - это форматы объектных файлов, утилиты для работы с ними плюс соглашения типа call conventions. От языка и компилятора Цэ они вполне отдираются, тот же gnu ld нормально работает c сlang

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

На счет "пароль останется в регистре процессора". Размер регистра не позволит пароль там хранить из-за размера регистров. Абсурд в квадрате.

Разыменовывание нулевого указателя тоже идея сомнительная. Можно же просто этого не делать, что человек пытается этим добиться? Но на некоторых аппаратных платформах по абсолютному адресу 0 можно найти таблицу векторов прерываний. В эмбеддед большинство железа имеет абсолютные адреса (без MMU) обращения к ним может иметь смысл.

Человеку, допустившему такие ошибки, ничего не помешает при портировании кода на Rust просто взять и затолкать в блок unsafe всё (взяв первую ссылку которая бы посоветовала это сделать и не дочитать). Если следовать той же логике - rust тоже дает себе в ногу стрелять и поэтому надо заменить. + большой пласт ошибок с дедлоками и конкаренси никак не наловишь.

Если интересно писать на си безопасно, обычно применяют базовые вещи - юнит тесты, и санитайзеры: undefined behavior, address sanitizer, memory sanitizer, thread sanitizer. Однако если стиль кода хороший, чаще всего ловятся логические ошибки, от которых ни один язык программирования не сможет защитить, только тестирование. Также существуют стат анализаторы которые большой пласт ошибок ловят.

Аргументация слабая и если рандомно стучать по кнопкам явно не следует ожидать чего-то хорошего. Раньше автор умрёт от старости чем си.

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

> На счет «пароль останется в регистре процессора». Размер регистра не позволит пароль там хранить из-за размера регистров. Абсурд в квадрате.

В 64-битном регистре помещается 8 символов ASCII. У многих пароли не длиннее. Если у кого-то длиннее — то знание первых 8 символов вместе с 1% личной информации может помочь догадаться об остальных.
В XMM, YMM соответственно 16 и 32 (через них любят копировать память). Редко какие пароли длиннее.
Прежде чем рассказывать про «абсурд в квадрате», примените арифметику.

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

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

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

> 8 символов влезет, но если пользователь 9 впишет?

Попробуйте внимательно прочитать мой комментарий.

> И куда терминальный символ делся?

Для задачи типа «подобрать пароль с 10 попыток» он не критичен.

> Как быть с фактом, что пароль в регистре ничего не дает?

Такого факта нет. Ознакомьтесь, пожалуйста, с проблемами неочищенной памяти в теме секьюрити.

> Пароль в регистре что-то может дать, если подключился дебаггером и бог ассемблере, который может весь фотошоп отладить внимательно смотря в регистры и аллокации.

И всё-таки ознакомьтесь с проблемами неочищенной памяти. Минимальный комплект проблем:
— Нарушение секьюрити в другом компоненте (библиотеке), через которую утекают данные (например, недочищенный padding).
— Дампы всяких падений, которые увидит админ.
— Spectre и тому подобные средства на чтение памяти.

Полный комплект сильно шире, но sapienti sat.

Не думаю, что другие языки не используют регистры и RAM. Соответственно это применимо ко всем языкам. Чем тогда именно си плох? Если в rust (да любой язык вместо) создать переменную строку, потом сравнить с другой, где строки будут?

И сколько байт от пароля окажется в регистре - детали имплементации. Может один байт. В других языках как будет дело?

Нарушение секьюрити в другом компоненте - также при чем тут си? В других языках такого нет? Например в Rust подключенная либа с unsafe (любой язык кроме rust также)

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

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

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

> Это просто то, что вне языка программирования.

Да, поэтому в набор требований security входят и внепрограммные методы, например, вплоть до полной радиоизоляции отдельных компонент.

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

Я понимаю, что си не без грешка. Но он и применяется сейчас в основном в эмбеддед. И да, рядышком улеплены везде ассемблерные вставки обычно в sdk так как работа в эмбеддед касается железа напрямую. Зачастую больше кода там "unsafe" так как приходится обращаться напрямую к регистрам, аппаратной части, DMA итд. Там и аут оф баунд используют на структурах во благо (в парсере протоколов кастуют буфер на структуру с массивом единичной длинны и вкладывают в структуру байтик длины, не безопасно, но видал такое часто...). Поэтому аргументы кажутся слабыми ибо рассматривался язык как высокоуровневый. Но это давно не так.

В мире эмбеддед еще есть свои ограничения по памяти, например 1кб под ROM. Есть специфические требования, например DMA не умеет работать с ROM и вызовет хардфолт. Иногда требуется вообще к какому-нибудь адресу пригвоздить данные. Я не пробовал rust, но подозреваю, что в тех местах, где меня надо "спасать", он не поможет. Ибо эмбедеры обычно берут готовые абстракции для очередей и мем пулов. Выключают heap. И работают на стат буферах, где ошибиться довольно сложно... Таблицу векторов прерываний на си пишут или asm, там вообще функции строго по адресам должны быть. Пишут стартап файлы свои, чтобы из своих секций загружать данные правильно.

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

А spectre не страшен, если ты экзекьюшен с RAM выключил и отключил debug интерфейс (аппаратная защита).

> Я понимаю, что си не без грешка. Но он и применяется сейчас в основном в эмбеддед.

Ну с точки зрения application development — вероятно, да. С точки зрения остального… Linux на толстом сервере это embedded? А какая-нибудь Oracle DB?
Вы слишком сбоку смотрите.

> И да, рядышком улеплены везде ассемблерные вставки обычно в sdk так как работа в эмбеддед касается железа напрямую.

Очень мало где. И везде где можно вместо этого применить интринсики — переходят на них, потому что безопаснее, переносимее и понятнее.

> Поэтому аргументы кажутся слабыми ибо рассматривался язык как высокоуровневый. Но это давно не так.

Как правильно говорят рядом в комментариях, он остаётся высокоуровневым, но иначе.

Прочитал, кто в своём уме будет сначала разименовывать указатель, потом проверять на ноль?

НЛО прилетело и опубликовало эту надпись здесь
Ну в случае инлайнинга, скорее всего, в компиляторе эта логика не сработает (хотя кто их знает, этих писателей). А вот если код подставлен макрой — я бы дал 100%…
НЛО прилетело и опубликовало эту надпись здесь
Ну это «немного» не то. Оно только удаляет бесполезную (по его мнению) проверку в контексте, где по условиям вызова «гарантировано» за счёт разыменования, что указатель не пустой. Так как разыменование безусловное, я бы это не считал диверсией.

А вот если бы был код типа

int val = *ptr;
if (!ptr) log();


в одной функции (существенно), и оно, определив, что есть проверка ptr, сочло, что чтение значения *ptr в val — UdB, и на этом основании что-то сломало (например, записало бы всегда 0 в val) — было бы интереснее. Но такого я добиться не смог.
НЛО прилетело и опубликовало эту надпись здесь

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

Почитайте блог PVS-Stuidio. Так регулярно кто-то делает.

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

Во-первых ансейф блок не гарантирует отсусттвия проверок: https://steveklabnik.com/writing/you-can-t-turn-off-the-borrow-checker-in-rust


Во-вторых стд останется в сейф блоках, да ещё и провалидированная сверху.


Так что даже в таком сравнении раст может выглядить интереснее

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

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

НЛО прилетело и опубликовало эту надпись здесь
так это ещё и максимально неудобная для железа структура данных — скакать по всей памяти, вымывая кеши и таблицы ради непонятно чего
Не обязательно выделять тупо каждый объект маллоком! Это лишь способ связать объекты в некий ряд по каким-либо правилам для мгновенной навигации между объектами без пложения всяких дополнительных таблиц (в которые именно что и придётся лазить, портя кэши, прежде чем всё равно обращаться к нужному объекту). Сами по себе объекты могут быть выделены как угодно и где угодно. Разумеется, объект не обязан быть элементом только одного списка и вообще структура быть именно линейным списком тоже не обязана. Ссылок из объекта может быть произвольно много на родительские, дочерние, соседние, связанные, и ещё какие угодно объекты. Двусвязность же даёт возможность мгновенно убирать объекты из структуры, имея только указатель на объект.
Можно наплодить сколько угодно «более лучших языков», но пока в них надо строить структуры данных из убогих «контейнеров», никуда сишечка гарантировано не денется.
Двусвязность же даёт возможность мгновенно убирать объекты из структуры, имея только указатель на объект.

При условии, что объект реально лежит в каком-то списке.


При условии, что вы это делаете из одного потока.


При условии, что указатели могут не указывать на сам этот объект.


При условии, что сам объект не меняет своего положения в памяти.


Ещё раз, для чего вам потребовались интрузивные списки?

Двусвязный список в сишке сделать просто только потому, что на часть проблем структуры ответ дается в стиле "да мамай клянус такого не будет", а раст в маму не верит, ему нужно доказать. А вот написать всегад корректный список который с помощью произвольного количества вставок/удалений не выйдет развалить — уже дроугая задача.


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

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

Это про Раст речь или про C? Я иногда краем глаза смотрю, что там в исходниках десктопных линуксов, и, по-моему, если проект на С написан - то там связные списки оказываются с завидной регулярностью.

Примеры - поищите по wl_list или wl_list_init в wlroots (https://gitlab.freedesktop.org/wlroots/wlroots/-/tree/master/), по GList в mutter (https://gitlab.gnome.org/GNOME/mutter), по list_init в libinput (https://gitlab.freedesktop.org/libinput/libinput/-/tree/main), не говоря уже про тысячи различных списков в ядре линукса (искать, например, по list_head).

Это коммент 100%)) я попытался граф на расте написать, вообще непонятно кто чем владеет. а уж если гиперграф то полный аут

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

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

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

А что имеется ввиду под этим кодом? Код, который пользуется графом максимально прост: 1) добавить вершин штук n со свойствами 2) добавить граней между ними со свойствами на гранях 3) найти вершину по значению свойства и от нее сделать 10 хопов с фильтром по значению свойства на грани

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

Почитайте что-нибудь про графовые субд - сильно удивитесь.

:))

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

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

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

ну это самый дубовый подход, скажем так, на каждом шаге обхода графа нужно дергать следующую грань по индексу, т.е. получится ничем не лучше табличных баз. Пока что два академических подхода существует - adjacency list и adjacency matrix. В первом объект вершины хранит на себе указатели на соседей (что я пытаюсь сделать, это подход neo4j https://youtu.be/LSKa3as_S7I?t=635), второй пока что только Редис прорабатывают, там можно почитать https://oss.redis.com/redisgraph/

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

Минус как раз за adjacency matrix. Это, наверное, самый идиотский способ хранения графовой СУБД, поскольку он требует квадратичной памяти относительно числа вершин, а вершин в БД могут быть миллионы (в лучшем случае).

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

Хранения разные бывают, в RAM это может быть 1) матрица смежности, 2) листы смежности (поинтеров) на объектах нодов, ну и эмуляции 3) на таблицах и 4) на колоночных базах. А на диске, ну матрицу понятно так и хранить, как хранить adjacency list это у каждого коммерческий секрет, у neo4j они рассказывают свой, таблицы и колоночные сами сериализуют. Последные два способа для RAM самые меделнные из-за индексов. Второй способ (лист) самый популярный сейчас

Ну там GraphBLAS это вычисления этих матриц на GPU, перспективное направление, вершин может быть и несколько миллионов. Зато есть интересные феномены, я в лекциях видел, можно умножить матрицу смежности саму на себя - получатся все random walk

Ну и как связаны СУБД и вычисления на GPU?

СУБД предназаначены для вычислений (ответов на query), хранение это вторичная функция. Вычисления могут быть на GPU. Траверсить граф умножая матрицы это хорошая идея

Траверсить граф умножая матрицы — это ужасная идея в общем случае.

А еще один аргумент по поводу adjacency matrix. Я вспомнил в лекциях говорили что если граф неплотный, то лучше adjacency list, а если плотный - лучше adjacency matrix, плотность это количество граней на вершину. И понятно почему. В adjacency list для хранения грани нужно два pointer'а по 8 байт, с одной и с другой стороны, это простейший случай когда нет свойств на грани и pointer прямо на вершину соседа. Pointer это uint64 = 8 bytes, итого 16. А сколько нужно для каждой грани в adjacency matrix? bool в ячейке, есть/нет, т.е. 1 байт

Ага, а теперь подумайте откуда возьмётся плотный граф в СУБД. А ниоткуда он не возьмётся.


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


Сильно ли похож граф на плотный? Можно ли пути в нём подсчитать на GPU вашим возведением матрицы в степень?

Похож как только они лайкать начнут :) каждый будет лайкать по 20 постов новых в день и все. Вообще графы очень разные бывают, это от предметной области зависит.

Неа, даже если они начнут лайкать по 20 постов в день, граф всё равно даже близко не приблизится к плотному.

В математическом определении - нет. "Граф, в котором число рёбер E близко к максимально возможному у полного графа с числом V вершин" А практически получается что база растет на 30 ГБ в день (100m*20*16bytes), а с матрицей не растет вообще, там уже размечено false'ами все

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

Мы обсуждали хранение граней, если что. Да, такая соцсеть неудачный пример, лайкают новые посты и число вершин будет расти. Есть графы где число вершин относительно стабильно и невелико, а число граней растет очень быстро. Мне пришел в голову пример соцсети типа Тиндер, где юзеры лайкают друг друга. Число вершин почти постоянно - это юзеры, невелико - граф локальный, например для москвы это будет миллион вершин, а вот налайкивают они там, я думаю просто с бешенной скоростью, 20 лайков в день далеко не предел. То есть понятно, что граней быстро станет очень много. А траверсал пускай будет рекомендация М->Ж<-М->Ж (если не брать "вырожденные" случаи М->М то работать будет :))

Грани существуют только у плоских графов, хватит уже путать их с рёбрами и дугами! Один раз вас поправили, второй — сколько можно?!


Есть графы где число вершин относительно стабильно и невелико, а число граней рёбер растет очень быстро.

Нет таких графов там, где применяются СУБД.

Оу я использую edges обычно, форум русскоязычный просто :) я кстати пришел к оценочной формуле в этом споре, пусть граф 1млн вершин, матрица сравнивается с листом по памяти при 62.5тыс edges на вершину (1млн / 16). А вот при 500тыс. вершин это уже 31тыс. edges на вершину. Я так думаю лайкнуть 30тыс. девушек из 250тыс. вполне живая цифра.

Про "не существуют" :)) да сложно вообразить какие графы существуют без практики

А второй момент, конкретно Редис говорят про sparce matrices https://github.com/RedisGraph/RedisGraph я не смотрел еще имплементацию, благо код открытый можно глянуть, но название звучит как раз то что нужно

sparce adjacency matrix — это примерно то же самое, что и adjacency list, только через жопу в оригинальном исполнении

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

Кстати, с подсчётом места для adjacency matrix вы тоже напутали. Дуги-то (а там хранятся именно дуги, а не грани!) в СУБД — "цветные"!


Возвращаясь с примером про соцсеть, рассмотрим отношения пользователей и постов.


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


И если вы собираетесь возразить что пять типов в байт влезают, то вспомните сколько ещё есть фич в соцсетях.

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

А вот еще над чем стоит подумать, есть такая проблема super-node, довольно частая кстати в практике, это когда у вершины очень много edges. Допустим есть две вершины по паре миллионов у каждой, и нужно проверить есть ли между ними связь. В матрице понятно стоимость такой операции минимальна и константа. А вот с листом это постоянная проблема, стоимость операции пропорциональна числу edges (меньшему)

Зато с матрицей у вас есть проблема с поиском соседей для всех остальных вершин.


Что же до проблем списка — если они актуальны, их можно решить переходом на более сложную структуру данных, например на B-дерево.

Для матрицы я проблемы не вижу, поиск соседей это всегда выбор в двумерном массиве, О(1). А вот для листа, даже не для super-node, это всегда O(n). Причем это операция довольно частая, проверить связь между известными вершинами https://neo4j.com/docs/cypher-manual/current/execution-plans/operators/#query-plan-expand-into

выбор в двумерном массиве, О(1)

Это шутка такая?

Да, точно, там же фильтровать надо будет еще false'ы, спасибо.

Про более сложную структуру (локальный индекс на вершине), да, я думал, там две проблемы. 1) там сейчас уже хеш-таблица на каждой вершине, из-за типов edges и IN/OUT, в neo4j вершина уже 15 байт из-за этого, ну вот похожие решения https://www.datastax.com/blog/solution-supernode-problem

2) второй момент для меня более важен, этот индекс должен зависеть от каких-то свойств вершин-соседей, а это coupling нам этого не надо

Во-первых, прелесть B-дерева в том, что пока данных мало — оно слабо отличается от массива.


Во-вторых, его можно построить просто по номеру/PK соседней вершины, не заглядывая в её данные. А если вам такого дерева по какой-тио причине не хватит — вам и матрица не поможет.

Грани отдельно можно не хранить. Графовую субд удобно строить на документах, которые ссылаются друг на друга. Грани и вершины - частный случай таких документов. В простейшем случае заводятся два файла: один для данных, другой со ссылками на актуальные версии этих данных. Документы соответственно ссылаются на позиции в этом файле со ссылками. Соответственно любой переход - это 2 хопа. Если база иммутабельная (точнее, если размер записей не может расти), то можно сразу ссылаться на позиции в файле данных и получить переходы за 1 хоп. Далее накручивается разделение на кластеры, но суть та же.

Даа :)) так построены как минимум ArangoDB и Azure CosmosDB (поверх их DocumentDB). Могу сказать это две чуть ли не самые медленные базы. Потом стоит задуматься как это все будет работать. То, что здесь называтется "хоп" это на самом деле операция I/O с диском. Допустим, нам нужно найти всех соседей на расстоянии 3. Берем первый "документ" vertex1, читаем с диска его данные, выбираем из этих данных ссылки на "документы" граней, идем за актуальными версиями для них, читаем сами документы, выбираем из них ссылки на вершины-соседы, идем за их актуальными версиями, читаем документы вершин-соседей... И это пока только первый шаг, а нам надо сделать еще два. Да нет эта идея жесть конечно. То, что набор свойств для вершины/грани похож на документ и его можно хранить отдельно. Да, можно, хотя бы ту часть свойств которая не используется в траверсалах, разгрузится графовая база. Но опять таки это перерасход места на 8 байт на объект как минимум и дополнительные I/O операции, лучше хранить скалярыне свойства прям на объекте вершины.

Тут в старых бенчмарках Arango совсем не медленная.

Хоп - это операция рандомного чтения. Если данных не окажется в памяти - будет чтение с диска. При джойнах в реляционках таких хопов будет логарифм, а не константа.

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

А если грань не легковесная, а нам не нужны ее свойства, просто пройти дальше?

Очевидно.

Очевидно что либо доставать документ грани, либо хранить два раза как "легковесную" и как обычную, для разных случаев :))

Давайте просто забудем про тяжеловесные грани. Не нужны они. Лишняя абстракция.

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

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

Кстати насчет Лиспа, I have thought, как говорится. Во-первых, этот подход не превносит в проблему ничего нового, мы можем в Расте сделать объект Граф, он будет владеть всеми вершинами, но тут проблема со временем жизни и ссылками друг на друга, вершины могут удаляться. Для это Computer Programs это и может подойдет, там граф не мутабельный (как правило, код программы статичен), а для данных - нет. А во-вторых, на самом деле граф не всегда имеет форму дерева, точнее почти никогда. Если задуматься над такими вопросами: 1) что если "лист" из одной ветви ссылается на "лист" из другой ветви? 2) Или например "лист" ссылается на самую верхнюю вершину? Я так понимаю, этот вопрос поднимается здесь https://coderoad.ru/23724226/ где ему отвечают что Лисп будет использовать CLOS (общая объектная система Lisp), т.е. ничего нового относительно других языков кроме Си и Раста, привет, сборщик мусора, который будет мониторить ВСЕ объекты.

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

А вот прямы ссылки на связанные вершины\ребра - надо хранить боксовыми указателями (или что вам нравится для необязательных ссылок).

Да, я подумал про Option, это будет безопасная проверка указателя на null в рантайме, причем всех указателей из adjacency list, каждый раз, и без надобности, это жесть. Надо чтобы сразу по указателю шло, ничего не проверяя

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

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

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

это ценноге замечание, надо замерить

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


Количество вычислений не зависит от формы этой проверки.

Не понял, зачем. Допустим query все соседи на расстоянии 1, разворачиваем лист. все на этом. Зачем знать что там дальше?

С завидной регулярностью ходят слухи о скорой смерти С/С++...

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

Просто не давайте спички детям без присмотра :)

Практика показывает, что на C не умеет писать никто.

Настоящая история и мотивация создания языка Си практически не отражена, приведена книжно-википедийная версия пересказа кума подруги тёлки брата. Даже языки B и BCPL не упомянуты.

Абстрактная машина должна была решить две проблемы одновременно

И создать новую проблему -- проблему "что такое абстрактная машина и как она работает?". Во всем стандарте нет ни малейшего намека, как работает абстрактная машина, но при этом весь стандарт построен вокруг неё, в частности, многие вещи подразумевают некий "доступ". Например, "a = &b->field" -- я делаю "доступ" к b? Отсутствие определения абстрактной машины -- это причина, по которой компиляторы ломают ранее работавшие программы. Отмаза одна и та же -- "стандарт не запрещает". Да, он и не разрешает. Абстрактная машина -- это одно сплошное undefined behaviour.

Эта свобода интерпретации стандарта привела к тому, что отдельные неадекватные вахтеры из команды разработки GCC забаррикадировались от здравого смысла и отстреливаются лишь дежурными "я так увидел стандарт". Что их фичи превращают написание и отладку программ в кошмар -- их не волнует. Слава богу, что у нас есть ключи "-fwrapv -fno-strict-aliasing"

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
Вижу статью с:
— Кликбейтным заголовком.
— Кучкой классических примеров UB.
— Ссылками на людей, не понимающих смысла слова «undefined».

Ожидаю увидеть в комментариях:
— Множество экспертов, точно знающих, как на самом деле надо правильно писать компиляторы.
— Комментаторов, в очередной раз тщетно пытающихся донести до экспертов смысл слова «undefined».

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

А можно выдержку из стандарта, где зафиксирована легитимность такого вывода?

Выдержки из стандарта не будет, да она и не нужна, т.к. такое поведение компиляторов - это объективная реальность. Вы же встретившись с гопниками в подворотне не будете рассказывать им про УК?

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

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

Возьмём следующий фрагмент кода на языке Си:

int x = 1;
x = x << sizeof(int) * 8;

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

Если размер байта больше 8 бит (такое вполне бывает и разрешено стандартом), ничего страшного не произойдет. Если хочется получить UB, лучше использовать макрос CHAR_BIT

int x = 1;
x = x << sizeof(int) * CHAR_BIT;

Кстати, тут можно и unsigned int взять. Если unsigned int 32-битный, сдвиг единицы на 32 тоже будет UB.

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

Да, есть такой баг. https://gcc.gnu.org/bugzilla/show_bug.cgi?id=56888

Только вы определитесь, вы конкретный компилятор Си ругаете, ли сам Си?

Ваша программа работает с базой данных пользователей, хранящей их имена и пароли, и вы описали примерно такую функцию:

int check_password(const char *pwd)
{
    char real_pwd[32];
    get_password(real_pwd);
    return !strcmp(pwd, real_pwd);
}

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

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

Кстати, по поводу хешей. Взять например SHA256Transform за авторством Aaron Gifford которое присутствует в OpenBSD например http://fxr.watson.org/fxr/source/crypto/sha2.c?v=OPENBSD#L309

Там есть такая замечательная строчка, как a = b = c = d = e = f = g = h = T1 = 0; , где все эти a b c d ... являются локальными переменными в этой функции. Что вообще автор хотел этим сказать? Видимо он хотел избежать утечки каких-то данных через стек, и даже возможно через регистры. Компилятор это зануление локалок конечно имеет право выкинуть, но ДОПУСТИМ он не выкинет их, и действительно занулит соответствующие регистры и стек. Только в стеке и в регистрах могут быть записаны какие-нибудь промежуточные результаты вычислений, а их как вообще занулить? Писать специализированный компилятор, который все использованные функцией адреса в стеке и все использованные регистры (кроме возвращаемого значения) забивает нулями?

Если размер байта больше 8 бит (такое вполне бывает и разрешено стандартом), ничего страшного не произойдет. Если хочется получить UB, лучше использовать макрос CHAR_BIT

Да, стандарт действительно не устанавливает верхнюю планку для размера char, однако минимальный размер - это всегда 8 (2.2.4.2 Numerical limits):

The values given below shall be replaced by constant expressions suitable for use in #if preprocessing directives. Their implementation-defined values shall be equal or greater in magnitude (absolute value) to those shown, with the same sign.

* maximum number of bits for smallest object that is not a bit-field (byte) CHAR_BIT 8

Так что использование явной константы 8 в качестве размера для битового сдвига достаточно, чтобы вызвать UB в большинстве случаев, в том числе и при компиляции примера gcc для x86 (что и было продемонстрировано).

Формально вы, конечно, правы, и чтобы словить UB во всех возможных ситуациях, нужно использовать макрос, но идея тут была в другом. На самом деле пример со сдвигом нужен, чтобы показать, что даже для той архитектуры, для которой известны размер минимально адресуемой ячейки памяти (8 бит) и поведение инструкции битового сдвига, нет гарантии, что будет сгенерирован тот код, который ожидает программист.

Да, есть такой баг. https://gcc.gnu.org/bugzilla/show_bug.cgi?id=56888

Только вы определитесь, вы конкретный компилятор Си ругаете, ли сам Си?

Спасибо за отличную ссылку, вот этот комментарий порадовал особенно:

We are not presently experiencing this issue in musl libc, probably because the current C memcpy code is sufficiently overcomplicated to avoid getting detected by the optimizer as memcpy.

Т.е. реализация memcpy должна быть настолько сложной, чтобы компилятор не смог оптимизировать ее исходный код - прекрасно!

Почему вы считаете, что это баг компилятора? Компилятор ведет себя в соответствии со стандартом (4.1.2 Standard headers):

Each library function is declared in a header, whose contents are made available by the #include preprocessing directive. The header declares a set of related functions, plus any necessary types and additional macros needed to facilitate their use. Each header declares and defines only those identifiers listed in its associated section. All external identifiers declared in any of the headers are reserved, whether or not the associated header is included. All external identifiers that begin with an underscore are reserved. All other identifiers that begin with an underscore and either an upper-case letter or another underscore are reserved. If the program defines an external identifier with the same name as a reserved external identifier, even in a semantically equivalent form, the behavior is undefined.

Да и на самом деле мы ругаем не вакуумный язык Си, а то, что его концепция и история вводит людей в заблуждение о его сущности. И это заблуждение поддерживалось в том числе комитетом стандартизаторов - для чего было вводить столько UB, перекладывая ответственность на создателей компиляторов, и делать вид, что с Си ничего не произошло? Си, который создал Ритчи, и тот Си, который определен в стандарте - это абсолютно разные языки, только внешне они практически неотличимы. Первый - это язык для конкретной машины PDP-11, второй - абстрагированный язык высокого уровня.

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

Мы упомянули о том, что пароль (или его часть) может остаться в регистре. На самом деле ответ прост - в рамках самого языка Си эта проблема неразрешима. В тексте стандарта даже, очевидно, слово "stack" не упоминается - об очищении чего тогда вообще может идти речь? Локальные переменные могут находиться где-угодно, и исключительно средствами Си это проконтролировать невозможно. Подробнее об этом написано тут (ссылка также есть в статье):

http://www.daemonology.net/blog/2014-09-06-zeroing-buffers-is-insufficient.html

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

Т.е. реализация memcpy должна быть настолько сложной, чтобы компилятор не смог оптимизировать ее исходный код - прекрасно!

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

Почему вы считаете, что это баг компилятора? Компилятор ведет себя в соответствии со стандартом (4.1.2 Standard headers):

Потому что см. https://gcc.gnu.org/bugzilla/show_bug.cgi?id=56888#c12 :

Now, if this replacement still happens when you compile with -nostdlib, that would be a bug since it becomes legal code in that case.

Т.е. флаг -nostdlib по-идее должен это разрешать, а он не разрешает. Там советуют использовать опцию -fno-tree-loop-distribute-patterns

Можно использовать для таких функций attribute ((optimize ("-fno-tree-loop-distribute-patterns")))и тогда должно нормально компилироваться без рекурсий.

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

Для переносимости и для более агрессивной оптимизации.

Мы упомянули о том, что пароль (или его часть) может остаться в регистре. На самом деле ответ прост - в рамках самого языка Си эта проблема неразрешима. В тексте стандарта даже, очевидно, слово "stack" не упоминается - об очищении чего тогда вообще может идти речь?

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

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

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

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

Потому что см. https://gcc.gnu.org/bugzilla/show_bug.cgi?id=56888#c12 :

Now, if this replacement still happens when you compile with -nostdlib, that would be a bug since it becomes legal code in that case.

Это не баг, -nostdlib работает так, как написано в документации gcc:

-nostdlib

Do not use the standard system startup files or libraries when linking. No startup files and only the libraries you specify will be passed to the linker. The compiler may generate calls to memcmp, memset, memcpy and memmove. These entries are usually resolved by entries in libc. These entry points should be supplied through some other mechanism when this option is specified.

Так или иначе вопрос не в том, как заставить компилятор не делать какие-то оптимизации, а в том, валидны ли эти оптимизации с точки зрения языка. И да, они абсолютно валидны. Единственный момент тут в том, что если вы сообщите компилятору, что у вас freestanding окружение (для gcc нужен флаг -ffreestanding), то тогда все, что связано со стандартной библиотекой, будет зависеть от реализации компилятора:

In a freestanding environment (in which C program execution may take place without any benefit of an operating system), the name and type of the function called at program startup are implementation-defined. There are otherwise no reserved external identifiers. Any library facilities available to a freestanding program are implementation-defined.

Но это совсем не означает, что в таком случае компилятор не может самостоятельно добавлять в код вызовы функций стандартной библиотеки. Однако implementation-defined behavior разработчики компилятора уже должны документировать, что они и сделали для упомянутого флага -nostdlib.

Для переносимости и для более агрессивной оптимизации.

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

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

Любой, в документации/спецификации/стандарте которого написано, что он на такое способен. И да, скорее всего это будет только ассемблер, но это не такой уж и плохой вариант. Никто не мешает вам линковаться с объектными файлами, написанными на асме. Решение с флагами компилятора тоже неплохое, но только при условии, что вы действительно уверены, что оно сработает как нужно. Но в рамках самого языка Си решить эту проблему невозможно - в этом и был смысл примера, который мы дали в тексте.

Это не баг, -nostdlib работает так, как написано в документации gcc:

Ну ок, значит не -nostdlib а какой-то другой флаг не сработал. Я просто привел цитату из багзиллы GCC.

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

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

Под который нужно было бы переписывать весь старый код? А кто сказал, что такой язык никто не сделал? Вот D например есть. Или Rust.

Почему не сделали совместимый с Си язык, назвав его по-другому? Тоже сделали, называется он "C++", есть еще "Obj-C". Хотя насчет прекрасности я б тут поспорил.

Зачем было брать прибитый ржавыми гвоздями к архитектуре PDP-11 Си и вводить в заблуждение кучу людей?

Потому что под Си написано много кода, и его хотелось бы не переписывать, а дорабатывать. Введение в заблуждение в чем заключается? Си времен K&R был хорошим, а потом стал плохим? Вот например вы ругаете правила сравнения указателей:

Вот небольшой фрагмент кода, демонстрирующий некорректное с точки зрения стандарта сравнение:

int *p = malloc(64 * sizeof(int));
int *q = malloc(64 * sizeof(int));
if(p < q) /* Undefined behaviour! */
    do_something();

Только вот это было еще в K&R, даже можно найти архивы ньюзгрупп, где обсуждалось еще в 1988 году https://compilers.iecc.com/comparch/article/88-07-002

>K&R 1, page 98:
>"But all bets are off if you do arithmetic or comparisons with pointers
>pointing to different arrays. If you're lucky, you'll get obvious
>nonsense on all machines. If you're unlucky, your code will work on one
>machine but collapse mysteriously on another."

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

Как багом может являться документированное поведение компилятора? Ни один из указанных автором поста флагов не гарантирует, что компилятор не будет вставлять вызовы memcpy в код. Даже упомянутый "-fno-tree-loop-distribute-patterns" не позволяет утверждать, что компилятор не добавит вызов memcpy куда-нибудь еще, так как этот флаг лишь отключает конкретную оптимизацию циклов.

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

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

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

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

Под который нужно было бы переписывать весь старый код?

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

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

Ну правильно, давайте бесконечно дорабатывать работающие программы - делать нам что ли больше нечего?!

А кто сказал, что такой язык никто не сделал?

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

Введение в заблуждение в чем заключается? Си времен K&R был хорошим, а потом стал плохим?

Си, созданный Ритчи, был языком для конкретной машины. Как только он вылез за рамки PDP-11, начались проблемы. Компиляторы Си под новые архитектуры постоянно ломали старый код. А стандарт еще больше подлил масла в огонь - внешне язык практически не изменился, но при этом связь с PDP-11 перестал иметь вообще. Но им было этого мало, и чтобы залатать часть неработающих оптимизаций, они добавили ключевое слово volatile. Без него компиляторы не могли понять, какие обращения в память можно оптимизировать, а какие - нет:

https://groups.google.com/g/comp.std.c/c/tHvQhiKFtD4/m/zfIgJhbkCXcJ

Но спустя 30 лет и этот костыль стал практически бесполезен и даже опасен из-за своей крайне туманной семантики:

https://www.kernel.org/doc/Documentation/process/volatile-considered-harmful.rst

K&R 1, page 98:

"But all bets are off if you do arithmetic or comparisons with pointers pointing to different arrays. If you're lucky, you'll get obvious nonsense on all machines. If you're unlucky, your code will work on one machine but collapse mysteriously on another."

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

+1 Мне не нравится определени UB в стандарте, и мне совсем не нравится текущая интерпретация UB современными компиляторостроителями. Понятно, что в стандарт вводили UB для того, чтобы дать возможность компилятору разрешить сложную ситуацию тем способом, который наиболее подходит для конкретной платформы. Почему нельзя было написать вместо UB — «поведение зависит от платформы», я не понимаю. Но они выбрали написать UB, и стало допустимым любое поведение. А потом случилась автоматическая генерация кода и dead code elimitation. И UB внезапно стали все понимать как "… и тогда код можно вообще не генерировать". Для C++ — это понятно, и он без этих оптимизаций не жилец. Но «C» еще можно спасти, чтобы UB перестали быть UB, а стали platform-specific задокументированным поведением.

Как багом может являться документированное поведение компилятора?

Багом это является по причине того, что в багзилле GCC признали, что это баг.

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

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

Т.е. надо было ничего в Си не менять, а создать новый язык, и было бы всем счастье? Ну допустим не меняли бы они ничего, выпустили бы вместо C89 новый частично совместимый с Си язык, назвали бы его как "Extended-C" например, напихали бы туда этих оптимизаций через неопределеное поведение при стрикт-алиазинге и прочее, на что вы тут жалуетесь, старые программы портировали бы на этот "Extended-C" а сам исконно-посконный Си забросили бы, как забросили языки B и BCPL, и что бы от этого принципиально поменялось?

Си, созданный Ритчи, был языком для конкретной машины. Как только он вылез за рамки PDP-11, начались проблемы.

А в каких случаях проблем бы не было? Вот например в паскале у Integer какой размер в байтах? https://archive.org/download/iso-iec-7185-1990-Pascal/iso-iec-7185-1990-Pascal.pdf - попробуйте там что-нибудь найти про это. На 55 странице этого PDF файла есть упоминание про maxint, который (сюрприз!) implementation-defined. Если посмотреть сюда https://wiki.freepascal.org/Integer - тут сказано Typical sizes of integer generally are 16 bit (2 byte), 32 bit (4 byte) or 64 bit (8 byte) - отлично, т.е. если кто-то в своей программе на паскале предполагает, что Integer 32-битный, на паскале с 16-битным Integer его код корректно не заработает. А если бит не 8-битный? Какие компилируемые в машинный код языки программирования, сопоставимые по уровню абстракции с тем же Си или паскалем, могут работать на (т.е. компилироваться под) архитектурах с не 8-битным байтом, чтобы код при этом не нужно было переделывать? Может надо делать отдельный язык для архитектур с 8-битным байтом, отдельный для 9-битного байта, отдельный для 16-битного байта, отдельный язык для two's complement, отдельный для one's complement знаковых чисел, и так под каждую архитектуру делать язык с особым уникальным именем?

Сейчас уже сами архитектуры подстраивают под языки, например в ARM есть инструкции, добавленные специально для джаваскрипта https://stackoverflow.com/questions/50966676/why-do-arm-chips-have-an-instruction-with-javascript-in-the-name-fjcvtzs

И специально для Си тоже добавляли инструкции чтобы код нормально работал http://c-faq.com/null/machexamp.html

The Prime 50 series used segment 07777, offset 0 for the null pointer, at least for PL/I. Later models used segment 0, offset 0 for null pointers in C, necessitating new instructions such as TCNP (Test C Null Pointer), evidently as a sop to [footnote] all the extant poorly-written C code which made incorrect assumptions.

Багом это является по причине того, что в багзилле GCC признали, что это баг.

Багом эту ситуацию обозвал Richard Biener, который в конечном итоге просто добавил к флагу -ffreestanding отключение конкретной оптимизации циклов, которая не позволяла реализовать memcpy на языке Си.

В том же обсуждении другой разработчик gcc упоминает, что "проблема" остается актуальной до сих пор, так как компилятор продолжает добавлять вызовы memcpy во freestanding окружении, но уже в других местах:

Note that the compiler emits calls to memcpy for struct copies anyway, so if there is a problem it is a long-standing one.

И это, как уже было отмечено ранее, не противоречит ни документации gcc, ни стандарту Си. То, что реализация компилятора не позволила реализовать на нем memcpy не говорит о том, что gcc - это неправильный компилятор языка Си. Это поведение абсолютно валидно с точки зрения стандарта - никто вам такой функционал предоставлять не обязан.

Эту точку зрения подтверждает все тот же Richard Biener:

-fno-builtin-XXX does not prevent GCC from emitting calls to XXX. It only makes GCC not assume anything about existing calls to XXX.

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

Ну допустим не меняли бы они ничего, выпустили бы вместо C89 новый частично совместимый с Си язык

Что вы имеете в виду под частично совместимым? C89 - это не частично совместимый с Си язык, а его единственно верный, как уверяет комитет, вариант:

The need for a single clearly defined standard had arisen in the C community due to a rapidly expanding use of the C programming language and the variety of differing translator implementations that had been and were being developed. The existence of similar but incompatible implementations was a serious problem for program developers who wished to develop code that would compile and execute as expected in several different environments.

Как говорится, благими намерениями вымощена дорога в ад.

старые программы портировали бы на этот "Extended-C" а сам исконно-посконный Си забросили бы

Разница тут в том, что когда вы переписываете программу на новом языке, вы не ожидаете, что семантика у него будет такая же, как и у старого. Тут же случилось так, что все написанные к 89-ому году работающие программы на Си неожиданно в одну секунду оказались, согласно стандарту, неработающими. Только никто этого не заметил, и проблема как раз в том, что программы людьми действительно не портировались (а этих программ было много - один 4.3BSD чего стоил). Все продолжали писать так, как делали это раньше. Последствия неожиданной метаморфозы языка Си из низкоуровневого (по сути ассемблера) в высокоуровневый язык программирования стали очевидны далеко не сразу. А большинство из них неочевидны до сих пор - сколько UB сейчас хранится в исходных кодах легиона программ - страшно себе представить.

Вот например в паскале у Integer какой размер в байтах?

Паскаль всегда был и остается языком высокого уровня, поэтому размер типа Integer там не указан намеренно. Причем переполнение числа является, согласно стандарту Паскаля, ошибкой (Error):

D.47 6.7.2.2
It is an error if an integer operation or function is not performed according to the mathematical rules for integer arithmetic.

Причем сам Error стандарт Паскаля определяет гораздо мягче, чем Undefined Behavior у Си. В частности, такие ограничения накладывает документ на обработку Error-ов:

treat each violation that is designated an error in at least one of the following ways:

1) there shall be a statement in an accompanying document that the error is not reported, and a note referencing each such statement shall appear in a separate section of the accompanying document;
2) the processor shall report the error or the possibility of the error during preparation of the program for execution and in the event of such a report shall b e able to continue further processing and shall be able to refuse execution of the program-block;
3) the processor shall report the error during execution of the program

Т.е., если компилятор Паскаля явно не указал в своей документации иное поведение, он обязан сообщить об ошибке либо во время исполнения программы, либо еще во время ее компиляции.

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

Какие компилируемые в машинный код языки программирования, сопоставимые по уровню абстракции с тем же Си или паскалем

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

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

То, что реализация компилятора не позволила реализовать на нем memcpy не говорит о том, что gcc - это неправильный компилятор языка Си. Это поведение абсолютно валидно с точки зрения стандарта - никто вам такой функционал предоставлять не обязан.

Еще раз. Критерием "баг это или не баг" я считаю статус в багзилле GCC. В багзилле GCC это подтвердили как баг - значит это баг GCC. Баги это по-вашему только те случаи, когда некое поведение компилятора не соответствует стандарту?

Что вы имеете в виду под частично совместимым? C89 - это не частично совместимый с Си язык, а его единственно верный, как уверяет комитет, вариант

То и имею в виду, что допустим вместо C89 выпустили бы "Extended-C", а Си оставили б в состоянии на момет выхода K&R. Это было бы существено лучше, это бы что-то радикально изменило?

Не нравится официальный стандарт - изобретите свой стандарт и сделайте там так, как вам нравится. Есть достаточно много ответвлений от языка Си.

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

Совсем-совсем все, т.е. 100% программ вдруг стали неработающими? Неработающими с точки зрения стандарта C89? А с точки зрения K&R они точно все были работающими? В K&R например написано, что бит в байте может быть 8, а может быть больше чем 8 - если какая-то программа полагается на то, что байт 8-битный, она может не заработать на архитектуре с 9-битным байтом. Или наоборот, если предполагается, что байт 9-битный, программа с 8-битным байтом может не работать корректно.

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

А какое это имеет отношение к "уровневости"? В Си кстати тоже размер int не указан, получается что он в вашей классификации язык высокого уровня? Вот в Java размер типа int как раз указан, выходит что Java это язык низкого уровня? https://docs.oracle.com/javase/specs/jls/se17/html/jls-4.html#jls-4.2 : The integral types are byte, short, int, and long, whose values are 8-bit, 16-bit, 32-bit and 64-bit signed two's-complement integers, respectively, and char, whose values are 16-bit unsigned integers representing UTF-16 code units (§3.1).

Причем сам Error стандарт Паскаля определяет гораздо мягче, чем Undefined Behavior у Си.

Это не отменяет того факта, что программы на языке Паскаль могут перестать работать из-за того, что тип Integer вдруг стал другим, и не вмещает в себя нужный диапазон. Так что же получается, "Паскаль должен умереть"?

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

Где в стандарте языка Си написано, что Си это язык низкого уровня?

Где в стандартах Си или Паскаля указано, что они компилируется в машинные коды?

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

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

А для языка низкого уровня не это безразлично? Писать интерпретаторы ассемблера незаконно? Что значит "для языка безразлично", у него какое-то свое мнение есть? Что вообще такое язык высокого и язык низкого уровня? Я язык Си не считаю языком низкого уровня, язык низкого уровня для меня это ассемблер, машинный код.

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

Но они это сделают поняным и задокументированным образом, а не "как оптимизатору в голову взбредёт", по идее, в этом разница?

Еще раз. Критерием "баг это или не баг" я считаю статус в багзилле GCC.

И что дальше? В самом начале вы писали:

Только вы определитесь, вы конкретный компилятор Си ругаете, ли сам Си?

Я продемонстрировал вам, что поведение компилятора до исправления "бага" и после не противоречит стандарту. Более того, компилятор продолжает порождать вызовы memcmp/memcpy/memset/memmove. По этой причине пример в статье вполне закономерный, и подобная оптимизация могла произойти в любом другом компиляторе.

Не нравится официальный стандарт - изобретите свой стандарт и сделайте там так, как вам нравится.

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

Совсем-совсем все, т.е. 100% программ вдруг стали неработающими? Неработающими с точки зрения стандарта C89? А с точки зрения K&R они точно все были работающими?

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

https://www.bell-labs.com/usr/dmr/www/cman.pdf

Там, к примеру, рукой создателя языка написано следующее:

C supports four fundamental types of objects: characters, integers, single-, and double-precision floating-point numbers.

Characters (declared, and hereinafter called, char) are chosen from the ASCII set; they occupy the rightmost seven bits of an 8-bit byte. It is also possible to interpret chars as signed, 2’s complement 8-bit numbers.

Integers (int) are represented in 16-bit 2’s complement notation

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

Вот в Java размер типа int как раз указан, выходит что Java это язык низкого уровня?

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

Это не отменяет того факта, что программы на языке Паскаль могут перестать работать из-за того, что тип Integer вдруг стал другим, и не вмещает в себя нужный диапазон. Так что же получается, "Паскаль должен умереть"?

Во-первых, как я уже сказал, Паскаль - это не Си, и такое поведение в нем всегда было определено как Error. Во-вторых, прочитайте внимательно тот текст стандарта Паскаля, который я процитировал. Описанное там поведение сильно отличается от того, что говорит о знаковом переполнении стандарт Си. Если реализация Паскаля соответствует пунктам 2 или 3, то тогда ваша программа в худшем случае аварийно завершит исполнение, в то время как аналогичная программа на Си может, согласно стандарту, сделать что-угодно. В случае, если реализация следует пункту 1, то тогда вам нужно обращаться к документации компилятора, где это поведение должно быть четко определено. Опять же, стандарт Си такое требование не накладывает.

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

Что такое достаточно эффективный код для фоннеймановских архитектур? И при чем тут упомянутые вами в предыдущем сообщении абстракции?

Что вообще такое язык высокого и язык низкого уровня?

Хорошо, я уточню используемую мною терминологию.

Когда я говорю, что язык Си - это язык высокого уровня, я имею в виду то, что он абстрагирован от конкретной реализации компилятора и компьютерной архитектуры. Как говорит сам стандарт, его описание содержит "unambiguous and machine-independent definition of the language C".

Когда же я говорю, что Си - это язык низкого уровня, я подразумеваю, что его описание напрямую зависит от реализации компилятора и особенностей компьютера PDP-11. Как пишет сам Ритчи в упомянутом ранее мануале:

This paper is a manual only for the C language itself as implemented on the PDP-11.

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

Писать интерпретаторы ассемблера незаконно?

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

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

Вы, к вашему счастью, не считаете, а огромное количество других программистов (в том числе и создатель языка, если верить написанному им же мануалу) считают или считали иначе. Статья написана именно для таких людей с целью предостеречь их от ошибок. Если мыслить о языке Си в терминах его реализации, предполагать, какой код сгенерирует компилятор, то тогда он действительно становится неотличим по семантике от того же ассемблера. При этом до 89-ого года писать на нем код каким-либо другим образом было невозможно - четкого, абстрагированного от конкретной архитектуры описания не существовало. За 20 лет жизни языка появилось огромное количество программ, которые в момент выхода стандарта оказались формально нерабочими. Причем из-за особенностей текста стандарта (в том числе благодаря постоянному использованию неопределенного поведения) это произошло практически незаметно. Разгребать последствия этих решений мы будем еще очень долго.

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

https://www.bell-labs.com/usr/dmr/www/cman.pdf

Там, к примеру, рукой создателя языка написано следующее: ...

Это очень похоже на текст стандарта?

Может и похоже чем-то, но нет, текстом стандарта это не является. Например вот фрагмент

6.1 Characters and integers

A char object may be used anywhere an int may be. In all cases the char is converted to an int by propagating its sign through the upper 8 bits of the resultant integer. This is consistent with the two’s complement representation used for both characters and integers. (However, the sign-propagation feature disappears in other implementations.)

В каких-то реализациях нет sign propagation при переводе из char в int? Очень интересный "стандарт". А про поведении при делении на ноль там что-нибудь вообще сказано в этом "стандарте"? Вот в K&R первой редакции https://ia801303.us.archive.org/1/items/TheCProgrammingLanguageFirstEdition/The%20C%20Programming%20Language%20First%20Edition%20%5BUA-07%5D.pdf про это сказано вот что:

The handling of overflow and divide check in expression evaluation is machine-dependent. All existing implementations of C ignore integer overflows; treatment of division by 0, and all floating-point exceptions, varies between machines, and is usually adjustable by a library function.

Machine-dependend уже предполагает, что есть не только лишь PDP-11. В K&R даже в первой редакции написано, что число бит в байте может быть разным, и тут уже явно упомянут не только PDP-11:

Во-первых, как я уже сказал, Паскаль - это не Си, и такое поведение в нем всегда было определено как Error. Во-вторых, прочитайте внимательно тот текст стандарта Паскаля, который я процитировал. Описанное там поведение сильно отличается от того, что говорит о знаковом переполнении стандарт Си. Если реализация Паскаля соответствует пунктам 2 или 3, то тогда ваша программа в худшем случае аварийно завершит исполнение, в то время как аналогичная программа на Си может, согласно стандарту, сделать что-угодно.

И что? Программы-то могут перестать работать, если в одной реализации программа работает нормально, а в другой получается Error? Вы жалуетесь, что программы на Си перестали из-за плохого стандарта работать, сломались работавшие когда-то сишные программы. Так вот, паскаль так тоже умеет. В одном компиляторе паскаля Integer один, и всё работает, в другом компиляторе паскаля Integer другой, и все сломалось. Паскаль должен умереть?

Что такое достаточно эффективный код для фоннеймановских архитектур? И при чем тут упомянутые вами в предыдущем сообщении абстракции?

Что касается Си и Паскаля, то они достаточно легко ложится на фон-неймановские архитектуры, можно код функций перевести в код на ассемблере, при вызове функций, адрес возврата (место, откуда функция была вызвана) записывается в стек. Функции в процессе работы выделяют себе место в стеке под локальные переменные, есть передача аргументов в эти функции, для этого есть определенные соглашения вызовов, в адресном пространстве процесса еще есть область для глобальных и статических переменных. И эти языки явно делали с оглядкой на то, чтобы их было удобно компилировать. А некоторые языки на это так просто не натягиваются, например Prolog. И язык Prolog работает на другом уровне абстракций. Так понятней?

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

Его еще в первой редакции "The C programming language" в 1978 году начали от PDP-11 отвязывать, разрешив разное количество бит под байт, и упомянув архитектуры, отличные от PDP-11. Уже тогда Си должен был умереть?

 И кстати пароли в открыдом виде не хранят, а хранят их хэши.

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

Здравствуйте

Не вдохновляющая статья для новичков. Хоть и полезная.

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

Смотрю курсы для новичков по С для STM32 на Udemy, потому что Ардуино без отладки очень неудобный, а тут такое ) Хочу потом еще попробовать Unreal Engine c С++ для визуализаций.

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

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

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

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

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

Смотрел интервью по питон у айтибороды. Говорили, что медленный и с многопотоком беда.

Я для распберри пи немного пробовал его. Не очень зашел синтаксис. Очень не хватает классического for(;;), i++ и т п.

Ну, и вряд ли вы от же распространный микроконтроллер STM32 (интересно сделать дома автоматизацию) будете программировать на питоне с возможностью отладки.

Игровые движки что-то тоже пишут на С++ и C#.

Хотя в планах питон есть. В тех же 3д редакторах много чего пишется именно на нем.

Про тайпскрипт, гоу и раст ничего не знаю. Почитаю. Спасибо за совет.

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

Спасибо.

"Ну и у вас немного каша в голове от всех этих "мнений экспертов"

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

"Вообще, если нет проблем с английским, то я бы с какого-нибудь https://teachyourselfcs.com/ (перевод самого гайда) начал разбираться, а не с того, что на ютубе рассказывает какая-то борода."

Я на Udemy смотрю курсы по STM32 на англ от индуса. Все понятно.

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

int *p = malloc(64 * sizeof(int));

int *q = malloc(64 * sizeof(int));

if(p < q) /* Undefined behaviour! */ do_something();

Меня интересует этот случай.
Если p и q преобразовать в size_t после получения памяти, и делать сравнение. То оно корректно будет? (у меня в коде, есть такая же конструкция, где я определяю смещение данных. И по всем тестам, какие делал, всё работает правильно.) Может ли случиться "аномалия" при таком определении смещения данных?

Да, может, если модель памяти отличается от плоской (в таком случае ваш указатель внутрь size_t просто не влезет). Если уж приводить к числу — надо приводить к uintptr_t (но его на платформе может и не быть).

Это не C должен умереть, а gcc с его volatile, const, -O

Публикации

Истории