Не было гвоздя - подкова отпала.
Не было подковы - лошадь захромала.
Лошадь захромала - командир убит.
Конница разбита - армия бежит.
Враг вступает в город, пленных не щадя,
Оттого, что в кузнице не было гвоздя.
Самуил Маршак «Гвоздь и подкова»
Статья нашего подписчика о проблемах балансировки, связанной с выбором между удобными языками программирования и языками, дающими возможность идеальной оптимизации. Множество интересных и простых примеров, которые будут понятны всем. По-настоящему экспертное мнение. Настоятельно советую к прочтению всем подписчикам.
Автор статьи: Виктор Дымов
Многажды много раз возникали, возникают и будут возникать споры, какие языки лучше с «сахаром» или без. Чаще всего противопоставляются языки Си и Java/C++. Заканчиваются эти дискуссии чаще всего классическим «Ты — х*й! — Нет, ты!». Происходит это потому, что стороны рассуждают в двух разных системах постулатов, где есть один постулат, которого придерживаются сторонники «сахара». Постулат этот может звучать по разному, но чаще всего сводится к одному простому утверждению: «на языках с ''сахаром'' удобнее писать код». Слово «удобнее» можно заменить на «проще», «быстрее», «дешевле», «безопаснее» - общий смысл от этого не изменится. И это отчасти правда, на языках с «сахаром» проще начать писать код; но и на языке без «сахара» можно научится писать быстро и безопасно, однако это требует бОльших знаний. А противники «сахарных» языков исходят из другого постулата «программное обеспечение должно быть надёжным и эффективным». Казалось бы, спроси сторонника «сахара», должны ли программы быть надёжными и эффективными, и он тоже скажет твёрдое «Да!». Но на самом деле эти два постулата взаимнопротиворечивы и языки с «сахаром» при их комплексной оценке могут быть хуже и чаще всего бывают хуже, чем языки без «сахара». И далее приведено объяснение почему.
Полагаю необходимым уточнить, что речь пойдёт только о полных по Тьюрингу языках программирования, не являющихся трясинами Тьюринга.
В данном документе не будет предложена какая-то формула по количественной оценке качества языка, и, конечно, не будут оцениваться какие-то конкретные языки. Их названия приведены лишь в примерах. Но, полагаю, приведенные здесь тезисы будут небезынтересны тем, кто попробует вывести подобную формулу.
Оценка
На данном этапе в демонстрационных целях разделим оценку языка выделив две компоненты (1) лингвистическую и (2) стоимостную. На самом деле свойств у языков программирования много, но остальные нас сейчас не интересуют, поскольку они либо легко регулируемы без вмешательства непосредственно в язык, либо входят в состав уже выделенных двух компонент.
Лингвистическая компонента включает в себя и наличие «сахара», это удобочитаемость, скорость написания кода, краткость в выражении сложных конструкций. Но в тоже время в эту компоненту включаются и свойства противоречащие «сахару» — простота, единообразие, разумное (небольшое) количество термов и другие. Для общей оценки лингвистической компоненты языка, в которую входят грамматика, лексика, синтаксис стоит почитать монографию Ахо-Ульмана [1972, том 1, гл.1-3] (список отсылок в конце документа). Здесь углубляться во всё это не будем, так как разделение оценки языка программирования на компоненты это просто демонстрационный приём, и да — у языков с «сахаром» некая композитная лингвистическая компонента обычно лучше, чем у более простых языков без «сахара».
Но надо помнить, что злоупотребление «сахаром» в отдельных случаях может ухудшать оценку по лингвистической компоненте. Так, можно сделать два десятка ключевых слов для описания разных циклов, а программист их просто не запомнит и будет пользоваться двумя-тремя самыми простыми словами, реализовывая на них те циклы, которые автор языка реализовал под другими ключевыми словами. Наиболее яркий пример, когда программисты не пользуются словом foreach, а перебирают элементы массива обычным while. И если сделать очень большой и сложный язык с богатыми лексикой, грамматикой и синтаксисом, может оказаться так, что никто кроме его создателя этот язык и не знает в достаточной степени, а может и создатель не знает.
Для примера: стандарт Си — примерно 500 страниц, стандарт Си++ примерно 1600 страниц. Лучший учебник по Си Керниган-Ритчи [1988] это примерно 350 страниц, из которых только 230 учебник, остальное — справочник. После прочтения первой части Кернигана-Ритчи можно начать более или менее уверенно писать на Си. Учебник по Си++/Java/C# в среднем по объёму 1000 страниц и это только вводная информация. Раньше шутили, что Си++ хорошо не знает и Бьёрн Страуструп, создатель языка. Можно смело шутить, что хорошим знанием Java или C# не обладают даже их создатели и в этой шутке только доля шутки.
Вторая компонента оценки языка программирования — стоимостная — это тоже композит свойств языка программирования: надёжности и эффективности (временной и пространственной) выполнения одного и того же кода. А вот о ней мы поговорим подробнее. Заметьте, стоимостная компонента не включает в себя стоимость написания кода, а только стоимость его исполнения.
Есть такое понятие как вычислительная сложность алгоритма, более подробно у Кормена-Лейзерсона-Ривеста-Штайна [1990, гл.3] и во многих других работах. Не будем разделять сложности на пространственную и временную за исключением некоторых мест, где это будет указываться особо, а просто будем оперировать некоторым абстрактным показателем «сложность» подразумевающим и время выполнения кода, и требуемую память.
Масштабирование
Перейдём к проблеме масштабирования. С экономической точки зрения существует некая стоимость копирования продукции. Нельзя дёшево скопировать автомобиль, к примеру, или работу стоматолога, парикмахера. При всех оптимизациях нельзя выпускать больше N автомобилей с одного конвейера, а будучи стоматологом нельзя пролечить зубы более, чем N человек в день, и с парикмахером похожая история. Даже если парикмахер плохо стрижёт каждого клиента, то не сможет испортить причёски и настроение более, чем N людям в день. То есть, производство автомобилей или сверление зубов масштабируются плохо, а вот с программистом дело другое. Стоимость копирования программы не нулевая, но крайне близка к нолю. Программист может «осчастливить» своей некомпетентностью очень много людей одновременно, причём так, что они этого даже не заметят.
Рассмотрим два случая:
(1) Случай неинтерактивный
Программист пишет приложение, оно работает на серверах, никаких действий от человека не требует, а просто пережёвывает информацию и выдаёт какие-то результаты. Сервера стоят в небольшом датацентре, для простоты пусть в датацентре будет 1000 серверов. Сервера работают постоянно 24/7 и на них работает приложение программиста. Программист сэкономил час времени и не оптимизировал некую функцию. Для человека эта неоптимальность, в общем, незаметна, для простоты предположим, что функция выполняется за 0,7 секунды, а могла бы выполняться за 0,1 секунды. Программа размножена с околонулевой стоимостью на все сервера датацентра — это первый шаг масштабирования. Теперь второй шаг масштабирования — а как часто вызывается эта функция? Положим для простоты, что функция вызывается 6 раз в минуту. Тогда каждую минуту на 1000 серверов
1000 серверов x 0,6 сек. x 6 вызовов = 3600 сек. = 1 час
теряется 1 (один) час работы одного сервера. Можно посмотреть на это и по-другому:
0,6 сек. x 6 вызовов = 3,6 сек. потери на одном сервере за минуту, тогда
3,6 сек. / 60 сек. = 0,06 одного сервера работает только на обслуживание сэкономленного времени программиста. Серверов у нас 1000, так что 60 серверов заняты не делом, а обслуживанием экономии времени кодера. А сервера работают постоянно, при более или менее полной нагрузке, следовательно 60 серверов выкинуты в помойку, это примерно полторы 2-хметровых 19-тидюймовых стойки с 1U серверами. Но они не просто выкинуты в помойку, они занимают место, аренда которого оплачивается; они едят электричество, которое оплачивается; система охлаждения датацентра ест электричество, которое оплачивается; сами эти 60 серверов чего-то стоили и на их производство потрачены материалы. А в обычных датацентрах не по 1000 серверов стоит, а гораздо больше.
(2) Случай интерактивный
Программист пишет приложение, которое будет работать на компьютере или телефоне пользователя и требует его участия. Программист сэкономил час времени и не оптимизировал некую функцию. Для человека эта неоптимальность, в общем, незаметна, для простоты предположим, что функция выполняется за 0,7 секунды, а могла бы выполняться за 0,1 секунды. Мелочи. Но программа была размножена с околонулевой стоимостью, на множество устройств пользователей. Таким образом первый шаг масштабирования это тиражирование программы. То есть, если её использует 1000 пользователей, то за один запуск функции они теряют
0,6 сек. x 1000 пользователей = 600 сек. = 10 мин.
Теперь второй шаг масштабирования, а как часто вызывается эта функция? Для простоты предположим, что вызывается она 1 раз в минуту работы программы. То есть, каждую минуту из-за экономии программиста теряется 10 минут. Положим, это офисная программа, которая работает, ну пусть 4 часа каждый рабочий день, тогда
4 часа x 60 мин. x 10 мин. = 2400 мин. = 40 часов за рабочий день
Одна рабочая человеконеделя потеряна за один рабочий день. Несложно посчитать за неделю, месяц, год. За год выйдет примерно 1200 рабочих дней, то есть за год теряется 5 лет работы одного человека. Это цена одного часа сэкономленного программистом. Теперь забудем о человекочасах. И вспомним про электроэнергию потраченную на это время работы компьютера, про электроэнергию потраченную на охлаждение процессора компьютера, или про разряд аккумулятора телефона, если это мобильное приложение. Числа выходят неприятные.
Это примеры умозрительные, в мире не один программист, и не 1000 серверов, а гораздо больше. Эти примеры максимально упрощены, но несложно написать формулу потерь времени, памяти, серверов или человекочасов для каждого конкретного случая. Только вот числа туда надо подставлять, конечно, другие, в мире миллионы серверов, сотни миллионов компьютеров и миллиарды телефонов. И тысячи ленивых, экономных или некомпетентных программистов.
Исходя их этого, даже при грубых оценках, в мире есть десяток-другой электростанций, которые вырабатывают энергию просто для обслуживания такой вот «экономии» программистов, а ТЭС работают на газе, угле, торфе с соответствующими выбросами в атмосферу. Тонны металла добывается на корпуса компьютеров/телефонов, которые не будут делать ничего, кроме как обслуживать «экономию» и некомпетентность программистов. Люди потратят деньги на электричество, которое расходуется только потому, что программист «сэкономил» час. Придётся купить компьютер/телефон побыстрей или с большей памятью, потому что программисту было лень, он спешил или просто был некомпетентен, а старый компьютер/телефон окажется на свалке. Аккумулятор в телефоне умрёт быстрее и тоже окажется на свалке, а люди пойдут за новым аккумулятором/телефоном.
Основные тезисы: неоптимальность программы масштабируется очень сильно и очень быстро. Сэкономленный программистом час на оптимизацию превратится в эпохи машинного времени, века человекочасов, мегаватты электричества, тонны сырья, тонны отходов и кубические километры неполезных выбросов в атмосферу.
Оптимальность
Сторонник «сахара» тут же возразит: «Но это вопрос некомпетентности программиста. Мы компетентны. Мы пишем оптимальный код и быстрее, чем эти ваши языки без ''сахара''. И компиляторы его оптимизируют». Нет, и сейчас станет понятно почему.
Дело в том, что когда учёные оценивают сложность алгоритмов в омега-нотации это всё очень интересно, вот только в этих теоретических расчётах оперируют даже не порядком числа, а глубиной рекурсии описывающей рост затрат на выполнение функции. Одна операция там может быть сколь угодно большой или малой, считается количество константных операций, не оглядываясь на время или пространство их реального исполнения. Но, если заглянуть под капот любого современного компьютера, там есть центральный процессор вполне определённой архитектуры. Этот процессор знает не очень большое множество простых команд +, -, *, / и ряд других. Команды эти применяются к разным типам данных в памяти, преимущественно к машинным словам. Положим, что одна такая команда занимает один такт. Такие команды обычно близки к языку ассемблера. Этих команд в секунду выполняется очень много, ведь частоты процессоров сравнительно велики - 2*10^9 команд в секунду, 3*10^9, 4*10^9. Кроме того, есть память, но памяти сейчас тоже много 8, 16, 32, 64, 128 гигабайт. Это много. Вроде бы.
А теперь представим, что на простом языке программирования без «сахара» берётся значение по указателю, делается это обычно в один-два такта. Но это же опасно! А вдруг берётся значение вне пределов отведённой памяти. Поэтому в язык с «сахаром» добавляются смарт-пойнтеры или, например, массивы определённой длины. Но что это значит в тактах и по памяти? В тактах это значит, что прежде чем выполнить операцию взятия значения из памяти будет произведена проверка, а не вылетела ли программа за пределы массива. Значит будет потрачен такт на сравнение двух чисел. Откуда процессор возьмёт значение длины массива? А оно тоже в памяти и его надо тоже взять, следовательно теряем ещё пару тактов. А где же это значение хранится? Оно же хранится в памяти и занимает байта 4, а то и все восемь. И вроде бы, ну потрачено на взятие значения из массива несколько дополнительных тактов, ну, потрачено 4 байта памяти на значение длины массива, у нас же и тактов, и байт миллиарды... Да только курочка по зёрнышку всё поле склюёт. А сам язык с «сахаром» просто не позволяет вам обойти массив известной длины, там других массивов просто нет, и хотя обычно обойти-то можно, но это дополнительные траты времени программиста.
Тут ещё следует упомянуть о такой особенности современных процессоров как конвейерные вычисления. Не вдаваясь в подробности, можно сказать, что поток команд выполняется быстрее, если он не прерывается, а прервать его может многое, но в том числе условие — то самое сравнение длины массива с номером элемента. То есть, конвейер вычислений останавливается и ждёт каков будет результат сравнения, ведь от этого зависит какую команду выполнять следующей. Раньше это решалось «спекулятивным выполнением команд», но потом были обнаружены уязвимости Meltdown и Spectrе, и с тех пор спекулятивное выполнение либо совсем запрещено, либо применяется очень ограниченно.
А что происходит в языке без «сахара»? Да бери что хочешь и откуда хочешь, получишь Segmentation fault/Access violation, ну или просто зависание и крах системы, если программа работает не под ОС. И тут проявляется компетентность программиста. Если он понимает, что в данном блоке кода принципиально есть риск вылететь за пределы массива, то он сам поставит проверку длины, а если такой возможности нет, он волен и не делать такую проверку.
Начинается самое интересное: а сколько раз в некой средней по больнице программе берётся значение из массива? Сложно сказать, но очень часто, за ту же секунду в разные массивы можно сбегать и миллион раз, и тут лишние несколько тактов начинают масштабироваться. А сколько всего в программе массивов? А сколько программ запущено одновременно? Тут же масштабируются и те 4 байта, которые мы отвели на хранение длины массива.
Вот и получается, что язык с «сахаром» считает программиста тупицей, не способным написать надёжный и безопасный код, поэтому всячески его ограничивает как лингвистическими средствами, так и средой исполнения.
Выше рассмотрен лишь один пример с массивом определённой длины. На деле же «сахар» в языке требует не несколько тактов и не несколько байт, а на порядки больше. Посмотрим на среду исполнения «сахарного» языка. Чтобы обеспечивать быстроту написания кода и сам «сахар», в среде исполнения есть:
— исполнитель промежуточного кода/байткод-интерпретатор/виртуальная машина (программист не умеет писать кроссплатформенный код и ему нужна дополнительная прослойка),
— сборщик мусора (программист не умеет управлять памятью),
— маршаллинг (программист не умеет преобразовывать сложные типы),
— сериализация (программист не умеет передавать данные в потоке бит),
— смарт-пойнтеры/массивы определённой длины (программист не уверен в валидности своего кода и полагает, что может вылететь за пределы массива/памяти, а также может забыть затереть память),
— …
И все эти инструменты (и другие, тут не упомянутые) вроде бы полезные, и защищают от ошибок, но какой ценой? Программа по решению квадратного уравнения, прастихоспаде, жрёт памяти сколько ещё 30 лет назад в принципе не было на топовом компьютере и выполняется миллионы тактов. А зачем? А в скобках написано, какую компетенцию программиста какой инструмент заменяет. И получается как в анекдоте: «...А теперь мы попробуем со всей этой х*йнёй взлететь...».
Стоит отдельно упомянуть оптимизаторы компиляторов — это очень большая и сложная тема. Там предельно строгие ограничения прежде всего на неизменность результата и из-за этого некоторые методы оптимизации просто не применяются, особенно к значениям с плавающей запятой, но их можно реализовать вручную на языках без «сахара». Так что не ждите от компилятора большой оптимизации и ускорения, а вот замедление за счёт «сахара» ждите и оно будет. Подробнее об оптимизации у Ахо-Ульмана [1972, том 2, гл.11].
И вроде бы для одного пользователя это не очень-то и заметно. Хотя тот же сборщик мусора Java, завешивающий приложение в непредсказуемый момент стал притчей во языцех, некоторые производители ПО на Java даже делают специальный индикатор используемой памяти и отдельную кнопку «Запустить сборщик мусора», например, в IDE Eclipse. Да и перерасход ресурсов средами исполнения весьма ощутим даже невооруженным глазом. А если масштабировать задержки, перерасход памяти и время потраченное на обслуживание некомпетентности программиста с учётом всех экземпляров программы, то становится понятно как сэкономленные часы работы программиста быстро превращаются в годы, мегаватты и огромное количество ресурсов выброшенных впустую.
К чему это приводит?
(1) Ну, очевидно, программы пишутся быстрее, а исполняются медленнее. И вопрос не в компетенции программиста, ему язык (компилятор и среда исполнения) не даёт писать быстро и компактно исполняемый код, выполняя за него часть работы. Хочет он или не хочет, надо это или нет, а смарт-пойнтер память очистит.
(2) Порог входа в программирование понижен до уровня дебила. Это не преувеличение, под дебилом подразумевается человек, у которого диагностирована соответствующая степень олигофрении. Фактически ряд языков сейчас может использоваться абсолютно неподготовленным человеком знакомым только с математикой 7-ого класса. Такие языки, казалось бы, не должны применяться в промышленности, высоконагруженных системах или энтерпрайзе, но нет — применяются. По большому счету, если Дейкстре или Ершову показать, кто сегодня называется программистами, и не просто называется, но им ещё и платят за это, то их (Дейкстру и Ершова) обнял бы дед Кондратий прям на месте. Распространение компьютерной грамотности это хорошо, такого должно быть больше. Но посмотрим на вещи серьёзно: если человек умеет писать, это не значит, что он писатель, он просто грамотен. Если человек может составить программу, то он не программист, он просто владеет компьютерной грамотностью.
(3) Все расходы сопутствующие «сахару» и некомпетентности программистов легли на пользователей. И вроде бы спорщики в этих дискуссиях сами программисты. Но они-то тоже пользуются программами от других программистов. И со стороны защитников «сахара» получается какой-то стокгольмский синдром, они сочувствуют тем и поддерживают тех, кто им же и гадит.
Основные тезисы: языки с «сахаром» приводят к неоптимальному конечному коду или требуют среды исполнения, исправляющей ошибки и некомпетентность программиста. Все расходы на «сахар» несёт конечный пользователь программы.
А давайте сделаем хорошо!
Возникает логичный вопрос, а нельзя ли сделать такой язык, где и «сахар» был бы и эффективность была бы? Вот тут две компоненты оценки языка программирования, лингвистическую и стоимостную придётся объединить, потому что на самом деле введённое разделение было искусственным и они зависят друг от друга.
Создание языка программирования можно сравнить с балансированием на лезвии меча. С одной стороны вы хотите добавить новое ключевое слово, предположим foreach для цикла обходящего все элементы массива. Но вы прекрасно понимаете, что формально оно избыточно, этот обход можно реализовать уже существующими for/while. И описание foreach должно быть настолько общим, чтобы работать безошибочно в максимуме случаев, которые могут встретиться. И получается, что либо вы не делаете foreach, позволяя программисту самому реализовывать обход массива так как он сочтёт нужным; либо вы делаете foreach, понимая, что даже если вы сделаете наилучшую его реализацию, это будет общая реализация эффективная в среднем, но неэффективная для большинства частных случаев. В этом как раз проблема не только языков программирования вообще, но библиотек и фреймворков. Там могут быть хорошие реализации итераторов, различных деревьев, сортировок и прочих алгоритмов. Они могут быть настолько хороши, что плохой программист лучше и быстрее в жизни не напишет. Но проблема в том, что в каждом конкретном случае хороший компетентный программист понимает с какими данными программа будет работать и он может написать лучшую реализацию для частного случая.
К тому же абстракции текут и практика показывает, что если схватиться за объектно-ориентированный язык, то надо либо разбирать библиотечные объекты до базовых типов, либо смиряться с очевидной неэффективностью кода. Даже если вы возьмёте программу от весьма уважаемых софтверных компаний и загоните её в профайлер (например, Valgrind) он покажет вам тысячи ошибок. Это сэкономленные часы программистов и, как результат, библиотеки и фреймворки, использованные без понимания их внутренней работы. Особенно это становится заметно при использовании закрытых библиотек: программист может написать качественный код, но когда программа использует библиотечную функцию из закрытой библиотеки, то память потечёт, то с десяток nop (пустых операций) пройдёт.
В целом, это особенность современной архитектуры компьютеров и логики, на которой они построены. Но другой логики у нас сегодня нет и система команд процессора не позволяет вводить «сахар» в язык, сохраняя оптимальность генерируемого кода. По большому счету, нет и не предвидится лучшего оптимизатора, чем программист со знанием компилятора и целевой архитектуры процессора. Но таких всё меньше, к сожалению.
Таким образом получается, что чем эффективнее код вы хотите написать, тем ближе вам надо быть к командам процессора. На приложениях, где время критично часто можно увидеть ассемблерные вставки в код. Зачем их пишут? В этом месте компилятор не справился с оптимизацией и выплюнул неэффективный код. А программист, найдя это место в профайлере, понял как сделать лучше.
То есть, чем выше абстракции и чем проще писать код, тем генерируемый код менее эффективен, и наоборот. Чем для большего числа случаев алгоритм эффективен в среднем, тем менее он эффективен для большинства частных случаев. Создатели языков/библиотек ищут золотую середину, но серебряной пули, как и панацеи, нет и в рамках современной матлогики не будет. С современной матлогикой можно познакомится по монографии Клини [1952].
Примеры из практики:
При написании некритичных программ, программист на Си обычно для выделения памяти использует malloc() из stdlib. Но malloc() содержит свои подводные камни. Например, обычный запрос памяти у ОС не всегда эффективен, так как ОС может позже сбросить часть памяти в swap. ОС сама решает, что и когда сбросить в медленный swap, что не всегда правильно с точки зрения программиста. Хороший программист при написании критичного по времени приложения сам решает, какие данные можно сбросить в swap, а какие нет. Поэтому в ОС реального времени или в критичных по времени приложениях, часто можно увидеть перереализации malloc() чаще всего с константным объёмом памяти, используемым программой, а также перереализации swap'пирования данных, которые применяются только к некритичным для исполнения программы блокам данных. В некоторых ситуациях организация свободных и занятых блоков памяти может быть неудовлетворительна, а вне ОС и на некоторых экзотических архитектурах malloc() может отсутствовать вообще. Тогда программист сам реализует аллокатор, либо изменяя организацию блоков памяти, либо в целом делая аллокатор с ноля. Некоторые простые примеры распределителей памяти можно найти у Кернигана-Ритчи [1988, разделы 5.4 и 8.7]. Некоторые структуры данных, которые можно использовать для организации блоков памяти, описаны у Кормена-Лейзерсона-Ривеста-Штайна [1990, части III и V].
Таким образом, никакой язык программирования, даже с библиотеками и фреймворками, не может удовлетворить все возникающие потребности.
Основные тезисы: В условиях текущей архитектуры процессоров и в рамках текущей матлогики нет и не предвидится возможности создать быстрый язык программирования с «сахаром». Любой «сахар» будет иметь цену по времени или по памяти или по обоим, и эта цена будет масштабироваться. Расходы на «сахар» будут оплачены пользователями. Хорошие языки программирования хороши тем, что нашли баланс между удобством написания и эффективностью выполнения кода.
Австрийская экономическая школа
Это стоит упомянуть отдельно. Не вдаваясь в подробности, это школа, представители которой считают, что свободный рынок сам регулируется и приходит в состояние близкое к равновесному. Учитывая, что у всех коммерческих компаний (включая софтверные) одним из первых пунктов устава идёт пункт о том, что «компания создана для извлечения прибыли», представитель австрийской школы может абсолютно обоснованно сказать: «Нам плевать на масштабирование. Нам плевать сколько пользователь платит на нашу некомпетентность. Нам плевать сколько ресурсов расходуется впустую. Если у нас покупают, значит программа столько стоит. Не надо думать, что вы умнее участников рынка и потребителей». И, в общем, в его парадигме, он будет прав. Но, скажем так, идея свободного рынка и извлечения прибыли любой ценой сама по себе небесспорна; а с другой стороны, да — специалист действительно не умнее, но компетентнее среднего участника рынка. Поэтому в сложных отраслях экономики типа IT весьма много сомнительных личностей, которые обманывают как инвесторов, вытягивая деньги под пустые стартапы; так и пользователей впаривая им весьма... хм... странные по исполнению или целям работы программные продукты. Но обсуждение политэкономических вопросов выходит за рамки обсуждения языков программирования.
Автор статьи: Виктор Дымов
Библиография (книги для почитать):
Клини С.К. 1952. Введение в метаматематику * https://vk.com/doc387118601_548422727
Ахо А.В., Ульман Д.Д. 1972. Теория синтаксического анализа, перевода и компиляции * том 1 — https://vk.com/doc387118601_548422821 , том 2 — https://vk.com/doc387118601_548422860
Кормен Т.Х., Лейзерсон Ч.Э., Ривест Р.Л., Штайн К.С. 1990. Алгоритмы. Построение и анализ * https://vk.com/doc387118601_548422894
Керниган Б.В., Ритчи Д.М. 1988. Язык программирования Си, 2-ое издание * https://vk.com/doc387118601_548422964
Physics.Math.Code в контакте (VK)
Репетитор IT mentor в Instagram