nabbla (nabbla1) wrote,
nabbla
nabbla1

"Пустопорожние" команды QuatCore

Довольно странная мысля пришла, не к селу не к городу.

Список команд QuatCore, сторона DestAddr. Задачка на внимательность: найдите здесь NOP, No OPeration :)

DestAddr


Адрес +0 +1 +2 +3 +4 +5 +6 +7
00 OUT OUT OUT OUT OUT OUT OUT OUT
08 OUT OUT OUT OUT OUT OUT OUT OUT
10 SIO SIO SIO SIO SIO SIO SIO SIO
18 SIO SIO SIO SIO SIO SIO SIO SIO
20 ACQ ACQ ACQ ACQ ACQ ACQ ACQ ACQ
28 ACQ ACQ ACQ ACQ ACQ ACQ ACQ ACQ
30 TRK TRK TRK TRK TRK TRK TRK TRK
38 TRK TRK TRK TRK TRK TRK TRK TRK
40 ERL ERL ERL ERL ERL ERL ERL ERL
48 ERH ERH ERH ERH ERH ERH ERH ERH
50 [ER++] [ER++] [ER++] [ER++] [ER++] [ER++] [ER++] [ER++]
58 [ER++] [ER++] [ER++] [ER++] [ER++] [ER++] [ER++] [ER++]
60 IR IR IR IR IR IR IR IR
68 IR IR IR IR IR IR IR IR
70 IR IR IR IR IR IR IR IR
78 IR IR IR IR IR IR IR IR
80 Acc PM ADD SUB ABS ABSPM ABSA ABSS
88 ZACC NOP C C DIV2 DIV2PM DIV2A DIV2S
90 MUL FMPM FMA FMS MULSU SUFMPM SUFMA SUFMS
98 MULU UFMPM UFMA UFMS SQRD2 SQRPMD2 SQRAD2 SQRSD2
A0 i j k Inv i++ j++ k++ ijk
A8 iLOOP jLOOP kLOOP Jik iLoopUp jLoopUp kLoopUp JNik
B0 JMP JMP JMP JMP JMP JMP JMP JMP
B8 JL JL JO JO JGE JGE JNO JNO
C0 [X+1] [X+2i+1] [X+2j+1] [X+3j+1] [X+i] [X+3i] [X+2j+i] [X+3j+i]
C8 [X+k] [X+2i+k] [X+2j+k] [X+3j+k] [X+i^j] X [X+2j+i^j] [X+3j+i^j]
D0 [Y+1] [Y+2i+1] [Y+2j+1] [Y+Treug[j]+1] [Y+i] [Y+3i] [Y+2j+i] [Y+Treug[j]+i]
D8 [Y+k] [Y+2i+k] [Y+2j+k] [Y+Treug[j]+k] [Y+i^j] Y [Y+2j+i^j] [Y+Treug[j]+i^j]
E0 [Z+1] [Z+2i+1] [Z+2j+1] [Z+3j+1] [Z+i] [Z+3i] [Z+2j+i] [Z+3j+i]
E8 [Z+k] [Z+2i+k] [Z+2j+k] [Z+3j+k] [Z+i^j] Z [Z+2j+i^j] [Z+3j+i^j]
F0 [SP+1] [SP+2i+1] [SP+2j+1] [SP++] [SP+i] [SP+3i] [SP+2j+i] [i-1+SP++]
F8 [SP+k] [SP+2i+k] [SP+2j+k] [--SP+k] [SP] SP [SP+2j] [--SP]


Когда кроме вычислительного ядра ничего нет (никакой периферии), всё просто: ВСЕ АДРЕСА от 0x00 до 0x7F означают NOP, в этом была определённая симметрия: на стороне SrcAddr это адреса для "непосредственных значений" (Immediate), когда нужно не в память лезть, не в регистры, а просто выдать на шину конкретное число. А тогда если надо из шины число записать не в память и не в регистр - то выходит как раз "пустота", /dev/null и так далее.

cat /dev/zero > /dev/null - переливание из пустого в порожнее. У нас когда-то команда "0 0" так и делала. Точнее, она брала ровно один нолик и отправляла его в никуда. Наглухо она не зависала.

Но потом начал обвешиваться устройствами, и такой "лакомый кусок" из 128 адресов быстренько занял. Очень жадно: каждая команда ввода-вывода отжирает целые области памяти (ради упрощённого декодирования), но почему бы и нет.

А NOP случайно получился в АЛУ, просто "сам собой". Отдельные биты адреса означали те или иные аспекты команды (длинная операция, вроде умножения, или короткая, знаковая или беззнаковая, с накоплением или без накопления), логика формировалась, чтобы получить всё это разнообразие, не прибегая к микрокоду (и вполне успешно), и вот просто повезло, что и NOP там образовался. Но адрес у него "хитрючий" :)

Хотя QuatCore не исключение, вот в процессоре 6502 код операции для NOP: 0xEA, что очень веселит "фанатов" компании Electronic Arts, ну реально она ничего не делает!

И не знаю почему, вдруг подумалось: а если бы не сложился в АЛУ этот NOP, и от ввода-вывода специально под это адрес не "откусывать", можно было бы без него обойтись?


У нас команда состоит из двух частей, "адрес источника данных" SrcAddr и "адрес получателя" DestAddr. На стороне источника, SrcAddr, большинство не имеют "побочных эффектов" - они просто дают на шину данных запрошенное значение. Это может быть регистр, может быть значение из памяти (тогда будет задержка на один такт), или непосредственное значение, или данные откуда-то из периферии. Лишь считанные команды, вроде [SP++], кроме выдачи данных на шину, делают дополнительные вещи, в данном случае прибавляют единичку к указателю стека SP (Stack Pointer). Так что "ничего не делать" со стороны SrcAddr довольно легко, проще всего взять любое "непосредственное значение". Например, 0x00.

А вот со стороны "получателя" DestAddr все на удивление деятельны. Данные из шины либо запишутся в регистр, либо в память, либо на периферию, либо над ними осуществляется арифметическая операция, либо вообще прыжки, условные и безусловные.

Но ведь наверняка можно придумать такое сочетание DestAddr - SrcAddr, чтобы результат нигде ничего не поменял!

Самое простое и многообещающее - записать в регистр его же значение, команды вроде
i   i,
     j   j,
     X   X

и так далее.

Пока QuatCore не имел конвейера, это работало. Но сейчас есть "подводные камни". Допустим, предыдущая команда была "i 3", то есть положить значение 3 в регистр i. Выполнение SrcAddr и DestAddr разнесены на один такт.

На первом такте идёт "выборка" непосредственного значения 3.
На втором такте это значение заносится в регистр i (левая часть команды "i 3"), и в то же время на шину данных кладётся значение из регистра i (правая часть следующей команды "i i"). На шину попадёт СТАРОЕ значение i, до его присваивания!

И наконец, выполнится левая часть команды "i i" - в регистр будет занесено его СТАРОЕ ЗНАЧЕНИЕ, ещё до присвоения тройки!

В общем, возник классический Read-After Write (RAW) Hazard, нужно эту команду отделить NOP'ом. WAIT, OH SHI...

Впрочем, за командой "i 3" мы имеем полное право в качестве NOP поставить "j j" - вот она не навредит. С другой стороны, если там была команда "ijk 0x0023", которая записывает одновременно регистры i,j,k и Inv, то ни одна из "i i", "j j" и т.д нам в качестве NOP не подойдёт! Зато, если нужно присвоить значения 3 из четырёх этих регистров, а один оставить "как был", мы можем вслед за "ijk" восстановить старое значение одного регистра, который был остаться "как есть"! Надо запомнить такую хитрость :)

Но за командой "ijk" можно с чистой совестью поставить "X X" в качестве NOP. С предыдущей командой он конфликтовать не будет.

Но не может ли возникнуть конфликт со СЛЕДУЮЩЕЙ командой? Компилятор пока что решит, что ДА. Например, если вслед за "X X" пойдёт "Acc [X+i]", он решит что выборка [X+i] совпадёт по времени с записью в регистр X, поэтому прочитано будет старое значение. Но мы-то знаем, что старое и новое значение - одно и то же, так что всё в порядке.

Выходит, что в каждом конкретном случае мы можем подыскать команду, перезаписывающую регистры, которая сработает как NOP. Одной универсальной быть не может, а вот ДВУХ вполне достаточно. Скажем, "X X" для любых команд, кроме тех, что производят запись в X. А для них - "Y Y".

С помощью молотка можно починить почти что угодно. А с помощью ДВУХ молотков - ВООБЩЕ ВСЁ!

Выполняться такой "NOP" будет за один такт.

Следующий вариант: Арифметическая операция в АЛУ, которая ничего не меняет.
SUB, DIV2S (вычесть из аккумулятора значение с шины данных, делёное на 2), ABSS (вычесть из аккумулятора модуль от значения с шины данных) и все операции умножения и возведения в квадрат нам не подходят - они меняют флаг знака.

Самое очевидное: "ADD 0". Да, прибавление нуля никак на аккумуляторе не отразится, и флаг знака не меняется, так уж у нас сделано (SUB меняет, ADD не меняет). В таблице непосредственных значений может добавиться "полновесный ноль", то есть все 16 бит нулевые. (когда пишешь NOP 0, компилятор знает, что подойдёт вообще любое "непосредственное значение" - и возьмёт первое попавшееся). Впрочем, "полновесный ноль" почти всегда и так есть, очень уж важное значение :) Ну и время выполнения такой операции очень специфично. Если АЛУ уже работает, мы остановимся, пока ему не останется один такт до завершения - в этот момент оно начнёт обрабатывать ещё и "ADD 0", на что уйдёт ещё 2 такта.

Чуть хитрее: "PM 0" (прибавить ноль, если Inv=0, вычесть ноль, если Inv=1) - эта команда также не меняет флаг знака. Чем это лучше "ADD 0" - не знаю. Также можно "DIV2A 0" и "DIV2PM 0" - они будут выполняться на такт дольше.

Ещё один забавный вариант: "C C" (записать в регистр C своё же значение). Кажется, что здесь те же проблемы, что раньше с "i i" - может возникнуть RAW Hazard. Но здесь его не возникнет, т.к в АЛУ есть блокировка - если запрашивается результат его работы, он не будет выдан до полного окончания вычисления.

Выходит, в АЛУ даже без самого NOP достаточно "пустых" команд: "ADD 0", "PM 0", "DIV2A 0" "DIV2PM 0" и "C C", каждая из которых могла бы сойти универсальной заменой NOP Но время выполнения - боль. Если нам нужен NOP, чтобы разделить "конфликтующие команды", и хотим разделения всего на 1 такт, то лучше АЛУ сюда не припахивать...

И ещё одна укуренная разновидность NOP: безусловный прыжок на следующую команду! В случае QuatCore вполне себе сработает, по всем переходам у нас все необходимые блокировки введены, т.е "лишние" команды выполнены не будут. Есть процессоры (по крайней мере, БЫЛИ), где нет как такового сброса конвейера, и сразу за командой прыжка как раз-таки должны стоять NOP'ы, т.к они будут исполняться как ни в чём не бывало, и надо, чтобы они "не наломали дров". То есть, там использовать прыжок в качестве NOP было бы "вытаскиванием себя за волосы", а в QuatCore вполне можно :) Но я бы не стал, тоже потери тактов идут. В момент выполнения JMP, счётчик инструкций опережает исполняемую команду на 2 позиции, и должен был прибавить ещё 1, но вместо этого "откатывается" на одну назад, после чего два такта ждёт, пока из конвейера уйдёт "недействительные" команды и уже зайдут новые (которые те же самые вообще-то). Ну и учитывая, что JMP у нас всегда шёл по абсолютному адресу (а сейчас вообще все адреса абсолютные), каждый такой "NOP" заспамит таблицу непосредственных значений адресами "следующей команды", а она и так у меня начала расползаться. Вон, два алгоритма захвата (ближней и дальней дистанции) уже генерят около 80 записей, из 128 допустимых! А когда объединю с обнаружением точек и всем остальным - вообще может не хватить! Глядишь, придётся относительные адреса вернуть... Хотя есть и ещё одна идейка дурацкая :)

Какая во всём этом мораль? Да никакой. Если вдруг захочу "перебрать" АЛУ в очередной раз - вполне могу NOP выкинуть, поместив что-нибудь более полезное. Главные кандидаты: MAX (взять максимум между аккумулятором и новым значением, т.е поместить значение только если оно больше текущего) и CMP (вычесть значение из аккумулятора, поставить флаг знака, но в аккумуляторе оставить старое значение), они на этом "железе" вполне исполнимы, нужно только логику управления чуть поменять. Но пока такого желания нет - как-то обхожусь без них.

На самом деле, это я снова жду заказчиков из РКК, а они чего-то не приходят. Ладно, сейчас делом займусь.
Tags: ПЛИС, программки, работа, странные девайсы
Subscribe

Recent Posts from This Journal

  • Тестируем atan1 на QuatCore

    Пора уже перебираться на "железо" потихоньку. Решил начать с самого первого алгоритма, поскольку он уже был написан на ассемблере. В программу внёс…

  • Формулы приведения, что б их... (и atan на ТРЁХ умножениях)

    Формулу арктангенса на 4 умножениях ещё немножко оптимизировал с помощью алгоритма Ремеза: Ошибка уменьшилась с 4,9 до 4,65 угловой секунды, и…

  • Алгоритм Ремеза в экселе

    Вот и до него руки дошли, причина станет ясна в следующем посте. Изучать чужие библиотеки было лениво (в том же BOOSTе сам чёрт ногу сломит), писать…

  • atan на ЧЕТЫРЁХ умножениях

    Мишка такой человек — ему обязательно надо, чтоб от всего была польза. Когда у него бывают лишние деньги, он идёт в магазин и покупает какую-нибудь…

  • Ай да Пафнутий Львович!

    Решил ещё немного поковыряться со своим арктангенсом. Хотел применить алгоритм Ремеза, но начал с узлов Чебышёва. И для начала со своего "линейного…

  • atan(y/x) на двух умножениях!

    Чего-то никак меня не отпустит эта тема, всё кажется, что есть очень простой и эффективный метод, надо только его найти! Сейчас вот такое…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 5 comments