Михаил Бобров дата публикации 19-09-2001 16:35 Несколько слов о теории и практике компиляции
Статья из серии "С точки зрения глупого ламера". Предлагалась в "КГ", но не
была напечатана.
Не так давно на форуме linux.org.ru среди прочих тем появилось вот какое
заявление:
"Некоторые пользователи Linux могут вернуться к Windows".
Статья,
обосновывающая данное утверждение, принадлежала МелкоМягкой Компании. Видимо,
поэтому народ на форуме даже не утруждал себя размышлениями и единогласно
заклеймил данное утверждение за происки и отрыв от.
Однако W-ME линия за последнее время стала несколько надежней, с чем
большинство народа соглашается. А Linux за то же самое время НЕ СТАЛ УДОБНЕЕ,
как не стал и дружественнее. Все чаще я ловлю себя на том, что не помню, где
именно мне искать ту или иную shared library; да и по-прежнему не слишком просто
его настраивать - несмотря на все заявленные графические рюшечки и ленточки.
Поэтому, дамы и господа пингвины, исход данного спора отнюдь не кажется мне
таким уж ясным. Билли сдаваться не собирается, чему подтверждением и служат
недавно вываленные на open source обвинения в якобы вирусной опасности
последних. Если Сообщество не отреагирует на развитие W- ME каким-либо образом,
то некоторые пользователи Linux действительно могут перейти на Windows.
Это была присказка. Сказка впереди, и заключается она в том, что все 233КВ
обсуждения вышеупомянутого вопроса на форуме linux.org.ru посвящены совсем
другому! То есть, один большой сплошной оффтопик с редкими вкраплениями вопиющих
об нарушении. Чему же тогда эти килобайты посвящены? Вечному вопросу о том,
какой язык программирования лучше, С++ или другой.
Так я наглядно убедился в следующих вещах:
- Отношение "сигнал-шум" в FIDO на порядок выше инетовского.
- Модератор - совершенно необходимая роль.
- Анонимность поощряет хамство.
- Спор о языках вечен.
И, раз уж зашла речь о языках, я подумал, что надо же решить хотя бы для
себя: а, действительно, который же из них лучше? А то попадешь в компанию, не
дай Бог, настоящих программистов, да и лопухнешься. Вот, собственно, мы и
подошли к заглавию статьи и скажем пару слов о компиляции.
Прежде всего, далее по тексту компиляция понимается как перевод входного
потока символов в выходной поток - команды процессора, т.е. в машинный код.
Теоретически, более широкое определение компиляции - замена символов входного
языка символами выходного языка на основании набора правил такой замены.
Теперь посмотрим на языки, которые джентльмены с linux.org.ru пытались
сравнивать между собой.
Помянутые языки четко разделяются на три группы:
- Языки с ярко выраженным функциональным подходом. Haskel, LISP, CLOS.
- Наследники ALGOLа С/С++, ADA и вся эта линия.
- Прочие языки, например, специализированные языки; в частности, схлестнулись двое товарищей по поводу управления атомным реактором.
Из приводившихся аргументов считаю достойными упоминания вот что:
- Язык плох, если он не поддержан библиотеками (bootstrap'ing боком вылез )
- Язык лучше своего конкурента, если он допускает включение конкурента как подмножество.
- Язык весьма сильно зависит от компилятора.
- Язык и решаемая на нем задача неразрывны.
Естественно, никакого кандидата в лучшие выдвинуто не было, да и не могло
быть выдвинуто. Сравнивая языки, джентльмены пытались возить грузовые контейнеры
на трековых болидах, буксировать трейлер за низкобюджетной малолитражкой, и
оценивать скорость машин по громкости издаваемого ими сигнала. Особенно меня
впечатлил пример, когда сравнивались скорости программ на .с и .срр, в каждом
цикле пишущих в системный stdout. На мой (исключительно непросвещенный) взгляд,
данный тест мог дать некоторое представление о загруженности потока вывода
конкретной системы в момент тестирования, но уж никак не об эффективности языка.
Однако критерий для сравнения должен был отыскаться, и мне хотелось, чтобы
этот критерий был наиболее объективным - при всей иллюзорности самого понятия
сравнения языков. Чтобы понять, что с чем сравнивать и какой результат сравнения
считать лучшим, а какой худшим, я попытался рассмотреть процесс компиляции и
исполнения программы вблизи. Способ выбрал тот, который показался мне самым
надежным - попытаться написать компилятор с некоего гипотетического языка "Х" на
некий гипотетический ассемблер некоей виртуальной машины "У". С последующим
переходом на вполне реальный ассемблер i586, как наиболее доступный для РС.
В процессе решения этой задачи я столкнулся поочередно со следующими
проблемами:
- Для каких задач вообще создается язык?
- Какие функции предоставить пользователю?
- Какой синтаксис наиболее удобен для данной конкретной задачи?
- Как представлять команды?
- Будет ли язык компилируемым или интерпретируемым?
- А почему именно таким?
- Какую команду исполнять следующей за текущей?
- Как осуществлять ветвления?
- Как представлять данные?
- Как найти требуемый элемент данных в памяти? (Какова должна быть форма адресации; структура хранения для быстрого поиска?)
- Как обеспечить поддержку структур средним объемом 80-120 МВ?
- Что делать, если команда или элемент данных не найдены?
- Как поддерживать возможность включения и отключения части команд\данных без остановки системы? (Важно для сетевых приложений и вообще для надежности).
Эти вопросы кажутся очевидными мне сейчас; однако на их формулировку и
постановку ушло немалое время. Естественно, изобретать велосипед не хотелось, и
я ознакомился с различными описаниями языков и операционных систем,
перечисленных ниже.
Языки: Pascal-Ada-Oberon-Modula-(2)(3), Object Pascal, Java, MUMPS (aka M),
LISP-Sheme-CLISP-Flavor-AUTOLISP, Haskel, Forth, APL, Eiffel. UML, XML, Perl,
Python, Tcl(for Tk), PHP (в весьма малой степени), HTML (куда уж без него),
Simula, Objective-C, Smalltalk. Язык скриптов для игр- квестов. Наконец, С++, о
котором мне известно больше всего.
OS: Unix-clones: Unix, Irix, QNX, Linux;
Others: OS\2, NextStep, BeOS, MSDOS, Win3.11.
Технологии: SOM, CORBA, Bonobo, WinAPI.
Возможно, эти описания не были исчерпывающими, и наверняка они не были очень
тонкими. Также в процессе поиска и прочтения попадались упоминания о других
языках, но, увы, без описаний. Наиболее удачными источниками информации,
оказались книги Грейди Буча "Объектно- ориентированное программирование" (далее
ссылка как 1) а также двухтомник Э.Хювенена и Й.Сеппянена "Введение в язык ЛИСП
и функциональное программирование".(2) В процессе поглощения всей этой
информации возникли выводы:
- Данные делятся ТОЛЬКО на две группы:
- Символьные (Символ - последовательность знаков, представляющая что-то из моделируемого мира - (2, стр 61) ) .
- Числовые. (Число - символ, представляющий самого себя (2, стр 63) )
- Любой подход не может быть равно эффективным в обработке чисел и символов.
- Свойство (атрибут) - одно из фундаментальных понятий мира, поскольку присутствует в семействе LISP и в семействе Smalltalk, а эти языки трудно назвать близкими. То же самое можно сказать о понятиях "объект" и "связь".
- Объем кода, скорость исполнения и суммарная эффективность системы зависят от того, насколько язык и процессор соответствуют друг другу. Говоря иначе, на символьном процессоре численные задачи будут выглядеть не лучше, чем на численном процессоре символьные задачи.
Однако, поскольку множество символов ВКЛЮЧАЕТ в себя числа как подмножество,
символьные процессоры (теоретически!) могут лучше работать с числами, чем
числовые процессоры с символами.
Отсюда довольно закономерное следствие: Операторные языки типа С со всеми
клонами, куда я отношу и Pascal- Modula-Ada-Oberon, развивающиеся в направлении
объектного моделирования мира, стремятся превратиться в символьные. "Объекты"
объектно- ориентированых (далее ОО) языков есть попытка отобразить символьную
обработку на числовые процессоры. Потому что числовые процессоры сейчас наиболее
представлены на рынке.
Докажем это утверждение.
Во-первых, посмотрим на набор структур данных, вошедших в Standart Template
Library языка С++. Это списки, вектора, хэш-таблицы, массивы, стеки, очереди.
Очевидно, включено наиболее необходимое и часто используемое. Практически любую
из этих структур можно реализовать на LISP'e со значительно меньшими затратами
нажатий клавиш, чем на том же С++, (кроме битовых наборов). А список вообще
является фундаментальным понятием ЛИСПа.
Во-вторых, рассмотрим итоговую структуру команд-данных, которая получается в
ОЗУ машины на момент исполнения. Ее пытаются создать ВСЕ ОО- языки, и для
описания ее же наперебой предлагают средства и фишки. (Кстати, именно за наличие
или отсутствие упомянутых средств\фишек дамы и господа программисты клеймят либо
возносят языки.) Согласно Грейди Бучу, структура описания задачи с точки зрения
ОО-дизайна, есть дерево с допущением множественного наследования. Язык ЛИСП
предполагает именно такую структуру данных в машинной памяти, только иначе
называет описываемые в ней вещи. Понятие "объект" в ОО-языках, почти входит в
определение ЛИСП- символа.
А в машинной памяти на момент исполнения с точки зрения компилятора и -
главное! - процессора, символ ЛИСП-языков и объект ОО-языков представлены
практически одинаково. Язык ЛИСП и его диалекты, ОО-языки и их диалекты в
конечном итоге создают одну и ту же структуру в ОЗУ. Даже с учетом, что
ЛИСП-процессоры все-таки чуствительно отличаются от обычных по архитектуре.
Древовидная структура цепочек команд и блоков данных, образовавшаяся в памяти
машины в результате применения любого из языков, выражена на языке команд
конкретного процессора. (Далее такой язык условно называется ассемблером).
Запомним факт одинаковости итоговых структур и перейдем к хранению
информации. Очевидно, что символ можно выразить как "набор чисел"+"правила
перевода" чисел в символ. Таков общепринятый способ хранения краткоживущих
цифровых данных на числовых процессорах. Если бы числа хранились как символы,
приходилось бы очень часто конвертировать их в числовую форму и обратно, что
вредно для обработки больших цифровых массивов. Поскольку моделирование мира
сейчас понимается исключительно через описание его множеством цифр, постольку у
нас главенствуют процессоры- цифродробилки и недружественный к человеку способ
хранения сэйвингов как сплошной цифровой массы.
Способ этот имеет два достоинства: цифры в большинстве случаев вкладываются в
стандартное количество символов, и правила их обработки просты. Поэтому правила
можно не хранить вместе с цифрами, а зашить в обработчик. Что позволяет
создавать быстрые переводчики пачки цифр в нужную форму путем изменения не всей
структуры данных, а одного только обработчика. Если точнее - путем изменения
набора правил интерпретации в обработчике. Недостаток же способа вот:
отсутствует гибкость в описании сложных объектов - простые правила плохо
справляются со сложными понятиями. Приходится брать массой, что иногда бывает
дорого даже для очень быстрых процессоров. Вот почему на сегодняшней технике
символьные языки типа LISP'a и (косвенно) Haskel'a, а также диалекты их,
проигрывали и будут проигрывать операторным языкам в тех местах, где требуется
большое количество цифр обмолотить за малое время.
Придуманный индустрией способ улучшить положение символьных языков - так
переформулировать задачу, чтобы проблемная область предстала бы не сплошной
массой данных, а некоторой УПОРЯДОЧЕННОЙ иерархией объектов, типа
BSP/PVS-структур в 3D-шутерах. Такие иерархии понятий символьные языки
обрабатывают лучше; сделать ошибку в иерархии также труднее, чем в сырой массе
цифр. Именно на создание иерархий, упорядочение и классификацию сырых масс
нацелены сейчас наиболее развитые ОО-языки, именно этому служит подавляющее
большинство нововведений в этих языках.
Откуда видно, что числодробильные и символьные языки, как правило,
пересекаются по решаемым задачам не на уровне кодирования, на котором их только
и пытаются сравнивать, а на том уровне, когда выбирают способ описания
проблемной области. Специальный язык тем и ценен, что позволяет описать
проблемную область как можно ближе к ней.
Предугаданный читателем вывод: Фортран и Кобол - специальные языки, Фортран в
математике, Кобол в экономике.
Несколько неожиданный вывод: С/С++ и операторные языки типа Pascal-
Ada-Oberon-Modula линии - также специальные языки, описывающие специальную же
область знаний - алгоритмическое представление данных. Отнесение их к
универсальным языкам есть (само)сужение мышления до понятия: "универсальность -
не более, чем хорошее представление алгоритмов".
LISP-Sheme и диалекты, Haskel и т.п. - специальные языки, описывающие область
знаний как набор вычислимых элементов, т.н. функций, атомов и списков.
Специальные языки разных областей неправомерно сравнивать между собой.
Поэтому вопрос, кто лучше, С++ или Haskel... продолжать?
Тогда что же такое универсальный язык? Универсальный язык - ассемблер. Он
единственный позволяет описать любую, подчеркиваю, любую, область (вычилимых в
принципе) данных максимально близко к тому процессору, который будет исполнять
работу. Вот почему (а не из пальцовки, как некоторые "настоящие програмисты"
говорят) весьма на пользу знание ассемблера: никогда не вредно удостовериться,
что тебя правильно поняли, что переводчик (компилятор/ интерпретатор/
транслятор/ линкер ) не переврал твои соображения в исполнимом коде.
Итак, мы возвращаемся к тому, с чего начали: компиляция есть перевод с одного
из специальных языков какой-то проблемной области на универсальный язык
ассемблера конкретной архитектуры, в которой и произойдет собственно исполнение.
Очевидно, наиболее эффективный код будет произведен из того специального языка,
примитивы и конструкции которого ближе всего к примитивам и конструкциям
ассемблера.
Откуда вывод: скорость и величина готового кода определяется тем, насколько
хорошо язык описания проблемной области ложится на архитектуру. Следуя этой
логике, лучшие языки/компиляторы те, кто генерирует маленькие быстродействующие
программки. Что и пытались доказать джентльмены на linux.org.ru, приводя примеры
кода и результаты компиляции и исполнения.
Но далеко не для всех задач важны скорость и малый объем. Некоторые задачи
должны быть решены независимо от количества затраченного времени и занятых
мегабайт. Наконец, малый объем кода далеко не показатель эффективности. Что
больше: броузер или Сеть, которой он оперирует? Ведь никому не нужна Сеть,
которую нельзя просмотреть, и никому не нужен броузер, которым нечего листать.
По исполняемым функциям Сеть+броузер - один программный продукт. И что же,
ценность данных в Сети как-либо зависит от броузера? И, наконец, эффективность -
только один критерий из 10 канонических показателей качества программы. По
значимости в общей оценке качества программы эффективность уступает и
корректности, и надежности и полезности. (2 том 2 стр 129: "Корректность,
надежность, полезность, эффективность, удобочитаемость, тестируемость,
отлаживаемость, сопровождаемость, переносимость, адаптируемость" - в порядке
источника.)
Учитывая вышесказаное, очень трудно написать гипотетический язык "Х" даже для
четко определенного реального процессора 586 семейства, а не то чтобы для
расплывчатой виртуальной машины "У". Большое количество рогаток и
взаимоисключающих выборов всплывает в процессе создания даже игрушечного,
учебного языка.
Для вышеупомянутой задачи по языку "Х" был принят следующий подход:
§ Область применения: язык позволяет описывать обычную пошаговую
стратегическую игру, не весьма графическую, но с некоторыми отличиями от
большинства. Должна быть возможность поддержки карт свыше 1000х1000, и
одновременного использования 50 000 юнитов, (по мере емкости железа), с
возможностью undo-отката, как в ACAD'e, например. (В натуре это, по-моему,
только "Казаки" могут, да и то не в таких масштабах.)
Задача переформулировалась примерно так: язык описывает сложные
многообъектные системы и должен предоставлять пользователю возможность выразить
то, что ему взбредет в голову с минимумом ограничений. Также без каких-либо
ограничений на создание\удаление объектов в процессе работы.
§ Язык является интерпретируемым. Это позволяет:
Во-первых, использовать некоторые части данных как команды и наоборот, что
облегчает поддержку генетических алгоритмов и мутаций.
Во-вторых, вычислять команды и получать физические адреса данных в процессе
исполнения. Систему, конечно, замедляет, но зато исключается вероятность
обрушения ее в результате разыменования несуществующего указателя. Для этой
задачи мне известно 4 способа решения:
- А. Физические адреса вычисляются ДО исполнения. В процессе исполнения к ним
обращаются без проверки их корректности. Экономит время и создает риск повалить
систему, если по указанному физическому адресу ничего нет. Самый быстрый из
известных способов. Малоприменим в условиях частого создания\удаления
объектов.
- Б. Способ А с созданием специального типа - "смарт-указателя". Этот указатель
призван служить прослойкой между объектом-клиентом и объектом- сервером, и
принимает на себя удар, если вдруг указуемый им объект-сервер несуществует или
временно недоступен. В наших (непрофессиональных) кругах стал известен после
распространения русского перевода книги Джеффа Элджера "С++ for real
programmers". Способы А,Б применяются в С/С++ модели.
- В. Создание сервера-таблицы существующих объектов. Иерархия объектов -
сортированный список или хэш-таблица. Каждый новый объект регистрируется на
сервере, где имени сопоставляется физический адрес. Клиент передает имя,
получает адрес объекта или адрес специального системного объекта,
обрабатывающего обращения к временно отключенным или еще неопределенным
объектам. Этот способ по философии очень отдаленно смахивает на CORBA.
- Г. Способ заключается в хранении символьного адреса объекта-параметра.
Иерархия объектов - древовидная. Каждый раз, когда объект нужен, адрес
разыменовывается, а полученный в реузльтате указатель проверяется на
корректность. Если указатель некорректен, выполняется следующая команда или
выполнение блока прерывается, по желанию пользователя. Это самый медленный
способ, но он позволяет:
- - не хранить никакой дополнительной информации о доступности объектов, (как в В)
- - не описывать то, чего на этапе компиляции сам еще не знаешь, (как требуется в А, Б)
- - иметь единый механизм защиты данных (проверка прав доступа того, кто пытается разыменовать любое имя),
- - не заводить специального механизма и жесткого формата регистрации разнотиповых объектов (как это требуется в В),
- - поддерживать выбор нескольких имен, указанных шаблоном (и делать это без просмотра всего списка имен),
- - отключать и включать объекты в любое время.
Во втором пункте выбор был главным образом, между В и Г. Победил способ Г,
перечислены его преимущества перед прочими четырьмя способами.
В-третьих, интерпретируемый язык позволяет одним объектам генерировать
команды для других, и тем самым облегчает поддержку иерархии принятия решений и
задания поведения для юнитов (игра все-таки).
§ Язык является символьным, но допускает возможность описания таких структур
данных, которые будут наиболее близкими к примитивным типам, то есть, допускает
возможность вставок на числодробительных языках и поддерживает необходимые типы
данных.
Потому что задача пошаговой игры с двумя подвохами сразу: описание юнитов, в
особенности, алгоритмов поведения, сложное, а описание карты - большое.
Символьные языки хорошо описывают сложные алгоритмы, но с трудом - большие
количества числовых данных по карте, тем более по сколько-нибудь серьезной, типа
1000х1000. Приходится уповать на то, что выигрыш от продуманного разбиения
модели игры на древовидный лабиринт данных превысит затраты по декодированию
символов в числа. Второе, на что остается надеяться - на применение скоростей
новых хардверных архитектур. Использование их взрывного нарастания мощности для
реализации экспериментальных (пока) принципов и логики построения языка мне
(субъективно, конечно) кажется куда более обоснованным, чем прямолинейное
наращивание быстродействия языков уже существующих.
Не знаю, будет ли иметь применение такой язык, если даже и появится, но
обдумывание вопросов его создания заставило меня понять довольно многое, не
всегда приятное и на первый взгляд совсем для любительского уровня неочевидное.
(Например, что С/С++ - не универсальные языки, хотя и близки к ним). И теперь
трудно мне высказываться о сравнении языков: слишком многое нужно принять во
внимание для корректности такого сопоставления. Ну и мнение о настоящести
програмистов по их высказываниям также было составлено. Соответствующее. Одним
из последствий этого мнения стало то, что я предлагаю этот текст в
"Королевство", а не на linux.org.ru - если я в чем-то неправ (наверняка, т.к.
специального программистского образования и опыта реальных проектов у меня нет),
то здесь мне это объяснят. А на LOR просто наорут.
Пишите, очень интересно, что думает All всеведущий по данному поводу.
Заранее извиняюсь за возможную задержку ответов.
Михаил Бобров.
PS Привет Uno, хоть и давно не виделись.
[Средства разработки. Языки программирования.]
Обсуждение материала [ 11-11-2005 15:12 ] 20 сообщений |