Выбрать главу

Надо признать, что достигнутые на этом пути успехи действительно впечатляли - в последних версиях ЭВМ выразительность ассемблерного листинга зачастую не уступала выразительности программы, написанной на языке высокого уровня. Одной-единственной машинной инструкцией можно было сказать практически все, что угодно. К примеру, такие машины, как DEC VAX, аппаратно поддерживали инструкции «добавить элемент в очередь», «удалить элемент из очереди» и даже «провести интерполяцию полиномом» (!); а знаменитое семейство процессоров Motorola 68k почти для всех инструкций поддерживало до двенадцати (!) режимов адресации памяти, вплоть до взятия в качестве аргумента инструкции «данных, записанных по адресу, записанному вон в том регистре, со смещением, записанным вот в этом регистре». Отсюда и общее название соответствующих архитектур: CISC - Complex Instruction Set Computers («компьютеры с набором инструкций на все случаи жизни»).

На практике это привело к тому, что подобные инструкции оказалось сложно не только выполнять, но и просто декодировать (выделять из машинного кода новую инструкцию и отправлять ее на исполнительные устройства). Чтобы машинный код CISC-компьютеров из-за сложных инструкций не разрастался до огромного размера, машинные инструкции в большинстве этих архитектур имели неоднородную структуру (разное расположение и размеры кода операции и ее операндов) и сильно отличающуюся длину (в x86, например, длина инструкций варьируется от 1 до 15 байт). Еще одной проблемой стало то, что при сохранении приемлемой сложности процессора многие инструкции оказалось принципиально невозможно выполнить «чисто аппаратно», и поздние CISC-процессоры были вынуждены обзавестись специальными блоками, которые «на лету» заменяли некоторые сложные команды на последовательности более простых. В результате все CISC-процессоры оказались весьма трудоемкими в проектировании и изготовлении. Но что самое печальное, к моменту расцвета CISC-архитектур стало ясно, что все эти конструкции изобретались в общем-то зря - исследования программного обеспечения того времени, проведенные IBM, наглядно показали, что даже программисты, пишущие на ассемблере, все эти «сверхвозможности» почти никогда не использовали, а компиляторы языков высокого уровня - и не пытались использовать.

К началу 80-х годов классические CISC полностью исчерпали себя. Расширять набор инструкций в рамках этого подхода дальше не имело смысла, наоборот - технологи столкнулись с тем, что из-за высокой сложности CISC-процессоров оказалось трудно наращивать их тактовую частоту, а из-за «тормознутости» оперативной памяти тех времен зашитые в память процессора расшифровки сложных инструкций зачастую работают медленнее, чем точно такие же цепочки команд, встречающиеся в основной программе. Короче говоря, стало очевидным, что CISC-процессоры нужно упрощать - и на свет появился RISC, Reduced Instruction Set Computer.

Регистровые окна SPARC

При построении RISC-процессоров принимается во внимание медлительность оперативной памяти. Обращения к ней (даже с учетом различных кэшей) - «дорогостоящи» и требуют дополнительных вычислений, а потому, насколько это возможно, их следует избегать. Но Load/Store-архитектура и большое число регистров - не единственное, что можно сделать.

В любом программном коде можно встретить немало вызовов функций - фактически требований к процессору перейти в заданное место программы, продолжить выполнение программы до специальной инструкции возврата, после чего - вернуться к тому месту, где произошел вызов, почти полностью восстановив свое состояние до вызова функции. Чтобы это можно было сделать, при вызове функции процессор должен «запомнить» свое текущее состояние - в частности, содержимое некоторых регистров общего назначения и значительной части специальных регистров. Традиционное решение - «запихнуть» все необходимые данные в специальную конструкцию - стек[ Стек можно условно представить как запаянную с одного конца трубку, в которую по одному кладутся и по одному же извлекаются шарики (данные). Первый положенный в трубку шарик извлекается последним, так что если мы, скажем, по очереди положим (push) в стек числа 1, 2, 3, то извлекая (pop) данные, мы поочередно достанем 3, 2, 1.], которую процессор поддерживает на аппаратном уровне и которая в большинстве процессоров реализована в виде пары служебных регистров и выделенного участка оперативной памяти, куда все складываемые в стек данные и записываются. Поэтому любой вызов функции в традиционной схеме неявным образом приводит к записи в оперативную память десятков, а то и сотен байт информации. Есть даже целый ряд модельных задачек на эту тему - как написать компилятор, минимизирующий количество сохраняемой информации; причем кое-какие из этих наработок поддерживаются популярными компиляторами (например, соглашение __fastcall в некоторых компиляторах C/C++). Но оказывается, что всего этого можно избежать.

В типичной SPARC-архитектуре используется регистровый файл из 128 регистров; причем пользователю из них одномоментно доступны только расположенные подряд 24 регистра, образующие в этом файле окно, плюс еще восемь стоящих особняком глобальных регистров. Глобальные регистры используются для глобальных переменных[В структурных языках программирования типа C принято разделять локальные переменные, которые определены и используются только одной конкретной функцией и существуют только то время, пока эта функция работает; и глобальные переменные, которые существуют все время, пока выполняется программа, и доступны всем функциям программы]; регистровое окно - для локальных. Когда нам нужно вызвать какую-нибудь функцию, мы записываем необходимые для ее работы исходные данные в конец окна, а процессор при вызове функции попросту смещает окно по регистровому файлу таким образом, чтобы записанные данные оказались в начале нового, пока пустого окна. Требовавшие сохранения временные данные вызывавшего функцию кода оказываются за пределами окна, так что испортить их нечаянными действиями невозможно. А когда функция заканчивает работу, то полученные результаты записываются в те же самые регистры в начале окна, после чего процессор смещает его обратно. И никаких расходов на сохранение-восстановление стека.

Расположение окон в SPARC’ах можно программировать, добиваясь максимально эффективного использования схемы (либо много окон, но маленьких, либо мало - но больших; в зависимости от того, что за функции встречаются в программе) - этот факт даже отражен в названии процессора (Scalable Processor ARChitecture). Подобно многим своим RISC-сестрам, разработанная в середине 80-х годов и пережившая расцвет в середине 90-х, SPARC-архитектура не выдержала «гонки мегагерц» и сегодня фактически умерла. Но предложенный ею подход живет и здравствует - его позволяет использовать, например, архитектура IA-64 (Itanium).

Шаг второй. RISC

Точно так же, как когда-то CISC-процессоры проектировались под нужды asm-программистов, RISC проектировался в расчете на типовой код, генерируемый компиляторами. Для начала разработчики свели к минимуму набор инструкций и к абсолютному минимуму - количество режимов адресации памяти; упаковав все, что осталось, в простой и удобный для декодирования регулярный машинный код. В частности, в классическом варианте RISC из инструкций, обращающихся к оперативной памяти, оставлены только две (Load - загрузить данные в регистр и Store - сохранить данные из регистра; так называемая Load/Store-архитектура), и нет ни одной инструкции вроде вычисления синуса, косинуса или квадратного корня (их можно реализовать «вручную»), не говоря уже о более сложных[Канонический пример - инструкция INDEX, выполнявшаяся на VAX медленнее, чем вручную написанный цикл, выполняющий ровно тот же объем работы]. Да что там синус с косинусом - в некоторых RISC-процессорах пытались отказаться даже от трудно реализуемого аппаратного умножения и деления! Правда, до таких крайностей ни один коммерческий RISC, к счастью, не дошел, но как минимум две попытки (ранние варианты MIPS и SPARC) предприняты были.