Увы, перечисления из прошлой части по-прежнему недостаточно, "циклические ссылки", или точнее, комбинаторные обратные связи (combinatorial loops) по-прежнему зловеще нависли над этим проектом. Скажем, пришли две очередные половинки команды, одна в DestAddr, другая в SrcAddr. В одной, DestAddr, лежит FMA - Fused Multiply-Add - умножение с накоплением, и запрашивает остановить конвейер на ближайшие 17 тактов. А в другой, SrcAddr, лежит CALL0 - вызов "нулевой" процедуры (у нас их может быть 16 штук, от 0 до 15, лежат в таблице QuatCoreCallTable.v), которая "хочет" поскорее перепрыгнуть на новое место, а последующую команду забраковать.
Или ещё интереснее - в DestAddr лежит прыжок, а в SrcAddr - чтение из памяти. Первый хочет объявить второго недействительным, а второй первого - остановить на один такт, пока правильные данные не придут.
Вот из-за таких хитростей у меня "самовозбуд" начался, пора разобраться. Всё не так сложно, как кажется.
Возьмём первый тестовый кусочек кода:
C [X+i] FMA [X+k] CALL foo CALL bar ... foo proc JMP [--SP] ;пока пустая процедура - сразу возвращает выполнение CALL bar foo endp
Сначала загрузили значение в регистр C, затем провели умножение с накоплением, а после этого вызываем процедуру. Причём мы только что перепрыгнули на первую строчку этого кода. Тогда на первом такте процессор увидит следующее:
XXX [X+i]
В DestAddr - "неверное" значение, соответствующее команде после команды прыжка. К шинам Data, DestAddr и SrcAddr у нас очевидно добавляются ещё 2 провода: DestDiscard и SrcDiscard. Вот на данный момент DestDiscard=1, а SrcDiscard=0. (как именно они сформировались, пока что опустим). Из-за того, что DestDiscard=1, мы даже не пытаемся изучать команду по DestAddr. К примеру, если на неё поступил DestDiscard=1, она заведомо не подаст запроса на остановку процессора, типа "я не я и команда не моя".
А вот [X+i] изучаем подробнее. Это чтение из памяти, поэтому из QuatCoreMem должно поступить stall=1 - запрос на остановку конвейера. Таким образом, DestAddr и SrcAddr фиксируются на том же месте, в QuatCoreMem формируется эффективный адрес и защёлкивается на входе ОЗУ.
На следующем такте у нас ОБЯЗАТЕЛЬНО должно сохраниться значение DestDiscard=1, иначе мы всё-таки выполним эту команду. Значит, и регистр, хранящий DestDiscard, также должен останавливаться в этой ситуации. Значит, команда в DestAddr снова игнорируется и сигнала stall не выдаёт. Тем временем на шину данных наконец поступили правильные данные и защёлкнулись в мультиплексоре шины данных. stall=0, так что мы наконец-то переходим к следующей команде:
C [X+k]
К этому времени должно установиться DestDiscard=0, т.е "неправильную" команду проехали уже. Также SrcDiscard=0, так что обеим командам "верить".
По DestAddr - команда записи в регистр C, одна из самых безобидных. Она выдаёт stall=0. Но по SrcAddr - опять идёт команда чтения из памяти, выставляющая stall=1. И вот тут мы понимаем, что команда "C" просто обязана выполниться ТОТЧАС ЖЕ! Только сейчас в шине данных лежит то, что нам нужно, значение [X+i]. К следующему такту значение может уже исказиться. Более наглядно, если вместо [X+i] там был литерал, например, 5. По окончании текущего такта в шину данных придёт другое значение, что-то из памяти, но ещё не [X+k], а какое-то прошлое значение. Итак, "C" выполняется, и [X+k] выполняется - на вход ОЗУ защелкивается правильный эффективный адрес.
Начинается следующий такт. Те же там же (конвейер был остановлен). И теперь уже мы ОБЯЗАНЫ остановить команду "C", иначе она повторно занесёт данные в регистр, только теперь это КРИВЫЕ данные! Получается, что stall, который ИНИЦИИРУЕТСЯ СО СТОРОНЫ SrcAddr, должен задерживаться на такт и только после этого управлять стороной DestAddr! Тогда всё получится... Наконец-то на шине данных появляется значение [X+k] и защёлкивается на мультиплексоре. А со стороны DestAddr всё без изменений.
Переходим к следующей команде:
FMA CALL0
Опять вспоминаем, что Call foo преобразуется в [SP++] CALLn, где n-номер процедуры в таблице QuatCoreCallTable.v. Команда CALLn подаёт на шину данных адрес возврата и осуществляет переход, а [SP++] заносит этот адрес возврата в стек.
И тут становится отчётливо понятно: FMA имеет явный приоритет перед CALL0, ведь это ещё до конца не выполненная ПРЕДЫДУЩАЯ КОМАНДА. Она обязана быть выполнена. Поэтому, FMA имеет полное право остановить конвейер и спокойненько в течение 18 тактов выполнять умножение с накоплением. Всё это время CALL0 "отдыхает".
Наконец, на последнем такте выполнения FMA, stall устанавливается в 0, и активируется CALL0. В итоге, к следующему такту на шине данных защёлкивается адрес возврата, на счётчике инструкций (PC) - адрес процедуры, и также к следующему такту "зажигается" SrcDiscard=1. В аккумуляторе лежит корректный результат умножения с накоплением.
Переходим к следующей команде:
[SP++] CALL1
При этом "горит" SrcDiscard=1. Поэтому команду CALL1 игнорируем начисто - нет её. А будь на её месте опять какой-нибудь доступ к памяти - не позволили бы останавливать процессор, ещё чего, это команда, идущая ЗА вызовом процедуры. Она случайно, "по инерции" к нам залетела. Так что мы спокойненько заносим значение из шины данных в [SP++] - и двигаем дальше.
[SP++] [--SP]
О, шикарно! Причём сейчас DestDiscard=1, но SrcDiscard=0. То есть, в левой части "огрызок" команды Call bar, а вот в правой части уже правильная команда из процедуры foo. Небольшое дежа вю - мы выполняем [--SP] за два шага, всё это время продолжается DestDiscard=1. На шине данных появляется адрес возврата.
Переходим к следующей команде:
JMP CALL1
Жизненно - куда бы мы не шли - попадаем в bar.
Итак, ситуация, которая раньше казалась нам происками злых хакеров, когда два разных способа прыжков соседствуют бок о бок в одном модуле. С введением конвейера это стало суровой правдой жизни. К счастью, ничего страшного здесь нет - ситуация разрешается абсолютно однозначно. Ведь мы знаем что JMP - это ПРЕДЫДУЩАЯ команда, а CALL1 - ТЕКУЩАЯ. У предыдущей очевидный приоритет. Если там прыжок, значит CALL1 недействителен. Так что уже сразу, увидев JMP, мы выставляем SrcDiscard=1, и он ТУТ же гасит нам любую команду, лежащую в SrcAddr.
Фух, что-то начинает проясняться, но мозги плавятся...