7.1 Важность быстрого вызова подпрограмм
==153
«Программистов научили избегать вызовов процедур и передачи параметров из соображений эффективности. Между тем, это очень важные инструменты проектирования хорошо организованных программ, а стековые архитектуры сохранили потенциал для чрезвычайно эффективного вызова процедур» ( Schulthess & Mumprecht 1977 , p. 25 ).
Сожаления о том, что затратные вызовы процедур приводят к плохо структурированным программам, сковывая программистов требованиями к эффективности, повторяются программными архитекторами и сторонниками как RISC, так и CISC машин ( Atkinson & McCreight 1987 , Ditzel & McLellan 1982 , Parnas 1972 , Sequin & Patterson 1982 , Wilkes 1982 ). И всё это в то время как Lampson ( 1982 ) успел заявить, что вызовы подпрограмм можно сделать столь же быстрыми, как безусловные переходы.
7.1.1 Важность небольших процедур
Использование большого числа маленьких функций при написании программ сокращает сложность каждого создаваемого, тестируемого, отлаживаемого и осмысливаемого программистом куска кода. Уменьшение сложности ведёт к уменьшению стоимости разработки и сопровождения и к увеличению надёжности. Почему же тогда программисты не используют небольшие процедуры?
==154
Большинство программ написано на универсальных языках, типа FORTRAN, COBOL, PL/1, Pascal и Си. Первые языки программирования высокого уровня - FORTRAN и ему подобные - были продолжением философии машин, на которых они запускались: последовательных регистровых машин фон-Неймановской архитектуры. Соответственно, областью их основного использования было выполнение длинных последовательностей операций присваивания с редкими вкраплениями условных переходов и вызовов подпрограмм.
За прошедшие годы сложность программ начала меняться. Принятый сейчас подход к конструированию программного обеспечения предполагает создание структурных проектов, разработанных по модульному принципу. Использование модулей - основа для распределения задач среди участников команды. При ближайшем рассмотрении, модули управляют сложностью, ограничивая объём информации, с которой приходится работать программисту в каждый конкретный момент времени.
Более совершенные языки Modula-2 и Ada созданы специально для продвижения модульного стиля разработки. Но единственным аппаратным нововведением, появившимся в результате увеличения популярности структурных языков, стал регистр-указатель стека. Он да несколько сложных инструкций ( которые не всегда используются компиляторами ) - всё, что было добавлено в структуру CISC машин. За все эти годы не появилось никакой аппаратной поддержки вызовов подпрограмм и код, выдаваемый оптимизирующими компиляторами для современных языков, по-прежнему больше напоминает результат трансляции их неструктурных предшественников.
Проблема именно в этом. Обычные компьютеры оптимизированы для выполнения потока последовательных команд. Трассировка большинства программ показывает, что вызовы процедур составляют незначительную долю от общего числа инструкций, что является, в известной степени, следствием того, что программисты их избегают. С другой стороны, современная методология построения программ акцентирует важность небольших процедур и нелинейного исполнения кода. Столкновение между этими двумя парадигмами приводит к неоптимальным и потому дорогим аппаратным и программным компонентам современных компьютеров общего применения.
Всё изложенное не означает, что при использовании структурных языков программы не становятся более структурированными и удобными в сопровождении, но только то, что аппаратура, которая побуждает писать линейный код, и установка на эффективность её использования не позволяет структурным языкам показать всё, на что они способны. Несмотря на то, что современная методология рекомендует дробить код на очень маленькие процедуры, большинство программ состоит из кусков, которые меньше по числу, больше по объёму и сложнее по выполняемым функциям, чем следовало бы.
7.1.2 Процедуры правильного размера
Как много функций должна выполнять типичная процедура? Miller обнаружил, что число «семь» плюс-минус два приложимо к большинству сторон мыслительной деятельности ( Miller 1967 ). При работе с информацией мозг связывает группы подобных понятий в меньшее число более абстрактных объектов. В приложении к программам это означает, что каждая процедура должна содержать приблизительно семь основных операций, подобных присваиваниям или вызовам других процедур, чтобы оставаться понятной. Если в подпрограмме есть более семи различных операций, её следует разбить на несколько частей, выделяя сходные действия в процедуры следующего уровня, уменьшая сложность каждого отдельного участка. В другой части своей книги Miller показал, что мозг может охватить только два или три уровня вложенности идей в одном контексте. Это означает, что глубоко вложенные циклы и условные операторы следует реорганизовать в виде вложенных вызовов подпрограмм, а не в виде запутанных структур внутри одной процедуры.
==155
7.1.3 Почему программисты не используют небольшие процедуры
Единственный оставшийся вопрос, почему большинство программистов не следует этим правилам?
Самой очевидной причиной, по которой они избегают маленьких процедур с глубокой вложенностью - цена в терминах скорости исполнения. Настройка параметров процедуры и собственно инструкция вызова может поглотить время исполнения программы, если используется слишком часто. В случае глубокой вложенности с оптимизацией справятся только очень сложные компиляторы, но и их возможности ограничены. В результате глубина вложенности процедур держится на небольшом уровне.
Следующей причиной ограниченного использования процедур является трудность их написания. Достаточно часто требование снабжать каждый кусочек кода заголовком делает создание маленьких подпрограмм достаточно утомительным занятием. К этой заботе прибавляется заметный объём сопроводительных бумаг и изменений в проектной документации, связанных с появлением новой процедуры в большом проекте ( обычное правило: каждой процедуре должно соответствовать описание в проектной документации ). Поэтому не следует удивляться тому, что средний её размер составляет одну-две страницы вместо одной-двух строк, считающихся приемлемыми.
Есть и менее очевидный источник трудностей создания процедур в современных языках программирования, который приводит к более редкому, чем ожидает читатель книг по структурному программированию, их использованию. Стандартные языки и их пользователи закостенели в традициях пакетных методов разработки. Пакетные методы дают мало возможностей для тестирования или удобств для работы с маленькими функциями. Настоящую интерактивность ( которая вовсе не означает пакетной последовательности «редактирование - компиляция - сборка - выполнение - сбой - отладка» ) дают лишь несколько сред разработки, которые не упоминаются в большинстве компьютерных курсов.
Вследствие всех этих факторов современные языки программирования дают довольно скромные возможности для эффективного модульного программирования. Современное аппаратное и программное обеспечение необоснованно ограничивает использование модульного подхода и, таким образом, без нужды увеличивает стоимость предлагаемых решений на базе вычислительных машин.
7.1.4 Архитектурная поддержка процедур
==156
Проблемы, появляющиеся из-за низкой производительности при вызовах процедур, решаются разработчиками множеством способов. Проектировщики RISC машин используют два различных подхода. Группа Stanford MIPS использует компиляторную технику, при которой процедуры разворачиваются во включаемый код везде, где это возможно. После этого компилятор проводит очень сложную работу по распределению регистров, чтобы избежать сохранения и восстановления их содержимого при вызовах подпрограмм. Статистика, обосновывающая использование этого метода, была набрана на программах, построенных по традиционным схемам, с достаточно большими процедурами и неглубокой вложенностью. Метод MIPS хорошо работает на существующем программном обеспечении, но может начать сбиваться при более совершенных стратегиях проектирования, которые можно увидеть в CISC машинах.
Другим вариантом, который изначально продвигался группой Berkeley RISC I , является использование регистрового окна, образующего регистровый стековый кадр. Сменой указателя на регистровый кадр можно быстро сохранить или восстановить набор регистров и обеспечить тем самым быстрый вызов подпрограмм. Такой подход имеет много общих положительных черт с решениями для стековых машин. Реализация, особенности которой в жизни определяют успех и провал проекта, выливается в выбор между одним или несколькими стеками, фиксированной или переменной величиной стекового кадра, а также в задание стратегии подкачки данных и решение общих вопросов конструирования вычислительных машин. [* Быстрота смены указателя сопровождается необходимостью наличия механизма выбора нового адреса для регистрового окна, т.е. получения содержимого для упомянутого указателя, о чём не стОит забывать ] .
==156