М. Бен-Ари - Языки программирования. Практический сравнительный анализ (2000) (1160781), страница 4
Текст из файла (страница 4)
Все языки, которые мы обсудили, имеют одну общую черту: базовый оператор в них — это оператор присваивания, который заставляет компьютер переместить данные из одного места в другое. В действительности это относительно низкий уровень абстракции по сравнению с уровнем проблем, которые мы хотим решать с помощью компьютера. Более новые языки скорее предназначены для того, чтобы описывать проблему и перекладывать на компьютер выяснение, как ее решить, чем для подробного определения, как перемещать данные.
Современные программные пакеты (software packages), как правило, представляют собой языки действительно высокого уровня абстракции. Генератор I Приложений позволяет вам описать последовательность экранов и структур базы данных и по этим описаниям автоматически генерирует команды, реализующие ваше приложение. Точно также электронные таблицы, настольные издательские системы, пакеты моделирования и другие системы имеют обширные средства абстрактного программирования. Недостаток программного обеспечения этого типа в том, что оно обычно ограничивается приложениями, которые можно легко запрограммировать. Их можно назвать параметризованными программами в том смысле, что, получая описания как параметры, пакет конфигурирует себя для выполнения нужной вам программы.
Другой подход к абстрактному программированию состоит в том, чтобы описывать вычисление, используя уравнения, функции, логические импликации или другие формализмы подобного рода. Благодаря математическим формализмам определенные таким образом языки оказываются действительно универсальными, выходящими за рамки конкретных прикладных областей. Компилятор реально не преобразует программу в машинные коды; скорее, он пытается решать математическую проблему и ее решение выдает в качестве результата. Так как абстракция оставляет индексы, указатели, циклы и т. п. вне языка, эти программы могут быть на порядок короче обычных программ. Основная проблема описательного программирования состоит в том, что «процедурные» задачи, например ввод-вывод на экран или диск, плохо «укладываются» в эту концепцию, и для этих целей языки должны быть дополнены обычными конструкциями программирования.
Мы обсудим два формализма непроцедурных языков: 1) функциональное программирование (гл. 16), которое основано на математическом понятии чистой функции, такой как sin и log, которые не изменяют своего окружения в отличие от так называемых функций обычного языка типа С, которые могут иметь побочные эффекты; 2) логическое программирование (гл. 17), в котором программы выражены как формулы математической логики и «компилятор», чтобы решить задачу, пытается вывести логические следствия из этих формул.
Должно быть очевидно, что программы на абстрактных, непроцедурных языках не могут оказаться столь же эффективными, как программы, закодированные вручную на С. Непроцедурным языкам следует отдавать предпочтение всякий раз, когда программная система должна осуществлять поиск в больших объемах информации или решать задачи, процесс решения которых не может быть точно описан. Примерами могут служить обработка текстов (перевод, проверка стиля), распознавание образов (видение, генетика) и оптимизация процессов (планирование). Поскольку методы реализации улучшаются и поскольку становится все сложнее разрабатывать надежные программные системы на обычных языках, области приложений непроцедурных языков будут расширяться.
Функциональные и логические языки программирования настоятельно рекомендуются как первые из изучаемых, для того чтобы студенты с самого начала учились работать на более высоких уровнях абстракции, чем при программировании на таких языках, как Pascal или С.
1.6. Стандартизация
Следует подчеркнуть значение стандартизации. Если для языка существует стандарт, и если компиляторы его поддерживают, то программы можно переносить с одного компьютера на другой. Когда вы пишете пакет программ, который должен выполняться на разных компьютерах, вы должны строго придерживаться стандарта. Иначе задача сопровождения чрезвычайно усложнится, потому что придется следить за десятками или сотнями машинно-зависимых вопросов.
Стандарты существуют (или находятся в стадии подготовки) для большинства языков, обсуждаемых в этой книге. К сожалению, обычно стандарт предлагается спустя годы после широкого распространения языка и должен сохранять машинно-зависимые странности ранних реализаций. Язык Ada — исключение в том смысле, что стандарты (1983 и 1995) создавались и оценивались одновременно с проектом языка и первоначальной реализацией. Более того, стандарт ориентирован на то, чтобы компиляторы можно было сравнивать по производительности и стоимости, а не только на соответствие стандарту. Компиляторы зачастую могут предупреждать вас, если вы использовали нестандартную конструкцию. Если необходимо использовать такие конструкции, их следует сконцентрировать в нескольких хорошо документированных модулях.
1.7. Архитектура компьютера
Поскольку мы рассматриваем языки программирования с точки зрения их практического использования, мы включаем короткий раздел по архитектуре компьютеров, чтобы согласовать минимальный набор терминов. Компьютер состоит из центрального процессора (ЦП) и памяти (рис. 1.1). Устройства ввода-вывода могут рассматриваться как частный случай памяти.
рис. 1 . 1 . Архитектура компьютера.
Все компоненты компьютера обычно подсоединяются к общей шине. Физически шина — это набор разъемов, соединенных параллельно; логически шина — это спецификация сигналов, которые дают возможность компонентам обмениваться данными. Как показано на рисунке, современные компьютеры могут иметь дополнительные прямые соединения между компонентами для повышения производительности (путем специализации интерфейса и расширения узких мест). С точки зрения программного обеспечения единственное различие состоит в скорости, с которой данные могут передаваться Между компонентами.
В ЦП находится набор регистров (специальных ячеек памяти), в которых выполняется вычисление. ЦП может выполнить любую хранящуюся в памяти команду; в ЦП есть указатель команды, который указывает на расположение очередной команды, которая будет выполняться. Команды разделены на Следующие классы.
• Доступ к памяти. Загрузить (load) содержимое слова памяти в регистр и сохранить (store) содержимое регистра в слове памяти.
• Арифметические команды типа сложить (add) и вычесть (sub). Эти действия выполняются над содержимым двух регистров (или иногда над содержимым регистра и содержимым слова памяти). Результат остается в регистре. Например, команда
add m,N R1,N
складывает содержимое слова памяти N с содержимым регистра R1 и оставляет результат в регистре.
• Сравнить и перейти. ЦП может сравнивать два значения, такие как содержимое двух регистров; в зависимости от результата (равно, больше, и т.д.) указатель команды изменяется, переходя к другой команде. Например:
jump_eq R1.L1
…
L1: ...
заставляет ЦП продолжать вычисление с команды с меткой L1, если со-держимое R1 — ноль; в противном случае вычисление продолжается со следующей команды.
Во многих компьютерах, называемых Компьютерами с Сокращенной Системой команд команд (RISC— Reduced Instruction Set Computers), имеются только такие элементарные команды.Обосновывается это тем, что ЦП, который должен выполнять всего несколько простых команд, может быть очень быстрым. В других компьютерах, известных как CISC (Complex Instruction Set Computers), определен Сложный Набор команд, позволяющий упростить как программирование на языке ассемблера, так и конструкцию компилятора. Обсуждение этих двух подходов выходит за рамки этой книги; у них достаточно много общего, так что выбор не будет иметь для нас существенного значения.
Память — это набор ячеек, в которых можно хранить данные. Каждая ячейка памяти, называемая словом памяти, имеет адрес, а каждое слово состоит из фиксированного числа битов, обычно из 16, 32 или 64 битов. Возможно, что Компьютер умеет загружать и сохранять 8-битовые байты или двойные слова из 64 битов.
Важно знать, какие способы адресации могут использоваться в команде. Самый простой способ — непосредственная адресация, при которой операндявляется частью команды. Значением операнда может быть адрес переменной, и в этом случае мы используем
нотацию С:
load R3, # 54 Загрузить значение 54 в R3 load
R2, &N Загрузить адрес N в R2
Следующий способ — это абсолютная адресация, в которой обычно используется символический адрес переменной:
load R3,54 Загрузить содержимое адреса 54
load R4, N Загрузить содержимое переменной N
Современные компьютеры широко используют индексные регистры. Индексные регистры не обязательно обособлены от регистров, используемых для вычислений; важно, что содержимое индексного регистра может использоваться для вычисления адреса операнда команды. Например:
load R3,54(R2) Загрузить содержимое addr(R2) + 54
load R4, (R1) Загрузить содержимое addr(R1) + О
где первая команда означает «загрузить в регистр R3 содержимое слова памяти, чей адрес получен, добавлением 54 к содержимому (индексного) регистра R2»; вторая команда — это частный случай, когда содержимое регистра R1 используется просто как адрес слова памяти, содержимое которого загружается в R4. Индексные регистры необходимы для эффективной реализации циклов и массивов.
Кэш и виртуальная память
Одна из самых трудных проблем, стоящих перед архитекторами компьютеров, — это приведение в соответствие производительности ЦП и пропускной способности памяти. Быстродействие ЦП настолько велико по сравнению со временем доступа к памяти, что память не успевает поставлять данные, чтобы обеспечить непрерывную работу процессора. Для этого есть две причины: 1) в компьютере всего несколько процессоров (обычно один), и в них можно использовать самую быструю, наиболее дорогую технологию, но объем памяти постоянно наращивается и технология должна быть менее дорогая; 2) скорости настолько высоки, что ограничивающим фактором является быстрота, с которой электрический сигнал распространяется по проводам между ЦП и памятью.
Решением проблемы является использование иерархии блоков памяти, как показано на рис. 1.2. Идея состоит в том, чтобы хранить неограниченное количество команд программы и данных в относительно медленной (и недорогой) памяти и загружать порции необходимых команд и данных в меньший объем быстрой (и дорогой) памяти. Если в качестве медленной памяти ис пользуется диск, а в качестве быстрой памяти — обычная оперативная память с произвольным
доступом (RAM — Random Access Memory), то концепция называется виртуальной памятью или страничной памятью. Если медленной памятью является RAM, а быстрой — RAM, реализованная по более быстрой технологии, то концепция называется кэш-памятью.
Обсуждение этих концепций выходит за рамки этой книги, но программист должен понимать потенциальное воздействие кэша или виртуальной памяти на программу, даже если функционирование этих блоков памяти обеспечивается компьютерными аппаратными средствами или операционной системой и полностью невидимо для программиста. Команды и данные передаются между медленной и быстрой памятью блоками, а не отдельными словами. Это означает, что исполнение последовательно расположенных команд без переходов, так же как и обработка, последовательно расположенных данных (например, просмотр элементов массива), должны быть намного эффективнее, чем исполнение групп команд с переходами и обращения к памяти в случайном порядке, что требует интенсивного обмена блоками информации между различными иерархическими уровнями памяти. Если вы пытаетесь улучшать эффективность программы, то следует противиться искушению писать куски на языках низшего уровня или ассемблере; вместо этого попытайтесь реорганизовать вычисление, приняв во внимание влияние кэша и виртуальной памяти. Перестановка операторов языка высокого уровня не воздействует на переносимость программы, хотя, конечно, улучшение эффективности может теряться при перенесении ее на компьютер с иной архитектурой.
1.8. Вычислимость
В 1930-х годах, еще до того, как были изобретены компьютеры, логики исследовали абстрактные концепции вычисления. Алан Тьюринг и .Алонзо Черч независимо предложили чрезвычайно простые модели вычисления (названные соответственно машинами Тьюринга и Лямбда-исчислением) и затем выдвинули следующее утверждение (известное как Тезис Черча —Тьюринга):
Любое исполнимое вычисление может быть выполнено на любой из этих моделей.