nabbla (nabbla1) wrote,
nabbla
nabbla1

Categories:

Холивор про Си и надёжность :)

Вот чем мне нравится ЖЖшная тусовка - выложив разжигающий пост о выборах в США, получил холивор, но не про выборы и не про политику, а про язык C/C++, микроконтроллеры и всё с этим связанное! А это всяко интереснее :)

Подкину что ль ещё дровишек, расскажу свои мысли по поводу. Сугубое ИМХО.

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

Картинка для привлечения внимания, "зловещая долина автоматизации". Видел что-то подобное в нескольких местах, но сходу найти, где именно, не смог, нарисовал сам в экселе :)

(цифры очень условные, главное тут форма кривой)

Пока возился со своим процессором, одолжил книжку у друга:

Думал, может сейчас почитаю "отцов-основателей" - и пойму, как это дело употреблять. Наверное, я понял, как он возник и почему обрёл такую популярность, но только укрепился в своём мнении, что не хочу писать на C...


На 18-й странице данной книжки мы узнаём, что все "классические" типы данных в С не имеют фиксированного размера: int может быть 16-битным или 32-битным, а может и какого-то другого размера, long должен быть не менее 32 бит, но точный размер неизвестен, float ОБЫЧНО представляется 32 битами, и так далее - всё определяется машиной, под которую это дело компилируешь!

Затем, когда заходит речь об арифметике, оказывается, что и она СТРОГО НЕ ОПРЕДЕЛЕНА! Про сложение, вычитание и умножение просто не сказали ни слова, как будто это "и так очевидно", а вот о делении и взятии остатка от деления даже обмолвились, что для отрицательных чисел результат "зависит от машины", где-то получится -1/2 = 0, а где-то -1. А с остатком вообще чудеса тогда могут твориться, когда он может и в минус уходить.

Но и сложение, вычитание и умножение не так просты. В ассемблере чётко объясняется, какие флаги "зажигаются" при переносе, переполнении и пр. - и можешь сразу (и весьма компактным кодом, используя JC / JNC / JO / JNO) решить, что в таких ситуациях делать. В С эту функциональность практически убили, сказав: проверять флаги - это слишком низкий уровень, но при этом не рассказывая, как именно должна работать арифметика. Если какой-то процессор работает с "насыщением" - это его дело. Без насыщения, просто с потерей бита переноса - тоже можно. А может и прерывание какое вызвать, ты должен об этом знать и написать обработчик :) С умножением всё время в сомнениях, сохранит ли он старшие 32 бита при умножении двух 32-битных чисел, или потеряет по пути?

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

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

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

Такой произвол в правилах языка как бы означал: типы данных используются те, что есть в процессоре, арифметические операции - те, что в процессоре, компилятор более-менее простой (превращает "+" в ADD, "-" в SUB, что бы это ни значило!), а когда программу пишешь под конкретную архитектуру - посмотри, как там "под капотом" - и станет ясно, как будет воспринят твой код.

А ещё есть злополучный endianness - сколько он крови всем попортил!

Получается, язык С нельзя назвать таким уж "переносимым" - это вовсе не гарантируется! Вон, java переносима как раз за счёт "виртуальной машины", чьё поведение чётко прописано, а C "почти" переносим - чаще всего придётся постучать бубном, прежде чем оно нормально запустится

И наверное, это действительно было бы хорошим компромиссным решением - всяко лучше, чем под каждую архитектуру писать с нуля на ассемблере, и при этом куда шустрее всяких java/.Net/питонов.

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

Но если начать автоматизировать ещё сильнее, так что ПРИ НОРМАЛЬНОМ ХОДЕ ВЕЩЕЙ пилоту достаточно будет нажать на кнопку "автопилот", после чего откинуться на спинку кресла и КОНТРОЛИРОВАТЬ ЕГО РАБОТУ - начинаются проблемы. Тяжело человеку наблюдать за процессами, которые, скажем, правильны на 99,9%. Мозг понимает всю бессмысленность этого дела - и начинает засыпать. А когда вдруг всё пошло не так, табло вспыхивает красными лампочками - чувак очухивается - "ЧТО? ГДЕ?", начинает вспоминать, а как это вообще руками-то исправлять? А иногда до этого и не доходит, поскольку показания приборов противоречат друг другу (не противоречили бы - автопилот и сам бы справился), и если бы пилот был бдительным и наблюдал, что творится, может и получилось бы восстановить картину, да вот уже поздно, и времени осталось 15 секунд, после чего ситуация уже не восстановится.

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

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

Языки совсем высокого уровня соответствуют самой правой точке - там попросту НИЧЕГО НЕ МОЖЕТ ПОЙТИ НЕ ТАК при переходе на другую платформу.

А С/С++ как раз плавает где-то в "зловещей долине". Программы ПОЧТИ портируются, т.е берёшь, компилируешь её, запускаешь - ух ты работает! Хм, какие-то глюки, ой, зависла. А кто её знает, что с ней. И если тебе кровь из носу нужно, чтобы оно заработало БЕЗУКОРИЗНЕННО - вот тут оказываешься в ситуации того пилота. Весь этот ворох библиотек, которые замышлялись как "поставил - оно работает" - оказывается, могут работать неправильно конкретно здесь. И вдруг нужно во всём этом нагромождении найти ошибку, а она найдётся только когда изучишь тот процессор, под который это пытался запустить, и изучишь САМИ БИБЛИОТЕКИ, т.е здесь 500-страничный талмуд на процессор, и десятки, сотни тысяч строк библиотек, да уже на миллионы счёт пошёл, даже в том же ядре линукса. На самом деле, если на такое налететь - ситуация попросту безнадёжная. Создателям библиотек, если они ещё поддерживаются, напишут багрепорт, а пока они ковыряются, придётся тупо найти чего-то другое и надеяться, что оно заработает нормально.

Я рассказывал как-то про доставшийся в наследство драндулет - Модуль Коррекции Угловых Скоростей, который вроде работает, но иногда наглухо зависает, так что спасает только перетыкание питания. По счастью, ошибку удалось найти: там при информационном обмене по RS485 придумали свой формат пакетов, где в одном из байтов содержался размер сообщения, от 1 до 256. Вот этот байт принимался в переменную типа char, а затем преобразовывался в uint32_t (возможно, в несколько этапов). Оказалось, что char в данном компиляторе по умолчанию ЗНАКОВЫЙ, и сообщения длиннее 127 байт воспринимались как имеющие размер -128..-1, но затем ЗНАК РАСШИРЯЛСЯ ДО 32 БИТ, и лишь затем оно запихивалось в БЕЗЗНАКОВОЕ ЧИСЛО, что означало - принять нужно сообщение длиной в 4 ГБ. Точнее, он считал, что сообщение уже принято и сидит в памяти, а теперь нужно проверить CRC. Данный процесс мало чем отличался от зависания... Самое смешное, нормальные сообщения не были столь большими (не более 32 байт), поэтому глюк проявлялся лишь при неправильном приёме, и чувак, возившийся с этим, так и не смог это отловить. Мне просто повезло, что я применил самый дешевый USB-RS485 конвертор за 200 рублей, который был немножко "шумнее" чем надо, так что эти зависания из таинственного явления, происходящего исключительно "где-то ещё, на испытаниях", но только не у тебя на столе, превратились в систему :)

Разумеется, ситуацию пытаются исправить. Теперь уже мало кто применяет классические int, long, short, вместо них пришли int32_t и иже с ними, а также size_t и много чего другого. Появилось куча всевозможных анализаторов кода, которые показывают "потенциально проблемные места", и в целом философия, мне кажется, сместилась.

Раньше использовать вещи, "зависящие от машины", было допустимо. Скажем, ты знаешь свою машину и пишешь конкретно для неё. Но людей от этого отучают, вводя особо маньячные оптимизаторы, для которых любой Undefined Behaviour - практически карт-бланш, позволяющий изуродовать код как угодно. Если раньше можно было сосчитать от 0 до максимального представимого числа примерно так:

int i = 0;
while (i+1 > i){
...
i++;
}


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

Но всё это приводит к тому, что теперь уже каждый программист рано или поздно пишет свой кривой-косой "виртуальный АЛУ", в котором он чётко задаёт, что происходит при переполнении, при переносе, при делении и взятии модуля, лишь бы только уйти от опасного undefined behaviour. То есть, то самое преимущество языка - компактность и эффективность написанных на нём программ - в итоге сходит на нет. И лучше было бы использовать язык, где это поведение задано ИЗНАЧАЛЬНО, чем изобретать велосипед раз за разом.

Решение идёт и с другого конца: ПОПРОСТУ ПЕРЕСТАЛИ ДЕЛАТЬ ЖЕЛЕЗО С ИНТЕРЕСНОЙ АРИФМЕТИКОЙ И РАЗРЯДНОСТЬЮ, потому как всё равно при компиляции сишных программ оно не даст выигрыша, а проблем может доставить вагон! Куда не глянь - все АЛУ одинаково бездарные. Не знаю, как по мне, целочисленная арифметика на обычных процессорах - это просто БОЛЬ, а когда её выполняешь на Си - боль вдвойне!

Мне довелось (совсем недолго) поковырять AVRку, без ардуин, а именно что "в голом железе". Там мне очень понравились операции умножения - их очень много, как раз на любой случай жизни. Есть "классическое", где 8 бит на 8 бит дают железные 16 бит результата. А есть "дробные", где как раз подразумевается формат 1.7, т.е числа от -1 до 0,992, либо беззнаковые от 0 до 1,992. И можно их "смешивать", т.к есть FMUL, есть FMULS и FMULSU. И где она теперь??? Microchip их поглотил, но даже на то, что выпускается, "накатывают" ардуину, из которой хрен доберёшься до аппаратного умножителя...

И ещё одно очень неприятное развитие событий - программистов начали учить НЕ ПИСАТЬ КОД САМОСТОЯТЕЛЬНО, ПОТОМУ ЧТО ЭТО СЛОЖНО И НЕ НАДЁЖНО! Теперь для каждого чиха надо использовать свою библиотеку, потому что её писали умные люди, а потом им прислали сотни этих самых багрепортов - и у них получился максимально портируемый и гибкий код. И снова здесь проблема - начинаешь терять квалификацию и веру в себя, и если необходимые библиотеки отсутствуют - всё, картина маслом "Приплыли". А даже если они есть, начинается весёлый Dependency hell, когда одна библиотека для работы требует ещё штук 10, а каждая из них - ещё по десятку, и почему-то простенькая программка вдруг растёт в размерах исполняемого файла до единиц и десятков мегабайт (если не больше), и почему-то никакой оптимизитор не способен это исправить. Опять же огромное ИМХО, но как будто народ так зациклился на оптимизации компилятора, что совсем забыл о линкерах и вообще о принципах построения программ, которые позволили бы максимально отсекать весь неиспользуемый код.

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

Конечно, всю вину сваливать на язык программирования нельзя, но это вот стремление что C, что C++ "усидеть на двух стульях", мне кажется, раз за разом приводит к выстрелу в ногу. Каждый раз им как будто бы удаётся абстрагироваться от "железа" (сначала от процессора, потом в C++ от непосредственной работы с указателями), но НЕ ДО КОНЦА, и это раз за разом попадает в зловещую долину, когда тебя призывают ЗАБЫТЬ О НИЖНЕМ УРОВНЕ (или вообще учат программистов и не объясняют, как всё это на самом деле работает, а лишь что надо написать, чтобы оно сделало что тебе надо), и ты забываешь, и оно работает ДО ПОРЫ ДО ВРЕМЕНИ, а когда что-то такое случается - понять, что творится, и исправить это куда сложнее, поскольку нужно знать не только работу железа, но и понимать логику компилятора - как он преобразует программу в код, и где он может протупить.

Когда аппаратной реализации позволено влиять на результат работы (как в С) - неизбежно приходится добавлять ещё один уровень абстракции, иногда в голове программиста. В С++, чтобы работать с классами, приходится очень далеко закапываться в указатели и работу с памятью, хотя казалось бы, ООП предполагал размышления на куда более "высоком уровне", а не из серии "тут у меня класс простенький, помещу его на стек, а там он полиморфный, его на стек нельзя, нужно сделать указатель, а потом не забыть его освободить." Как результат, и здесь добавляют 1-2 уровня абстракции: умные указатели, всякие std::move и прочие страшные вещи, и при этом каждый уровень абстракции несёт в себе возможность выстрела в ногу...


Вся надежда на то что, в эпоху "тёмного кремния", которая вовсю началась, да ещё с торговыми войнами, когда все на всех вводят санкции, нам неизбежно придётся всё это дело переосмыслить и, кто знает, прийти к более адекватным штукам. Но это уже тема для другого холивара :) Приятно временами языком почесать!
Tags: бред, программки, работа, странные девайсы
Subscribe

  • Нахождение двух самых отдалённых точек

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

  • Слишком общительный счётчик

    Вчера я чуть поторопился отсинтезировать проект,параметры не поменял: RomWidth = 8 вместо 7, RamWidth = 9 вместо 8, и ещё EnableByteAccess=1, чтобы…

  • Балансируем конвейер QuatCore

    В пятницу у нас всё замечательно сработало на симуляции, первые 16 миллисекунд полёт нормальный. А вот прошить весь проект на ПЛИС и попробовать "в…

  • Ковыряемся с сантехникой

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

  • Мартовское велосипедное

    Продолжаю кататься на работу и с работы на велосипеде, а также в РКК Энергию и на дачу. Хотя на две недели случился перерыв, очередная поломка,…

  • Обнаружение на новом GPU - первые 16 мс

    Закончилась симуляция. UFLO и OFLO ни разу не возникли, что не может не радовать. За это время мы дошли до строки 0x10F = 271. Поглядим дамп памяти:…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 66 comments

  • Нахождение двух самых отдалённых точек

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

  • Слишком общительный счётчик

    Вчера я чуть поторопился отсинтезировать проект,параметры не поменял: RomWidth = 8 вместо 7, RamWidth = 9 вместо 8, и ещё EnableByteAccess=1, чтобы…

  • Балансируем конвейер QuatCore

    В пятницу у нас всё замечательно сработало на симуляции, первые 16 миллисекунд полёт нормальный. А вот прошить весь проект на ПЛИС и попробовать "в…

  • Ковыряемся с сантехникой

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

  • Мартовское велосипедное

    Продолжаю кататься на работу и с работы на велосипеде, а также в РКК Энергию и на дачу. Хотя на две недели случился перерыв, очередная поломка,…

  • Обнаружение на новом GPU - первые 16 мс

    Закончилась симуляция. UFLO и OFLO ни разу не возникли, что не может не радовать. За это время мы дошли до строки 0x10F = 271. Поглядим дамп памяти:…