Принципы развития ЭВМ и RISC-V
Базовая статья — в Википедии
Конец 1970-х / начало 80-х: новые архитектуры ЭВМ
Ранние ЭВМ: дна инструкция = одна понятная человеку операция (пример — PDP, в частности, PDP-11)
- Усложнение системы команд (Complex Instruction Set Computing, CISC)
- Переменная длина
«Ортогональные» инструкции: одинаковая возможность использовать регистры, ячейки, константы, прямую и косвенную адресацию — зачастую один и тот же результат можно достичь различными командами
- Поддержка команд высокого уровня (например, двоично-десятичной арифметики, «строковых» операций)
Активное использование микропрограмм — недоступных пользователю команд ещё более низкого уровня, суперпозицией которых формируется сложная команда высокого уровня
- …
=> Бóльшая часть программистов и компиляторов не пользуется этими возможностями
- Упрощение системы команд (Reduced Instruction Set Computing, RISC)
- Запрет на «тяжёлые» операции (например, «память-память» иногда даже на умножение)
- Повышение эффективности такта работы процессора
- Постоянная длина команды
Ограничение на работу с памятью (например, только одна операция обмена за такт)
- Оптимизация работы с памятью (например, обмен только машинными словами)
- Неблокирующее выполнение стадий обработки команды (конвейер)
- …
- Большое количество регистров общего назначения (для эффективных вычислений)
- Отказ от «тяжёлых» микропрограмм: сложные операции (обработка промахов кеша, подгрузка страниц памяти, переключение контекста задач и т. п.) выполняются обычным программным кодом, а не специальной одной инструкцией, выполняющей массу действий.
Увеличение сложности поначалу (1970-е) было удачным ходом (PDP-10/11, Motorola 68k+, …), но дальнейшее усложнение было либо невозможно, либо приводило к сверхсложным архитектурам (поздние версии VAX, PentiumII, … — 1980-е/90-е)
Отдельная ветка развития — это архитектуры VLIW (Very Long Instruction Word), в которых одна команда может задействовать большое количество вычислительных устройств (сейчас это направление ушло в GPU).
RISC наиболее эффективно развивались в 1990-е/2000-е (DEC Alpha, MIPS, Sparc, Power, ARM, AVR, SGI, …).
Рынок компьютеров общего назначения захватили сверхсложные процессоры типа «всё включено» (например, современные intel и AMD, по слухам, представляющие микропрограммную реализацию всего на базе RISC-ядра)
- Рынок микроконтроллеров, малых устройств, встраиваемых систем, систем реального времени и др., где важна эффективность при малых затратах энергии, держат RISC-подобные архитектуры (ARM, AVR, RISC-V, …)
RISC-V
Архитектура RISC-V появилась намного позже — в 2010, и немедленно заместила собой своего предшественника — MIPS. Немного истории:
1985-2019 — MIPS, Джон Хенесси (Стенфорд)
1993 — Computer Organization and Design совместно с Девидом Паттерсоном (Беркли)
- …
2010 — Паттерсон: RISC-V
Соответствие принципам RISC (унаследовано от MIPS)
- отсутствие вычислительно сложных инструкций
- RISC-V: + отсутствие дублирующих инструкций
- RISC-V: + удобство чтения/написания инструкций ассемблера (псевдоинструкции0) и неудобство чтения машинного кода человеком (упаковка битов для быстрейшего аппаратного анализа и т. п.)
- фиксированная длина инструкции
- RISC-V: + трёхадресность\
- большое количество регистров общего назначения
- ограничения на работу непосредственно с оперативной памятью как с медленным устройством
- оптимизация под конвейер
Главная особенность — Open Hardware
…сообществом (а кого там нет?)
- Система команд:
Непривилегированные команды (это правда можно читать!)
Привилегированные команды (это тоже можно читать!)
Исходные тексты (хороший, годный LaTex)
Свежие версии (в т. ч. HTML)
Т. н. расширения
Больше спецификаций (ABI, IOMMU, Platform и т. п.)
Эмулятор RARS
(прототип: MARS)
RARS (RISC-V Assembler and Runtime Simulator).
- Полный эмулятор
- Ассемблер
- IDE (редактор + отладчик + визуализация выполнения)
- RISC-V (расширения IMFDN) rv32 и немного rv64
- «Функции окружения» (ecalls) — MARS / SPIKE
- Эмуляция внешних устройств
- Режим работы «интерпретатора» — без GUI
- Лицензия BSD, написан на Java, запускается везде
- Есть также встроенная
На 2024-06-24 в состав репозитория ALT Linux Team входит расширенная версия с исправлениями в работе с регистрами управления и возможностью запуска с включённым таймером.
- Запускаем:
java -jar rars-1.6.alt3.jar
- Если шрифты выглядят как кошмар из прошлого века, включаем антиалиасинг вручную:
java -Dawt.useSystemAAFontSettings=on -jar rars-1.6.alt3.jar
Если шрифт невозможно разглядеть, всю картинку можно увеличить вдвое (или втрое, но вещественные числа у меня не заработали) одним из двух способов:
GDK_SCALE=2 java -Dawt.useSystemAAFontSettings=on -jar rars-1.6.alt3.jar
- или
java -Dsun.java2d.uiScale=2 -Dawt.useSystemAAFontSettings=on -jar rars-1.6.alt3.jar
Интерфейс RARS разработан таким образом, чтобы вся нужная информация и инструменты находились на экране одновременно. По сути, это интегрированная среда разработки с редактором, отладчиком и встроенным эмулятором.
Редактирование файлов на языке ассемблера:
- Все доступные функции RARS отражены в меню (если в данный момент функция недоступна, она замазана серым)
- Часть функций доступна в виде пиктограмм
- Вкладка «Редактор»
- Внутренние вкладки с именами редактируемых файлов
- Текстовый редактор с подсветкой синтаксиса ассемблера RISC-V
- Блок пиктограмм «компиляция - запуск - отладка»
- Скорость эмуляции (если не выставлена на максимум, после выполнения каждой инструкции содержимое соответствующих окон обновляется)
- Вкладка «Выполнение»
- Окно дизассемблированного участка оперативной памяти, в котором указан адрес инструкции, содержимое ячейки в числовом и в дизассемблированном виде, а также соответствующая строка исходного текста (в случае псевдоинструкции может не совпадать с дизассемблированным представлением)
- Активированная точка останова на конкретной инструкции
- Окно меток (адреса, соответствующие идентификаторам в программе)
- Окно регистров (содержимое каждого регистра можно редактировать вручную)
- Окно содержимого оперативной памяти (ячейки памяти можно редактировать вручную)
- Откуда начинать просмотр памяти (переход на соседние участки) и выбор стандартного базового адреса (стек, куча, область глобальных данных и т. д.)
- Формат представления ячеек памяти
- Вкладка сообщений от ассемблера и исполняющей системы «Messages» и вкладка «Run I/O», предназначенная для ввода и вывода данных с помощью внешних вызовов
Цикл разработки
В окне редактора набираем текст программы на языке ассемблера и сохраняем её в файл (без этого шага трансляция работать не будет)
- Продолжаем редактирование
Нажимаем кнопку компиляции (с ключом и отвёрткой) или Run → Assemble в меню
- Если в окне сообщений выдалась ошибка, кнопка запуска становится недоступна — надо вернуться к п. (1) и исправить ошибку
Если ошибок трансляции не было, открывается вкладка «Execute» и можно нажимать кнопку запуска или Run → Run в меню
- При этом в нижней части откроется вкладка «Run I/O» — если в программе предусмотрен ввод, вводить надо с клавиатуры в эту вкладку, туда же попадёт и вывод программы
Отступление про современные и не очень архитектуры
Развитие индустрии, даже (в особенности?) наукоёмкой, подпадает под т. н. «принцип легаси»:
- Появление новой задачи,
- Победа одного-двух частных решений этой задачи, актуальных для своего времени
- «Канонизация» этих решений (legacy)
Legacy впоследствии (почти) не пересматривается — до тех пор, пока не станет непреодолимым препятствием в решении актуальной задачи. Далее см. п. 1
Пример: «Принципы фон Неймана» как исторический казус.
Общее правило: «так пошла эволюция, переделывать дороже».
Побитовые операции
Представление числа (повторение):
- последовательность битов в ячейке памяти как двоичное число
- положительное двоичное число
- отрицательное двоичное число в дополнительном коде
- сложение и вычитание двоичных чисел
- Побитовые операции
Операции над машинными словами, которые интерпретируются как набор битов. Каждый бит — 0 или 1 — может восприниматься как логическое «ложь» и «истина» соответственно. В этом случае результат побитовой операции — это машинное слово, результат попарного применения одноимённой логической операции к соответствующим парам битов. Например, в формуле для для отрицательного числа в дополнительном коде -N = ~N - 1 операция ~N (поменять все биты на противоположные) — это «побитовое НЕ». Эта операция иногда также называется дополнением — отсюда и термин «дополнительный код».
Чаще всего побитовые операции нужны, когда отдельные биты числа имеют собственное значение. Например, старший (знаковый) бит отражает знак целого числа, младший равен 0 для чётных чисел и 1 для нечётных и т. п. Регистр флагов в учебных машинах (и не только в них) устроен по тому же принципу: каждый его бит — это отдельный элемент данных.
Распространение знака
Распространение знака — это не отдельная команда RISC-V, а свойство всех знаковых операций. Если последний бит (в архитектуре rv32 — тридцать первый) числа равен 1, его можно интерпретировать как отрицательное вы диапазоне от
- При загрузке положительного байта или полуслова в регистр значения равны при условии, что оставшиеся биты заполняются нулями:
байт 5 == 000001012 == 0x05; слово == 0x00000005
- полуслово 5 == 0x0005; слово == 0x00000005
То же самое происходит при загрузке числа, которое считается беззнаковым:
байт 111110102 == 0xfa == 250; слово 0x000000fa == 250
- полуслово 0xfffa == 65530; слово 0x0000fffa == 65530
При загрузке отрицательных байта и полуслова все оставшиеся биты слова надо для сохранения знака заполнять 1. Следовательно, при любом значении знаковый бит надо распространять на всё слово
распространение знака: 5 = 000001012 == 000000000000000000000000000001012
распространение знака: -6 = 111110102 == 111111111111111111111111111110102
Для следующих побитовых операций можно воспользоваться вот такой (пока что — вполне непонятной) программой:
1 .macro printb %prompt %reg %ecall # Вывод регистра в двоичном виде в отдельной строке
2 .data
3 prompt: .asciz %prompt
4 .text
5 li a7 4 # Вывод строки
6 la a0 prompt
7 ecall
8 li a7 %ecall # Вывод числа в требуемом виде
9 mv a0 %reg
10 ecall
11 li a7 11 # Вывод перевода строки
12 li a0 '\n'
13 ecall
14 .end_macro
15
16 .macro printop %c1 %op %c2 %id # Результат применения двухместной операции к парметрам
17 li t0 %c1
18 li t1 %c2
19 printb " " t0 35
20 printb %id t1 35
21 %op t0 t0 t1
22 printb " == " t0 35
23 printb " " t0 34
24 .end_macro
25
26 .text:
27 printop 0x12345678 and 0x90abcdef "AND "
В действительности нас интересует только последняя строка этой программы — подстановка макроса printop. Этот макрос выводит два параметра бинарной операции и её результат в двоичном виде. У макроса четыре параметра:
- Первый операнд
- Инструкция RISC-V (годится любая типа R)
- Второй операнд
- Строка с названием операции (для удобства чтения должна состоять из 4 символов)
Числа в шестнадцатеричной системе счисления очень просто переводить в двоичную — достаточно заменить каждую шестнадцатеричную цифру на ровно четыре цифры соответствующего ей двоичного числа. Например a96e16 = 10101001011011102.
Побитовые «И», «ИЛИ» и «исключающее ИЛИ»
Вспомним простые правила вычисления логических операций «И», «ИЛИ» и «исключающее ИЛИ»:
- Результат «А или Б» — ложь тогда и только тогда, когда оба операнда — ложь
- Результат «А и Б» — истина тогда и только тогда, когда оба операнда — истина
- Результат «А «исключающее или Б» — истина тогда и только тогда, когда операнды не равны
X |
Y |
X and Y |
|
X |
Y |
X or Y |
|
X |
Y |
X xor Y |
0 |
0 |
0 |
|
0 |
0 |
0 |
|
0 |
0 |
0 |
0 |
1 |
0 |
|
0 |
1 |
1 |
|
0 |
1 |
1 |
1 |
0 |
0 |
|
1 |
0 |
1 |
|
1 |
0 |
1 |
1 |
1 |
1 |
|
1 |
1 |
1 |
|
1 |
1 |
0 |
Один из приемов работы с битовыми данными — использование маски. Маска позволяет получать значения только определенных битов в ячейке.
Операция and нужна, чтобы проверить, чему равен конкретный бит:
Проверка нечётности целого числа N: N and 1
- Равно 0, если младший бит N был равен 0 (число было чётное)
- Равно 1, если младший бит N был равен 1 (число было нечётное)
Проверка отрицательности 32-разрядного целого числа N: N and 0x80000000
- Равно 0, если знаковый (старший, 31-й) бит числа нулевой и число положительное
- Не равно 0 (равно 0x80000000), если знаковый бит числа ненулевой и число отрицательно
Проверка делимости на 4 — операция с двумя битами: N and 3
Как это работает?
Операция or используется для того, чтобы установить конкретный бит (или группу битов) в значение 1
Ближайшее нечётное число M, по модулю не меньшее N: M = N or 1
Ещё одно применение and — установить бит или группу битов в 0. Для этого применяется маска, в которой в 1 выставлены все биты, кроме требуемых
Ближайшее чётное M, по модулю не превосходящее N: M = N and 0xfffffffe
Операция xor используется для того, чтобы поменять конкретный бит или группу битов на противоположные
Побитовое «НЕ»: N xor 0xffffffff (в RISC-V оно так и реализовано)
Инструкция для этих операций в RISC-V называются так же, как в таблице: and, or и xor.
Проверьте их работу, заменив соответствующие параметры printop в программе.
У побитового «ИЛИ» есть замечательное свойство — обратимость. В самом деле, если N = A xor B, то про N можно сделать четыре утверждения:
Биты числа N — это биты числа B, часть которых заменена на противоположные там, где были единичные биты у числа A
Биты числа N — это биты числа A, часть которых заменена на противоположные там, где были единичные биты у числа B
A = N xor B
B = N xor A
Проверьте все эти утверждения с помощью программы, подставляя в printop результаты предыдущих её запусков
Побитовый сдвиг
Сдвиг целого влево на n битов с заполнением младших битов нулями соответствует умножению на 2n. Для отрицательных чисел вытесняемые единичные биты считаются незначимыми
000000000000000000000000011110112 == 123
000000000000000000000011110110002 == 123 * 10002 == 123 * 8
111111111111111111111101110010012 == -567
111111111111111111101110010010002 == -567 * 8
Сдвиг положительного целого вправо на n битов с заполнением старших битов нулём (логический сдвиг) соответствует делению с остатком на 2n, при этом вытесняемые биты и есть остаток
000000000000000011001101001101012 == 52533
000000000000000000011001101001102 == 52533//8 == 6566 (остаток 5)
- Логический сдвиг отрицательного числа влечёт потерю знакового бита и образование некоторого иного положительного числа
111111111111111011100000110011002 == -73524
000111111111111111011100000110012 == 536861721
Для сохранения знака старшие биты сдвигаемого вправо отрицательного числа надо заполнять 1. Следовательно, в общем случае для соответствия делению знаковый бит нужно при сдвиге вправо распространять на все дополнительные биты (арифметический сдвиг)
000000000000000011001101001101012 // 8 == 000000000000000000011001101001102
111111111111111011100000110011002 // 8 == -73524 // 8 == 111111111111111111011100000110012 == -919 (остаток 4)
Правило «сдвиг M влево на n = умножение M на 2n» не работает, если |M| * 2n не помещается в машинное слово.
Инструкции RISC-V:
sra — арифметический сдвиг вправо (shift right arithmetic)
srl — логический сдвиг вправо (shift right logic)
sll — логический сдвиг влево (shift left logic)
Почему нет арифметического сдавига влево?
Попробуйте эти операции в программе.
Пример. Проверить является ли заданное положительное число степенью двойки. Если да записать в регистр $t3 1, иначе записать 0. Сейчас мы не готовы писать такую программу, но можно обсудить, как для проверки использовать побитовые операции. Идея решения: Переберём все маски с единственной единицей, и посчитаем, сколько будет ненулевых результатов операции and. Если ровно один, то исходное число было степенью двойки.
Алгоритм:
Начнём с маски M = 1 и счётчика совпадений P = 0
Если (N and M) == M, прибавим 1 к P
Сдвинем M на 1 влево: M = M sll 1
Если M все ещё помещается в ячейке, повторим цикл
Если P == 1, число N — степень двойки
Проверьте, чему равен сдвиг 0x80000000 на 1 влево