Pull to refresh

Comments 32

Надо будет попробовать поиграться.
Первый регистр, x0 имеет специальное назначение, он содержит 0. Вне зависимости от того, какое значение вы в него записываете, при чтении из этого регистра вы всегда получите 0.

Прикольно, не знал. Но какие это плюсы даёт в сравнении с одноразовым обнулением регистра и не записью в него в дальнейшем?

Это на самом деле очень удобно, его можно использовать везде, где один из операндов равен нулю. Один из наиболее очевидных примеров использования: add rd, rs, zero - переносит rs в rd, что заменяет команду mov, необходимую в других архитектурах.

В MSP430 тоже есть близкая этому идея - генерация констант (степени двойки) при использовании определённых способов адресации и некоторых регистров. Это позволяет существенно разгрузить количество опкодов необходимых для реализации базиса команд. В MSP430, вроде, 23-и разных команды, а остальные "псевдокоманды" получаются из них и с помощью вот таких архитектурных решений ("трюков"). PDP-11, прародитель MSP430, в этом смысле, была более прямолинейна и ортогональна.

Zero register — очень популярная штука. Есть в Alpha, MIPS и SPARC.

Еще этот регистр помогает модифицировать некоторые команды.

Например, в команде CSRRS (Atomic Read and Set Bits in CSR), при использовании регистра x0 как источника маски, команда будет атомарно только читать CSR регистр без его модификации. Если вы захотите использовать другой регистр в котором хранится ноль, то команда все равно произведет запись в регистр CSR, поэтому если необходимо только прочитать биты, то нужно использовать регистр zero.

Соглашение об использовании регистров Gnu ASsembler, переведённое в "железо"?

Нет, в железо ничего не переведено, это остаётся просто соглашением, и вы можете использовать какое-то другое соглашение для своих проектов. Однако это соглашение является стандартным и изменять его без необходимости не нужно.

Но какие это плюсы даёт в сравнении с одноразовым обнулением регистра и не записью в него в дальнейшем?

В него нужно или каждый раз записывать ноль, или проверять ноль ли там, ну или очень хорошо следить чтобы не записать туда ноль, и в итоге получим тот же регистр постоянно занятый под ноль. Ну и на аппаратном уровне такой регистр легче сделать, чем регистр с записью и чтением. Возможно его физически даже и нет — на уровне декодера или исполнителя команд обрабатывается (но я не спец в этом — просто догадка).
Физически вместо триггеров там просто перемычки на землю (hardwire to zero)
Но какие это плюсы даёт в сравнении с одноразовым обнулением регистра и не записью в него в дальнейшем?

Ну например у i8080 и потомков (Z80, x86) есть инструкция сравнения CMP. Технически это вычитание с отбрасыванием результата. Здесь это можно было бы реализовать не как отдельную инструкцию, а как вариант SUB с записью результата в х0. Впрочем, флагов здесь тоже нет, так что это не имеет смысла.

Насколько перспективно изучать RISC-V? Это чисто академический интерес? Какое реальное оборудование сейчас есть под RISC-V?

Сейчас на ядро Risc V перешли в своей продукции компании MIPS, Western Digital, и ряд других крупных компаний имеют такие планы.

Пока на рынке мало микросхем, но кое-что есть. Есть аналоги STM32 на RISCV, есть чипы SiFive, скоро выйдет одноплатник от Beaglebone.

Ну, есть например вот такой контроллер: habr.com/ru/post/533272 Правда применяются ли они где-то в реальности не знаю.
За RISC-V очень сильно топят европейцы. Официально они там за открытость, равенство и прочие чмоки, реально же их очень выбесили эпизоды с прослушкой телефона Меркель, скандал со швейцарским производителем криптосредств и другие, не так распиаренные эпизоды. От закрытых прошивок, Интел МЕ и прочих ТПМ их малость коробит. Так что критическая инфраструктура в ЕС будет переводиться на RISC-V, а затраты переложат как обычно на граждан (читай, заставят покупать ширпотреб на этом железе, чтобы снизить цену).
PS. У нас для этих целей Эльбрус, у китайцев — Лунгсон или как там его.
Интересно, как выглядит 64-битная версия архитектуры.
Чтобы в регистр загрузить 64-битную константу, нужно 3 инструкции? (учитывая фиксированный размер инструкции в 32 бита)

64-битная версия выглядит практически так же, но добавлены команды для загрузки и сохранения 64-битных чисел и некоторые другие.
Пример:

long long foo() {

    return 0x1122334455667788LL;

}
foo():                                # @foo()
        lui     a0, 1097
        addiw   a0, a0, -1843
        slli    a0, a0, 14
        addi    a0, a0, 1109
        slli    a0, a0, 12
        addi    a0, a0, 1639
        slli    a0, a0, 12
        addi    a0, a0, 1928
        ret

Здесь в a0 загружается 64-битная константа. Главным образом используются сложения и сдвиги. Это несколько громоздко, но иначе придётся делать загрузку константы из памяти, а это нежелательно.

Я думаю, загрузка из памяти была бы в 2-3 инструкции.
Константы расположить где-то рядом с PC, например после текущей функции.

А почему нежелательно? Думаю, по тактам будет меньше, чем тут 4 сложения и 3 сдвига.

Загрузка из памяти, это одна инструкция. В современном мире загрузка из памяти - это самая медленная операция, и может занимать сотни тактов. Зависит от попадания в кэш.

Загрузка из памяти, это одна инструкция
Да, но нужно ещё адреса настроить. Тут ведь нет такой инструкции:
LW x3, pc+3000?

В современном мире загрузка из памяти — это самая медленная операция, и может занимать сотни тактов
Эту константу можно считать частью кода. Инструкции функции тоже как-то попадают в кеш, и это первый раз долго. Так и блок констант функции тоже один раз должен попасть в кеш, а дальше, всё время работы функции он будет доступен. Хотя с данными не сработает prefetch, как в случае с кодом, но это не так важно.

Но почему данные должны храниться относительно pc? Есть инструкция вида LW rd, imm(rs). Если мы загружаем константу (а мы же говорим о константах, да?), то они лежат обычно в секции .rodata, по фиксированному адресу.

Кэш ускоряет работу с памятью, но хорошей практикой считается как можно меньше трогать память, и выполнять всё, что можно, на регистрах. Отсюда и замена lw/ld длинными цепочками команд, вычисляюших константы в рантайме.

Но почему данные должны храниться относительно pc? Есть инструкция вида LW rd, imm(rs).
Потому что в архитектуре все смещения короткие. И относительно PC довольно легко получить адрес. А загрузить произвольный 64-битный адрес в rs — та же самая морока на 8 инструкций, т.е. проще сразу загрузить 64-битную константу.
Кэш ускоряет работу с памятью, но хорошей практикой считается как можно меньше трогать память, и выполнять всё, что можно, на регистрах
Вот это непонятно. Кеш инструкций от этого будет распухать. Уж лучше плотненько положить данные в кеш данных, чем те же данные положить в код с 4-кратным оверхедом (8 инструкций по 4 байта, чтобы получить чистые 8 байт данных) и ещё проиграть по тактам на горячем участке (внутри цикла, например)

Я не буду разводить бессмысленных дискуссий, но данные относительно pc не хранятся ни в одной архитектуре. Предлагаю на этом закончить.

По моему опыту, адресация данных относительно PC/IP — очень частое явление.

en.wikipedia.org/wiki/Position-independent_code
Скрытый текст
Some processor architectures, such as the Motorola 68000, Motorola 6809, WDC 65C816, Knuth's MMIX, ARM and x86-64 allow referencing data by offset from the program counter. This is specifically targeted at making position-independent code smaller, less register demanding and hence more efficient.


godbolt.org/z/Mf6a14WfW

Кэш - это просто быстрая память прямо возле процессора.

А для инструкций, я подозреваю, ещё существенную роль играет конвейер. Когда все инструкции строго одинаковой длины, их очень удобно загружать потоком из памяти (или из кэша - не сильно важно) и начинать исполнять. Загрузка константы, особенно большой, это либо чтение из какой-то сторонней памяти, куда надо отдельно сходить и потратить сотни тактов. Либо, если прямо в коде - это уже скорее CISC. А если делать всё же инструкциями одинаковой длины через конвейер - то посчитать константу займёт столько же тактов, сколько для этого требуется команд. На взгляд воспринимается не так стройно, но пишут то для железа, а не для разглядывания.

Загрузка константы, особенно большой, это либо чтение из какой-то сторонней памяти, куда надо отдельно сходить и потратить сотни тактов
Мой аргумент в том, что инструкция (буду использовать для примера x86, как наиболее популярный)
mov eax, [esi]
или в x86-64
mov eax, [rip+0x20]
займёт пару тактов, если операнд есть в кеше.
А если операнда нет в кеше, чем это отличается от случая

… много-много кода, которого ещё нет в кеше инструкций
mov eax, 0x12345678
… ещё много-много кода

то же самое — если кода, который содержит нужную константу, нет в кеше инструкций, подгрузка строки памяти с этой инструкцией — те же сотни тактов.

Что будет быстрее прочитать: 200 байт кода + 100 байт данных к нему, или 700 байт раздутого кода, в который встроены данные? Учитывая, что шина памяти узкое место, видимо, первое быстрее. Физику не обманешь.

Ну тут очень многое начинает зависеть от "если есть в кэше". И если для инструкций можно сказать, что "скорее есть" - потому что предвыборка, предсказатели и т.д. не зря изобретали, то вот для лежащих где-то в стороне 100 байт это уже не так очевидно.

А дальше уже конвейер и сами команды. Если они все одинакового размера и выполняются за одинаковое время - то делать предвыборку становится ещё проще. И декодировать проще. Шагай по 32 бита и всё. А не так, что вдруг тут 2 байта команда, а следующие 4 байта её непосредственный операнд, а за ней вообще однобайтовая... Это не к тому, что такой подход лучше или хуже, он просто другой. Когда в архитектуре всё из стройных одинаковых кирпичиков - и загрузка константы в виде её вычисления выглядит многословней, но для выполнения проще.

Ну и если бы всё зависело исключительно от шины памяти... А там ещё надо адреса странслировать, Да и память оказалась на соседнем процессоре (не подумали про NUMA), а может вообще страничка уже в подкачке... И только потом уже, наконец, начали читать и упёрлись в шину. Пенальти может быть таким, что однозначно оценить скорость чтения 200+100 против 700 уже невозможно.

Да, много надо учитывать.
Но необходимость синхронизации кеша ядер тут не так критична, потому что линии данных с константами используются только на чтение, не будет события по всем ядрам «линия изменилась, кеш недействителен».
Префетч кода тут тоже не сильно важен. Если префетч начинается за 5 инструкций, например, то есть, за 20 тактов до выполнения инструкции, которой ещё нет в кеше, то в итоге на загрузке отсутствующей строки, когда до неё дойдет дело, будет пауза не 200 тактов, а 180. То есть тут префетч ускоряет, но не так, чтобы в разы.
Когда в архитектуре всё из стройных одинаковых кирпичиков — и загрузка константы в виде её вычисления выглядит многословней, но для выполнения проще.
Можно было бы сделать загрузку 64-битной константы хотя бы за 4 инструкции. (каждая устанавливает определённые 16 бит у 64-битного регистра), но не пачку из сложений/сдвигов.

Вы хотите сказать, что чтение 32 байт из памяти кода более желательно чем 8 байт из памяти данных?

На самом деле тут вопрос совсем в другом, чем объясняется в статье. Разработчики RISC-V просто упростили до предела выборку из памяти и сделали кеш инструкций некогерентным с кешем данных. Как следствие, запрещён самомодифицирующийся код без специальных ритуалов. Поэтому они могут заранее спекулятивно выбрать константу из памяти инструкций ещё до того момента, как подойдёт время фактического исполнения этих инструкций, и не беспокоиться о том, что константа за это время изменится. А в памяти данных спекулятивная выборка сделала бы необходимым механизм проверки когерентности кеша и отмены уже выполненных инструкций, как, например, в POWER. Поэтому они выбирают из памяти данных строго синхронно с потоком инструкций (хотя и через кеш), что очень упрощает схемотехнику, но делает доступ к памяти ещё более медленным.

Короче говоря, инструкции в RISC-V допускают спекулятивную выборку, а данные не допускают. Вот и пытаются перетащить все константы в сегмент кода.

UFO just landed and posted this here

Наткнулся на этот пост из поиска. Не работает всё. Записываю в регистры 3 и 4, выполняю add x3, x2, x1 и получаю нифига без масла.

Записываю в регистры 3 и 4, выполняю add x3, x2, x1

Если вы выполняете x3 = x2+x1, то почему заполняете x3 и x4, а не x1 и x2? Да и использовать мнемоники x1, x2,… не стоит, лучше t0, t1, s2, s3, a4 и т.д.


А вообще, если хотите, чтобы вам помогли, показывайте код и рассказывайте чего вы хотите добиться и что именно не получается.

Sign up to leave a comment.

Articles