Несмотря на то что ассемблер избавляет от необходимости решать наименее творческие задачи программирования (вручную преобразовывать программу в машинный код), он не решает двух основных проблем, связанных с этим языком. Возможно, вы уже догадались, что первая проблема в том, что все действия с ассемблером могут быть крайне утомительными, поскольку вам приходится работать на уровне микропроцессора и беспокоиться о каждой мелочи.
Вторая проблема: написанные на языке ассемблера программы не являются переносимыми. Если вы пишете ассемблерную программу для микропроцессора Intel 8080, она не подойдет для микропроцессора Motorola 6800. Вам придется переписать ее на языке ассемблера 6800. Вероятно, это будет не так сложно по сравнению с написанием исходной программы, поскольку вы уже решили основные организационные и алгоритмические задачи. Однако это все равно потребует серьезных действий.
В предыдущей главе я говорил, что в современные микропроцессоры встроены машинные инструкции, выполняющие арифметические операции над числами с плавающей точкой. Это, безусловно, удобно, но это мало что дает. Предпочтительнее было бы полностью отказаться от машинно-зависимых инструкций, которые производят отдельные элементарные арифметические операции, а вместо этого выражать множество математических операций, используя проверенную временем алгебраическую форму записи. Например:
A × sin (2 × π + B) / C,
где A, B и C — числа, а число π равно 3,14159.
Почему бы и нет? Если такое выражение записано в текстовом файле, у вас должна быть возможность написать ассемблерную программу, которая читает этот текстовый файл и преобразует алгебраическое выражение в машинный код.
Если бы вам требовалось вычислить значение этого алгебраического выражения только один раз, вы могли бы сделать это вручную или с помощью калькулятора. Вероятно, вы собираетесь использовать компьютер, поскольку вам необходимо вычислить значение этого выражения при многих различных значениях A, B и C. По этой причине вы также должны предусмотреть для данного алгебраического выражения некоторый контекст, позволяющий вычислять его значение при разных коэффициентах.
То, к созданию чего вы приблизились, называется высокоуровневым языком программирования. Язык ассемблера считается низкоуровневым, поскольку взаимодействует непосредственно с аппаратным обеспечением. Несмотря на то что термин «язык высокого уровня» используется для описания любого языка программирования, отличного от языка ассемблера, некоторые языки считаются более высокоуровневыми по сравнению с другими. Если бы вы, будучи президентом компании, могли бы сесть за компьютер и ввести команду (еще лучше просто положить ноги на стол и продиктовать): «Рассчитать все прибыли и убытки за этот год, написать годовой отчет, распечатать несколько тысяч копий и разослать всем нашим акционерам», это бы означало, что вы работаете с языком очень высокого уровня! В реальном мире языки программирования даже не приближаются к идеалу.
Человеческие языки — это сотни и тысячи лет сложных взаимодействий, случайных изменений и приспособлений. Даже в основе таких искусственных языков, как эсперанто, лежит реальный язык. Однако компьютерные языки высокого уровня — результат более целенаправленной работы. Задача изобретения языка программирования интересна для некоторых людей, поскольку язык определяет то, как человек передает инструкции компьютеру. По оценкам, сделанным в 1993 году, с начала 1950-х были изобретены и внедрены более тысячи языков высокого уровня.
Конечно, недостаточно просто создать высокоуровневый язык (что подразумевает разработку синтаксиса, то есть набора правил для составления выражений). Вы обязательно должны написать компилятор — программу, которая преобразует инструкции вашего высокоуровневого языка в машинный код. Подобно ассемблеру, компилятор должен прочитать файл с исходным кодом символ за символом и разбить его на короткие слова, символы и цифры. Тем не менее компилятор намного сложнее, чем ассемблер. Относительная простота последнего обусловлена взаимно-однозначным соответствием между инструкциями на языке ассемблера и машинным кодом. Компилятору обычно приходится преобразовывать одну инструкцию, написанную на языке высокого уровня, во множество машинных. Компиляторы писать нелегко, о чем свидетельствуют множество книг, посвященных их разработке.
Языки высокого уровня имеют свои преимущества и недостатки. Основная ценность в том, что их обычно легче изучать и использовать, чем языки ассемблера. Программы, написанные на языках высокого уровня, часто получаются более понятными и краткими. Кроме того, такие языки в основном являются переносимыми, то есть не зависят от конкретного процессора, что позволяет программисту не учитывать базовую структуру компьютера, где будет работать программа. Разумеется, если хотите запустить программу более чем на одном процессоре, вам потребуются компиляторы, которые генерируют машинный код для этих процессоров. Исполняемые файлы по-прежнему будут специфическими для каждого из них.
Однако код, написанный хорошим ассемблерным программистом, почти всегда превосходит код, созданный компилятором. Это означает, что исполняемый файл, получившийся из программы, написанной на языке высокого уровня, будет более объемным и медленным по сравнению с функционально идентичной программой, написанной на языке ассемблера. (В последние годы это стало менее очевидным в связи с усложнением микропроцессоров и усовершенствованием компиляторов в плане оптимизации кода.)
Несмотря на то что язык высокого уровня может упростить работу с процессором, он не сделает его более мощным. С помощью ассемблера вы можете получить доступ ко всем функциям процессора. Поскольку высокоуровневый язык необходимо преобразовывать в машинный код, он может только сократить возможности процессора. Действительно, если высокоуровневый язык является переносимым, он не может использовать функции, характерные для определенных процессоров.
Например, многие процессоры предусматривают команды побитового сдвига. Как вы помните, эти команды сдвигают содержащиеся в аккумуляторе биты вправо или влево. Однако таких команд практически не существует ни в одном высокоуровневом языке программирования[32]. Если перед вами стоит задача, требующая использования побитового сдвига, придется имитировать его путем умножения или деления на 2. (Нельзя сказать, что это плохо: многие современные компиляторы используют команды побитового сдвига процессора для умножения или деления на степень двойки.) Кроме того, во многих языках не предусмотрены и булевы операции над битами[33].
На заре эры домашних компьютеров большинство прикладных программ были написаны на ассемблере. Однако в наши дни этот язык используется редко и только для решения особых задач[34]. По мере добавления в процессоры аппаратного обеспечения для конвейеризации — одновременного прогрессивного выполнения нескольких команд — язык ассемблера постоянно усложнялся. В то же время компиляторы становились все более интеллектуальными. Увеличение емкости диска и оперативной памяти современных компьютеров также сыграло свою роль: программистам больше не нужно создавать код, требующий небольшого объема памяти и умещающийся на небольшой дискете.
Несмотря на то что разработчики многих ранних компьютеров пытались формулировать для них задачи, используя алгебраическую форму записи, первым настоящим рабочим компилятором считается A-0 для компьютера UNIVAC, созданный в 1952 году Грейс Мюррей Хоппер (1906–1992) в корпорации Remington Rand. Доктор Хоппер пришла в компьютерную индустрию в 1944 году, когда присоединилась к команде Говарда Эйкена для работы над компьютером «Марк I». В свои восемьдесят с лишним лет она продолжала работать в этой сфере, занимаясь связями с общественностью в корпорации Digital Equipment Corporation (DEC).
Самый старый из используемых сегодня языков высокого уровня — ФОРТРАН (FORTRAN) (хотя за прошедшие годы он был многократно пересмотрен). Названия многих языков программирования пишутся прописными буквами, потому что являются акронимами. Название FORTRAN образовано от слов FORmula и TRANslation («трансляция формул»). Он был разработан в IBM для компьютеров серии 704 в середине 1950-х годов. На протяжении многих лет именно ФОРТРАНом пользовались ученые и инженеры. Он предусматривает обширную поддержку операций с плавающей точкой и работу с комплексными числами (которые, как я объяснил в предыдущей главе, представляют собой комбинации действительных и мнимых чисел).
У каждого языка программирования существуют сторонники и противники, которые могут горячо отстаивать свои предпочтения. В попытке занять нейтральную позицию при описании следующих концепций программирования я выбрал язык, который почти никто уже не использует, — АЛГОЛ (ALGOL — ALGOrithmic Language, алгоритмический язык; так же называется вторая по яркости звезда в созвездии Персея).
АЛГОЛ подходит для исследования природы высокоуровневых языков программирования, поскольку во многих отношениях это прямой предок многих популярных языков общего назначения, разработанных за последние 40 лет. Даже сегодня люди называют некоторые языки программирования языками типа АЛГОЛ.
Первая версия этого языка, АЛГОЛ 58, была разработана международным комитетом программистов в 1957 и 1958 годах. Два года спустя, в 1960-м, язык был доработан, а его пересмотренная версия получила название АЛГОЛ 60. Со временем появился АЛГОЛ 68, однако в этой главе я буду обращаться к версии, описанной в документе «Переработанное описание алгоритмического языка АЛГОЛ 60», который был завершен в 1962 году и впервые опубликован в 1963-м.
Давайте напишем краткий код на языке АЛГОЛ. Предположим, у нас есть компилятор под названием ALGOL.COM, который работает под управлением операционной системы CP/M или MS-DOS. Наша первая программа, написанная на языке АЛГОЛ, — текстовый файл с именем FIRST.ALG. Обратите внимание на тип файла ALG.