Это - копия документа, находившегося на http://dz.ru. Авторские права, если не указано иначе, принадлежат Дмитрию Завалишину и/или Евгении Завалишиной. Все изменения, внесенные мной, находятся в этой рамочке.Пожалуйста, прочитайте disclaimer. |
Палы-выры Мерсед.
(Прим. ред - "палы-выры", детск., сокращение от палочки-выручалочки - ключевая фраза при игре в прятки, обозначающая , что прячущийся сумел раньше водящего добежать до контрольной точки и не был найден водящим, а напротив, сумел его обыграть и сам "распрятался". Обозначет, фактически, выигрыш.)
Интел, наконец, закончил нас мучать и опубликовал спецификации на архитектуру и систему команд линии IA-64, в просторечии - Merced. По этому поводу сегодня Московское представительство провело пресс-брифинг, где ещё раз рассказало всё, что было известно про Мерсед уже года с два. А потом указало на пачку бумаги в 300 с лишним листов, где и находится то, что мы столько времени ждали. Полное описание системы команд 64-битных Интеловских процессоров.
Заглянем же в него. И порадуемся. Интел наградили таким количеством тумаков за нерегулярный и замысловатый набор команд 8086 и т.п., что, кажется, тут они решили доказать - можем, мол.
Итак. Процессор держит четыре режима. Реальный (8086), защищённый 32-битный (80386, 80286), виртуальный 8086 и защищённый 64-битный. Добавлены команды перехода с переключением режима на 32-битный или 64-битный. Все прерывания исполняются только в 64-битном режиме. Что, конечно, не мешает сделать обёртки с переходом на 32 или 16-битный код.
Регистры. В режиме IA64 доступны 128 целочисленных и 128 плавающих регистров, 64 предиката и 8 регистров перехода (содержат адрес для косвенного перехода). При этом процессор поддерживает локальный стек в файле регистров - при вызове подпрограммы происходит сдвиг номеров регистров и занятые вызывающей подпрограммой регистры оказываются в конце регистрового файла. Вызываемый же командой alloc заявляет необходимое ему число регистров, и если столько свободных есть - время на их сохранение не тратится. Таким образом вызовы подпрограмм происходят быстрее. Конечно, если все регистры уже заняты, alloc вызовет традиционное сохранение их в память, а return - восстановление. Регистры с 8-го по 31-й доступны из 32-битного и 16-битного режимов.
Кроме этих есть ещё спец-регистры типа профайлерных, управляющих и т.п. К примеру, среди них есть очень интересные регистры, доступные ядру на запись, а иным - лишь на чтение. Полезно для указания точек входа и глобальных таблиц, к примеру. Есть регистр-таймер, который просто равномерно увеличивает своё значение - для отсчётов времени.
Предикатные регистры устанавливаются командами сравнения, причём сравнение может учитывать другой предикат, что облегчает кодирование комплексных логических выражений. Для обычных операций предикат работает как выключатель - если он нулевой, то команда просто не исполняется. Для команд же сравнения он является ещё одним операндом. Нулевой предикат всегд аравен единице и используется для безусловного исполнения команд.
Доступ к памяти. Поддерживается как режим с младшими байтами в младших адресах, так и режим с младшими байтами в старших адресах. Адрес - 64 бита. Работа с памятью осуществляется, как в RISC-машинах, только специальными инструкциями. Вычисления идут в регистрах. Адрес памяти тоже указвывается регистром. Есть команды предварительной загрузки (хинты), команды обмена и семафора.
Исполнение. Инструкции группируются в пачки по три (16 байт), за такт может быть запущено на исполнение более одной пачки. Каждая инструкция занимает 41 бит, и оставшиеся 5 бит описывают раскладку инструкций по исполняющим блокам процессора. Переход возможен только на пачку целиком, а не на конкретную инструкцию в ней. Перехорд из начала или середины пачки обрывает её исполнение - инструкции, следующие в пачке за ним не исполняются.
Любая инструкция может быть условной и выполняться если установлен тот или иной бит предиката. Это позволяет избегать условных и безусловных переходов, особенно в тривиальных случаях типа if( a > b ) a = b; else b = a;
Соответственно, в наборе команд есть переход, вызов и возврат из подпрограммы, и они, как и почти все остальные команды, могут быть безусловными (если указан предикат 0 - вечно истинный) или условными по указанному предикату.
Прикидочное исполнение. Его все и везде будут называть спекулятивным, хотя оно прикидочное. :-) Идея крайне проста. Можно выполнить несколько инструкций, а потом проверить, насколько правомерно было их исполнять. Если правомерно - ну и хорошо. Если ситуация изменилась - не повезло. Придётся переделать. Есть два варианта прикидочного исполнения.
Первый позволяет вынести считывание из памяти поперёд группы команд, которые (если нам не повезёт) могут модифицировать то, что мы читаем. По окончании этой группы придётся проверить - а на самом деле оно как - не модифицировали? Тогда без задержки продолжаем. Модифицировали - придётся считать ещё раз. Можно даже выполнить считывание и часть обработки - тогда проверка должна быть сделана другой командой, которая в случае неудачи переходит на код, где считывание (на сей раз обычное) и обработка будут сделаны ещё раз.
Второй вариант приуидочного исполнения позволяет отработать "на удачу" кусок кода, который может потребоваться, а может и нет. Например, это позволяет часть кода, лежащего внутри условного блока вынести до него. Понятно, что потом придётся проверить условие и либо использовать, либо не использовать результаты прикидочного исполнения, но, что важно, процессор позволяет отключить генерацию исключительных ситуаций в прикидочном коде. После, когда решено, что его не зря исполняли, исключения могут быть запрошены. А если код оказался ненужным - то и исключения в нём не нужны.
Всё это - только ради того, чтобы компилятор мог хорошо оптимизировать код и загружать параллельно работающие блоки процессора. А для этого, как известно, нужно активно перегруппировывать код, вынося вперёд загрузки из памяти и тяжеловесные команды. Прикидочное исполнение нужно там, где честным путём вытащить команду вперёд нельзя, но очень хочется, так как иначе блоки процессора будут простаивать. Поэтому более выгодно загрузить их "на авось", чем вообще не использовать.
Строковые команды. Это, господа, не CISC, это самый что ни на есть RISC, хотя и очень нетривиальный. С памятью тут не работают, работают только с регистрами. И строковые команды работают со строками в регистрах. Очень симпатишно - в регистр-то влезает 8 байт, или же 4 юникодных кода. Строковая же команда, собственно, одна - поиск в регистре нулевого байта/двухбайтника.
Резюме. Процессор несложный. Именно потому программировать его - тяжело. Позиция такова - пусть у компилятора голова болит. Процессор берёт на себя минимум трудов по оптимизации процесса исполнения и сильно зависит от хинтов, которых в командах - вагон. Размещение инструкций в пачках (определяет их раскладку по исполняющим узлам), ручное управление кешированием, управляемая вручную ротация регистров, явные обмены регистр-память и отсутствие иных команд доступа к памяти, указание вероятности срабатывания условного перехода и необходимости хранить его историю, прикидочное исполнение - всё, всё нацелено на то, чтобы именно компилятор решал, как процессору себя вести. Это представляется разумным. Зачем тратить дикое количество места на кристалле на те функции, которые могут быть выполнены компилятором единократно? Лучше уж пустить это место на увеличение регистровой памяти.
Некоторые вещи сделаны очень красиво - тот-же стек регистров, к примеру - очень прилично выглядит. Фактически, регистры в этом процессоре выполняют фенкцию кеша верхушки стека и автоматически сбрасываются в память при вызове подпрограмм и восстанавливаются при возврате. Чрезвычайно хорошо - сильно сокращается пролог/эпилог функций, передача параметров и результатов в 90% случаев идёт через регистры, и возникает бесплатная возможность избавиться от неочевидных обращений к памяти. Например, вызов подпрограммы кладёт адрес возврата в регистр. Это очень похоже на безстековые машины, в которых его девать больше и некуда (команда BALR, если не ошибаюсь, в IBM360) но в данном-то случае регистры - это и есть стек! Однако если их хватает, то вызов подпрограммы может пройти вообще без единого обращения к памяти, а это - достижение. Конечно, при глубоких вызовах и больших контекстах функций сброс регистров в память и их подгрузка будут происходить, как не крути. Но - массированно, а это дешевле.
Ещё одна интересная проблема отчасти решена. В классических процессорах есть беда из серии неизбежных перестраховок. Если регистры сохраняет вызываемая функция, то она сохраняет те, которыми пользуется, даже если вызывающей они и не нужны. Если вызываемая - то наоборот. В любом случае есть существенный шанс сохранить регистр, который не используется либо там, либо там. В IA64 выделен блок несохраняемых регистров (0-31), которые не являются частью стека. Кстати, из них регистры 8-31 доступны из 32-битного режима, а 0-й всегда читается как 0. (Это применимо и к целочисленным, и к плавающим. Среди плавающих ещё одно исключение - 1-й регистр читается как 1). Понятно, что регистры 0-31 имеет смысл использовать как временные, не сохраняемые при вызове подпрограмм. И, таким образом, не тратить время на сохранение в стек ненужных значений.
Некоторые вещи сделаны странно (я бы сказал - безобразно, но боюсь, что пока не разобрался до конца). К примеру, есть команда для организации цикла, но счётчиком цикла является не любой указанный регистр, а специальный, даже не входящий в список общих регистров. Крайне, крайне странно. Впрочем, в IA64 есть специальная поддержка циклов, позволяющая накладывать исполнение хвоста одной итерации на начало другой с тем, чтобы держать процессор под полной нагрузкой. Эта поддержка включает в себя ротацию регистров так, чтобы две последовательные итерации могли пользоваться двумя отдельными копиями некоторых регистров. К этому механизму мы ещё вернёмся.
А пока - у меня воскресенье, хватит копаться в описании процессора. :-) И так уж три дня на него угрохал...