Я открываю настоящую тему с целью поделиться своими размышлениями о том, каким мог бы быть новый язык программирования и компилятор к нему. Сразу оговорюсь, что какого-то целостного видения такого языка у меня нет, есть только замечания к существующим и некоторые мысли о том, что должен уметь делать язык высокого уровня, предназначенный для сложнейших научных расчётов. Также читатели должны понимать, что на мои взгляды накладывает большой отпечаток и специфика моей деятельности: я занимаюсь труднорешаемыми задачами (
NP,
#P) перечислительной комбинаторики с использованием высокопроизводительных параллельных вычислений (HPC). Эти задачи, как правило, требуют больших ресурсов как по числу процессоров, так и по объёму оперативной памяти. Можно сказать и проще: это самые трудные задачи из всех, которыми можно грузить компьютер, поэтому если новый язык будет удовлетворять моим требованиям, то он, скорее всего, подойдёт для реализации любых других задач, для которых сейчас используют, например, С++/Assembler. Долгое время я пользовался различными СКА (системами компьютерной алгебры типа Maple, Mathematica, Maxima) для своей работы, а также языками С++ и Assembler (yasm), что непременно наложит отпечаток на ход моих мыслей также. Например, мыслимый мною язык будет похож на СКА, при этом беря для себя лучшее от языков типа C++.
Моими основными задачами при разработке языка, которые прямо вытекают из моей научной деятельности, я вижу задачи следующие.
- Определение узких мест, концептуальных недоработок и прочих недостатков имеющихся инструментов для научных расчётов.
- Определение требований к тому, какими свойствами должен обладать инструмент на самом деле.
- Поиск способов осуществления требований в едином языке.
- Предложение конкретных синтаксических особенностей языка.
- Может что-то ещё, посмотрим…
Я собираюсь написать в этой теме серию постов, которые буду набирать по мере появления свободного времени (то есть весьма медленно). Можно сразу обсуждать, если хотите, дополнять, возражать и т. д.
! Сразу предупреждаю, что холиварить ни с кем не буду. Кому эти идеи помогут – буду рад, берите, не жалко, а кому не нравятся – аргументируйте, но данная тема настолько
личная для меня, что успешная аргументация может быть
только на мировоззренческом уровне. То есть, скорее всего, каждый останется при своём. Пишите свои идеи тоже, кому не жалко.
Для начала я буду рассказывать о некоторых принципиальных особенностях, которые должны быть в новом инструменте.
[ 1 ] :: Компилятор или интерпретатор?Речь
не пойдёт о поиске универсального способа трансляции, поскольку все мы знаем, что в каждом случае подходит свой способ: кому-то больше подойдет только компиляция, кому-то только интерпретация, а кому-то динамическая компиляция, ведь всё зависит от целей. Для моих целей, а, следовательно, для целей множества других людей, занимающихся требовательными к ресурсам научными расчётами, наиболее хорошим может стать некий компромисс между интерпретируемым и компилируемым инструментом. Выглядеть это должно примерно так: имеется некая среда разработки, напоминающая среду Maple, Mathematica или другие похожие СКА, пользователь вводит команды, а среда исполняет их, выдавая результат. Но все эти команды, вызываемые из ядра или библиотеки СКА, являются заранее скомпилированными под конкретное железо подпрограммами. Пользователь может по своему желанию превратить и набранную им в этой среде программу в бинарный код, который будет оптимизирован под его машину.
Таким образом, новый инструмент должен предоставлять как функции интерпретатора, так и компилятора в бинарный код. Теперь я поясню свою мысль более подробно, приводя (только короткие) обоснования из собственного опыта.
Проблема в том, что интерпретаторы работают медленно, так как полноценная оптимизация кода в чистом интерпретаторе невозможна. Бинарная реализация, даже не очень аккуратная, рвёт любую интерпретируемую СКА на части, если руки у программиста растут и плеч, конечно. Я не буду долго описывать конкретные случаи (их очень много), но могу с уверенностью заявить, что разница в эффективности порой достигает нескольких порядков. Так, мне пришлось переписать в бинарный код (на C++) функцию rgf_findrecur; из Maple, поскольку она умела выводить рекуррентные соотношения порядком («глубиной») лишь несколько сотен. Мне в то время нужно было найти соотношение порядком 14 с половиной тысяч, а впоследствии понадобилось вводить соотношения порядком почти 30 тысяч. Так вот, моя функция этим соотношением даже не подавилась, тогда как Maple уже не смог считать из файла исходные данные, по которым её нужно строить.
Почему? Всё просто, Maple - это интерпретатор. Пусть даже он вызывает примитивные функции своего уже откомпилированного ядра (например, длинную арифметику GMP), это не спасает его от необходимости высокий уровень абстракции переводить «на лету» в эти самые примитивные функции, поскольку все промежуточные функции, которые подключатся с помощью различных пакетов, также интерпретируются. Я же хочу, чтобы подключаемые модули были исключительно бинарными. Но почему бы тогда вообще не избавиться от интерпретатора и не писать всегда компилируемый код для нужных вычислений?
Компилятор для работы с набором вычислений тоже не очень хорошо подходит. Основная причина в том, что исследователь – не всегда профессиональный программист. Он совершенно не обязан владеть методами разработки полноценных программ и всеми тонкостями проектирования, отладки, профилировки и т. д и т. п.
С интерпретатором должно быть удобно работать в интерактивном диалоговом режиме. Представьте, мне нужно провести ряд расчётов, причём следующий мой шаг может зависеть (а может и не зависеть) от ответа системы на предыдущих шагах. Например, если некоторая функция вернула мне отрицательное число, я делаю для себя один вывод и веду исследование одним способом, если число положительное, я выполняю совершенно другой набор действий. Казалось бы, почему бы не написать тогда условие и не реализовать обе ветки? Потому что на самом деле нужна только одна! Нафига писать вторую (причём я заранее не знаю, которая сработает), если она никогда не понадобится? А если ветвлений не 1, а 10, то что, мне нужно реализовать 1024 куска? Пусть даже можно будет сократить это число до двух-трёх десятков, если заранее подумать, какие могут быть варианты. Но, повторюсь, исследователь - это не совсем программист. Он не обязан сидеть и разрабатывать проект своей программы, рассматривать и прописывать в коде все случаи, почти 100% из которых всё равно не осуществляется. Процесс научных расчётов напоминает скорее диалог с компьютером, и каждая фраза этого диалога, сказанная компьютеру, должна быть понята максимально эффективно. Затем учёный может сохранить результат диалога и отправить его своему коллеге, а тот сможет заново прогнать диалог на своей системе.
Если резюмировать, то от компилятора у СКА должна быть скорость работы бинарного кода его подключаемых модулей (библиотек) и его минимальная требовательность к памяти, от интерпретатора у СКА должно быть удобство диалогового режима общения и упрощённый процесс разработки несложных программ, когда отсутствует необходимость владения профессиональными навыками программирования. При этом возможность написать полноценный код, компилируемый в бинарный, должна оставаться для создания каких-то своих пакетов, с которыми затем можно будет работать в диалоговом режиме, ведь системой будут пользоваться и профессиональные программисты. В этом случае уже вступают в силу законы разработки ПО в своей полной силе, поэтому инструмент должен предоставлять функции редактора кода, отладчика, профилировщика и другие (кому какие нужны).
Ещё один, но слабый, аргумент в пользу интерпретатора. Это переносимость и документированность. Переносимость обеспечивается тем, что программу нельзя запустить без самой среды СКА, поэтому если среда на машине работает (то есть под машину удалось скомпилировать все библиотеки и интерфейс), то будет работать и любой написанный для неё код. При этом интерпретируемый код, особенно выводящий результаты выполнения операций сразу после их выполнения в тот же worksheet, во многом больше напоминает некий научный текст, который так же удобно читать, как научную статью. Там прослеживается ход мыслей автора, сразу виден результат выполнения того или иного шага.
[ 2 ] :: Принцип параллельностиТакая СКА должна быть сразу параллельной без всяких вариантов. Параллельность должна быть также естественно зашита в синтаксисе вызова команд, как естественно зашиты там операторы присваивания или сравнения. Каждая скомпилированная заранее функция, которую можно вызвать из командной строки интерпретатора, реализована в параллельном исполнении. У каждой функции есть описание, которое показывает, насколько хорошо функция масштабируется по результатам исследования её разработчика.
Разумеется, эти результаты не должны вводить в заблуждение пользователя, а должны лишь подсказывать, что ему делать, на сколько ядер запускать процесс для первой пробы, чтобы определить для себя золотую середину, если это потребуется.
Таким образом, любую функцию можно запустить с некоторым параметром, означающем число процессов или потоков (в зависимости от ситуации).
Различные функции и команды синхронизации также должны быть частью языка, как и прочие функции управления параллельными процессами и потоками. Представьте себе команды MPI или директивы OpenMP. Вот всё это должно наличествовать в языке не как надстройка, а как часть встроенной системы команд и операторов.
[ 3 ] :: Принцип отзывчивости и дружественностиЛюбой вызов функции не должен блокировать командную строку. Пользователь запустил функцию и может работать дальше, а функция крутится в фоновом режиме. Чаще всего происходит наоборот: ввёл команду и сиди жди или создавай новый worksheet и работай там. Но это мелочи по сравнению со следующей проблемой.
Одна из основных проблем взаимодействия между пользователем и СКА - отсутствие обратной связи, то есть информирования пользователя.
Выглядит это примерно так: пользователь ввёл команду, нажал Enter, и ждёт... Он совершенно не представляет, сколько ему ждать. Может час, а может неделю. Были случаи, когда приходилось ждать две недели, а потом дома выключали свет, или он просто «моргнул»
Поэтому нужен бесперебойник.
Это состояние неопределённости просто выбешивает. При работе с чрезвычайно затратными задачами наивно полагать, что всё происходит так же быстро, как в компьютерной игре. Расчёты ответа одной задачи могут занимать от суток до полугода.
Любой алгоритм, если он работает долго, выполняет какой-то набор шагов, которые условно можно разделить на ряд блоков, итераций или каких-то ещё структурных элементов. По запросу пользователя система просто обязана выдать позицию, на которой находятся вычисления. В описании вызываемой функции обязательно должно быть предусмотрено краткое описание алгоритма ровно на таком уровне, чтобы пользователь мог примерно прикинуть «ага, тут у нас ожидается миллион итераций какого-то цикла, почти все итерации выполняются с одной сложностью, хорошо», и наблюдать в реальном времени, на каком шаге сейчас находится алгоритм. Другой вариант – создавать так называемые
info_level. Чем выше это число при вызове функции, тем больше промежуточной информации выводит функция в процессе своей работы, то есть тем более детально она описывает обход определённых контрольных точек. Механизм создания таких контрольных точек должен быть зашит в язык как родной.
Далее, я сторонник той позиции, что командная строка должна быть доступна с наивысшим приоритетом. Ничто и никогда не должно как-либо отрезать пользователя от управления процессом. Даже если процессор занят на полную мощность, даже если на нём уже можно жарить сосиски, командная строка обязана послушно принять любую команду! Единственное, что может тут помешать - сбой аппаратуры. Ни одна СКА даже близко не отвечает этому требованию. Наиболее частая грабля, на которую я наступал, состояла в том, что система могла незаметно уйти в своп. Всё, можно сразу нажимать Reset (у Вас ведь ещё есть такая кнопка?), расчёты уже не спасти. В Maple при глубоком свопе никакие прерывания ОС просто не доходят до компьютера на протяжении нескольких часов.
--- Далее идёт лирическое отступление ---Почему Maple может незаметно уйти в своп? Потому что он совершенно неконтролируемо жрёт память. В моей практике были случаи, когда на пустом месте сжиралось два с половиной десятка гигабайт, тогда как если пытаться руководствоваться здравым смыслом, там больше двух с половиной гигов быть не может в принципе.
Простой пример: записать в массив числа от 1 до 2^20 в Maple 18. Сразу съедает 20 Мб. Не многовато? А если заполнить этот массив полиномами a[k]=1+x^k, то сожрано будет уже 250 Мб.
Даже если Maple делает «запас на будущее», процесс этот не контролируется никак. Когда он хапнет и сколько, предсказать невозможно. Дело в том, что внутренние структуры Maple, различные типы данных и способ их организации до ужаса неэффективны в плане экономии ресурсов системы. И проблема не только в интерпретации кода (хотя она кажется мне главной). Так, например, функция rgf_findrecur, упомянутая выше, которая выводит линейное рекуррентное соотношение по заданной последовательности чисел, помимо прочего решает систему линейных уравнений в целых числах. И всё бы ничего, но система эта
теплицева (т. е. матрица системы n x n состоит только из 2n-1 различных чисел, которые копируются в неё в особом и очень простом порядке, дублируя друг друга)! Это значит, что для хранения матрицы нужна линейная память, а не квадратичная, что в алгоритме никак не заложено. Алгоритм сначала послушно создаёт теплицеву матрицу из длинных чисел - это просто преступление. Отчасти это объясняет причину тормознутости функции, но лишь отчасти.
Подобных глупостей в системе чрезвычайно много. При всём при этом, указанная функция работает на порядки быстрее аналогичных функций, которые есть (если вообще есть) в других СКА. Это вообще удивительно. Но ещё более странным является то, что есть в Maple функция listtoratpoly которая по сути делает абсолютно то же самое, но вместо рекуррентного соотношения выводит рациональную производящую функцию (я уверяю читателя, что в данном контексте это одна и та же задача и по сути разница только в выводе, если не считать одной мелкой и быстрой операции, которая их отличает). Так вот, эта функция на порядок медленнее, чем первая. Почему? Для меня это осталось загадкой. Первая функция более ранняя и находится в пакете genfunc, вторая появилась позже в пакете gfun. Оба пакета призваны делать по сути одну и ту же работу, однако являются разными, и второй умеет делать больше всяких полезных штук, но и тормозит сильнее.
Такого не должно быть в СКА. Система команд должна быть продумана очень хорошо, а если это невозможно сделать сразу, должен быть продуман стандарт, по которому будет расширяться система команд, чтобы не получилось так, что когда один пакет настолько обрастает ошибками или настолько выходит из-под контроля, что пишут второй (третий, четвёртый), который делает по сути то же самое или чуть больше, а первый оставляют для совместимости со всем мусором, там образованным.
--- Конец лирического отступления ---Что касается интерфейса, то он должен быть быстрым. Мне не так важно, будет он красивым или нет. Он должен быть быстрым. В Maple взаимодействие с пользователем осуществляется через Java-морду. Она настолько безбожно тормозит, что порой хочется взять и выключить компьютер. В последних версиях (17, 18) тормоза стали меньше, и вроде бы даже команда типа «1+1» уже выполняется быстрее, чем за одну секунду (раньше приходилось ждать 2-3 секунды). Но тормоза остаются при работе с достаточно большими файлами, где много команд, особенно когда включается сборщик мусора. Я убеждён, что интерфейс должен быть максимально независимым от самих вычислений. Нельзя функции взаимодействия с пользователем хоть как-то объединять с вычислениями.
Можно сделать красивый и удобный интерфейс, но при условии, что он будет моментально отзываться на любую просьбу. Я даже «за» красивый интерфейс, в котором будет не просто голая командная строка, а разные информеры, сообщающие полезную информацию, подсказки, справки и т. д. Всё это можно удобно расположить по бокам монитора. Например, где-то же нужно отображать список запущенных в фоновом режиме команд и иметь кнопки их отключения, отображать контрольные точки восстановления, объём съеденной и оставшейся памяти и т. д.
[ 4 ] :: Принцип безопасности расчётовОчень обидно, когда в случае возникновения ошибки (в любом месте программы), вся промежуточная информация теряется. В язык должны быть зашиты простые средства работы с log-файлами для выяснения причины сбоя и отката на предыдущую удачно выполненную операцию.
Также очень часто требуется такой механизм, как создание точек восстановления. Представьте себе, что по техническим причинам работа программы должна быть прервана. В этом случае плохо терять месяц или два работы, если никак нельзя сохранить dump на диск, чтобы при следующем запуске программа начала ровно с того же места. Либо должен быть механизм транзакций, позволяющий создавать точки восстановления после каких-то определённых моментов. Но я проще всего вижу ситуацию следующим образом. Программа работает, пользователь вводит команду создания точки восстановления, создаётся такая точка, теперь программу можно отключить. При следующем запуске старт по желанию пользователя начинается с любой желаемой точки восстановления. Средства безопасности, перечисленные в этом абзаце, должны быть продуманы в синтаксисе и семантике языка. Понятно же, что организация подобных контрольных точек потребует в коде программы указывать места, где программа может безопасно остановиться, где данные потоков гарантированно синхронизированы и можно сливать свою копию на диск, временно блокируя все порождённые процессы и потоки.
[ Продолжение следует ]