Не надо иллюзий

Когда человек увлечен своим делом — это хорошо. Если при этом человек умеет еще и умные книжки писать — просто замечательно. Можно только позавидовать.

Но, как говорит французская пословица, nul miel sans fiel. Чего регулярно не хватает пишущим профессионалам — это самокритичности.

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

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

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

Но самое смешное начинается там, где творцы (и продавцы) программных продуктов начинают хвастать их невероятной производительностью. Когда вам пытаются впарить браузер, который "работает быстро даже при медленном Интернете", — это реклама законченного идиотизма. Козе понятно, что если у вас сеть не дает закачать гигабайт данных за две секунды — это никаким боком от браузера не зависит. Быстрее, чем полоса позволит, — не проскочить. Ну невозможно смотреть фильмы HD по телетайпу! А браузер что? — он, быть может, сам по себе, безо всякой сети, быстро работает, — только пользователю от этого ни холодно, ни жарко — ему качать надо.

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

Пропагандисты Erlang многократно повторяют, что виртуальная машина Erlang якобы способна поддерживать одновременное выполнение миллионов процессов — программистам на C такое не по зубам. Ну, считать зубы мы не будем... А вот оценить ресурсы, необходимые для большого количества процессов, мы можем. Каждый процесс требует, как минимум, места в физической оперативной памяти. Если его данные сваливаются в виртуальную память (то есть на жесткий диск), от производительности ничего не останется. Сколько-нибудь осмысленный код вряд ли займет меньше 16 килобайт — учитывая заголовочные структуры, тело процесса, стек, статические данные и динамическую память. На миллион процессов надо 16 гигабайт оперативной памяти. То есть, далеко не всякий сервер такое потянет. А если учесть еще и необходимость работы с жестким диском, и нагрузку на сети — заявленный параллелизм выглядит глупой рекламной сказочкой.

В чем тут собака? А дело в том, что процессы в Erlang не настоящие, а "легковесные". То есть, на самом деле выполняется только один процесс — виртуальная машина, которая подменяет собой операционную систему, принимая на себя ряд ее функций. То, что в Erlang громко называют процессами, — всего лишь объекты определенных классов, с которыми работает управляющая программа. Понятно, что инициализация объекта требует куда меньше работы, чем запуск нового процесса, — достаточно выделить память и передать управление конструктору. Для примитивных задач типа коммутации и пересылки сообщений объем собственных данных объекта невелик, порядка сотен байт. Это и позволяет без чрезмерного напряга разместить "параллельные процессы" в оперативной памяти. При этом неявно предполагается, что "процессы" большей частью однотипны — то есть, количество классов (а следовательно, и объем кода, который надо хранить в оперативной памяти) невелико. Время от времени управляющая программа обращается к тем или иным методам класса для обработки конкретного объекта, после чего "процесс" завершается или приостанавливается. То есть, речь идет не о параллельной работе нескольких процессов, а о последовательной обработке запросов одним-единственным процессом (который, в принципе, может быть исполняться в нескольких параллельных потоках, на нескольких процессорах или ядрах). Ограничения подобной схемы очевидны. Стоит только попробовать одновременно запустить хотя бы тысячу совершенно разных программ, требующих постоянного внимания, — и ни о какой производительности уже не будет и речи; начнутся проблемы с распределением ресурсов и ожиданием в очереди. Несравненный параллелизм Erlang оказывается при таком раскладе чистой воды жульничеством.

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

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

Поначалу, впрочем, даже на C писали со вставками в машинном коде, и даже появились синтаксические конструкции, поддерживающие обращение к процессору напрямую. Потом, по мере увеличения железной производительности, необходимость в столь суровой оптимизации отпала, и код стал полностью высокоуровневым. Непосредственно в кодах теперь не работают даже производители разного рода электронных устройств — у всех есть специализированные языки микропрограммирования. Как всегда, тон задает коммерция. Чем ниже уровень программирования — тем больше оно требует искусства. А с бизнесом искусство всегда было в напряженных отношениях. Бизнесу надо, чтобы не обязательно красиво — зато быстро, без лишних затрат, и ближе к продажам. Вот тут и расцвели всяческие надстроечные языки. Которые, в принципе, все равно, чем транслировать. Особая роль C (давно переросшего себя и превратившегося в C++/C# с многочисленными функциональными расширениями) сегодня не более чем традиция. С тем же успехом можно было бы писать компиляторы на фортране, а нынешние высокоуровневые хвалятся тем, что сами себя компилируют, — еще одна иллюзия. Есть процессор, который понимает только встроенный в него производителем набор команд. Архитектура всех существующих процессоров (кроме, быть может, человеческого мозга) принципиально одинакова, различие только в деталях. Из этого все следует. В частности, можно заранее предсказать, что никакой декларативный язык не может достичь эффективности языков императивных. Потому, что декларативный код — это всего лишь данные, и превратить их в нечто исполняемое может только другая программа — виртуальная машина, интерпретатор, компилятор... Но появление каких-то подготовительных стадий перед этапом исполнения — это дополнительное время и место. Никакая оптимизация тут не спасет. Вот когда научатся данные сами себя преобразовывать и пересылать — тогда и посмотрим, как оно сложится. А пока — не надо иллюзий.


[Компьютеры] [Наука] [Унизм]