Анализируем FADD из Altair Basic


Вот он: https://github.com/option8/Altair-BASIC
Там, как оказывается, были полноценные 32-битные числа с плавающей запятой (8 бит экспоненты, 1 бит знака, 23 бита мантиссы) и библиотека для работы с ними, вплоть до тригонометрии, при том что весь интерпретатор бейсика занимал менее 4 кБ. Весь математический код писал Монте Давидофф.
Как всегда, начнём с реализации сложения и вычитания с плавающей точкой, FADD / FSUB. Весь этот код занимает 274 байта, что меньше 320 байт под 8051 (см. раз, два), впрочем, и формат числа чуть проще - тут экспонента 0 заведомо означает ноль (никаких "денормализованных чисел" даже на перспективу), нет Nan или Inf, при переполнении можно просто сделать JMP Error, чтобы интерпретатор грязно выругался и завершил исполнение программы. И размещение знака после экспоненты, похоже, упрощает распаковку и упаковку назад.
Код, похоже, вполне рабочий. Местами он написан даже не на ассемблере, а ещё ниже - там применено "перекрытие инструкций", когда одни и те же байты, в зависимости от того, откуда начинается исполнение, значат разные команды.
Проблему пока вижу только одну - ради экономии пары байт здесь отказались от проверки "несопоставимости" чисел. Если попросить эту штуку сложить два числа, отличающиеся в 2254 раза, меньшее число будет сдвигаться вправо 254 раза подряд, а потом получившийся ноль будет честно прибавлен к мантиссе бОльшего числа. На одно только это сдвигание уйдёт 50 миллисекунд, могло оказаться неприятным сюрпризом, в общем, привычка продуктов Билла Гейтса тормозить непредсказуемым образом была заложена ещё тогда!
Разберёмся, что там под капотом... Англоязычные комментарии - с гитхаба, русскоязычные - мои. Процессор 8080.
Начнём с "рабочего пространства":
016F: 00000000 FACCUM DB 00000000h ; 0173: 00 FTEMP DB 00h ;
В памяти зарезервировано 32 бита под "аккумулятор" - всегда один из операндов для работы с плавающей точкой. И ещё один временный байт, где будет храниться один-единственный бит, определяющий, "сложить или вычесть".
Теперь глянем "входной" код:
0801: 210B0C FAddOneHalf LXI H,ONE_HALF ;Load BCDE with (float) 0.5. 0804: CD200A FAddMem CALL FLoadBCDEfromMem 0807: C31208 JMP FAdd+2 080A: C1 FSub POP B ;Get lhs in BCDE. 080B: D1 POP D ; 080C: CDFA09 CALL FNegate ;Negate rhs and slimily 080F: 21 DB 0x21 ;LXI H,.... ;LXI into FAdd + 2. 0810: C1 FAdd POP B ;Get lhs in BCDE. 0811: D1 POP D ; 0812: ;к этому моменту в регистрах B,C,D,E лежит второе число, в H,L - "мусор"
При вызове FAdd или FSub предполагается, что второй операнд (первый всегда FAccum) лежит в стеке. Вообще, я пока не вполне понимаю, как он вообще вызывается - такое ощущение, там нужно сначала "ручками" занести адрес возврата, потом аргументы, а в процедуру прыгнуть через JMP или нечто похожее. И действительно, прямого вызова FADD я нигде не нашёл. Видимо, он вызывается как-то через "виртуальную таблицу".
Также есть вариант FAddMem: тогда в паре регистров (H,L) нужно указать адрес второго числа в памяти, и оттуда оно перемещается непосредственно в B,C,D,E.
И здесь же, ради небольшой экономии, примостилась функция FAddOneHalf - прибавить к аккумулятору число 0,5. Как видно, мы просто заносим в (H,L) адрес этой самой "половинки", а дальше по-накатанному.
Вариант FAdd проще всего понять: все 4 байта "выталкиваются" из стека в регистры B,C,D,E. Да, у intel 8080 PUSH/POP всегда двухбайтовые. В них под B имеется в виду пара (B,C), под D - пара (D,E). А я "с нахрапу" как увидел "BCDE" - подумал, что это Binary-Coded-Decimal чего-нибудь там, а оно вон оно что...
lhs в комментариях это left-hand-side, т.е левый операнд, и rhs - это right-hand-side, т.е правый операнд. Как нам объяснили, в аккумуляторе лежит правый операнд, как это ни странно.
FSub поинтереснее. Сначала, как и ранее, загружаем левое число в B,C,D,E, потом вызывается FNegate, вот он:
09FA: 217101 FNegate LXI H,FACCUM+2 09FD: 7E MOV A,M 09FE: EE80 XRI 0x80 0A00: 77 MOV M,A 0A01: C9 RET
Отсюда мы первый раз узнаём, в каком порядке хранятся байты в аккумуляторе: FACCUM самый младший байт мантиссы, FACCUM+1 - средний, FACCUM+2 - старший, его старший бит выражает знак. И в FACCUM+3 лежит экспонента. Старший байт мантиссы загружаем в A, там XORим ему старший бит и помещаем назад. Логично.
Дальше происходит странная вещь (снова глядим начало работы с FSUB, FADD):
080F: 21 DB 0x21 ;LXI H,.... ;LXI into FAdd + 2. 0810: C1 FAdd POP B ;Get lhs in BCDE. 0811: D1 POP D ;
Этот 0x21 в дизассемблере не указан как команда, хотя управление сюда приходит. В комментарии указано, что 0x21 - это команда LXI H, Imm, команда "загрузить непосредственное (Immediate) значение в регистры H,L". Она 3-байтовая, поэтому "захватывает" и следующие два байта, загружая в H,L значения 0xC1 и 0xD1. Эти значения и нафиг нам не сдались в (H,L), вся хитрость в том, чтобы пропустить "ненавистные" POP B, POP D малой кровью, ведь мы уже извлекли число из стека!
Красиво? На первый взгляд, да. Поначалу я думал, что FNegate поменяет знак у только-только вытащенного из стека числа, тогда всё логично. Но когда оказалось, что FNegate меняет знак в FAccum - это на самом деле оказался костыль. Ведь можно было повременить с извлечением из стека, спокойненько вызвать FNegate, а потом воспользоваться уже имеющимися POP B / POP D из процедуры FAdd. Сэкономили бы ещё 3 байта в дополнение к тому, что здесь. Полагаю, раньше FNegate должен был менять знак именно загруженного числа, потом что-то поменяли в парсинге, сменили FNegate, а этот код трогать не стали, "работает-не лезь". Опять же истоки микрософтовской борьбы с пиратством! Всё вокруг open source, если ты знаешь ассемблер. В те времена все знали ассемблер. Но здесь ассемблера мало, нужно в опкодах мыслить...
Поехали дальше:
0812: 78 MOV A,B ;If lhs==0 then we don't need 0813: B7 ORA A ;to do anything and can just 0814: C8 RZ ;exit. 0815: 3A7201 LDA FACCUM+3 ;If rhs==0 then exit via a copy 0818: B7 ORA A ;of lhs to FACCUM. 0819: CA120A JZ FLoadFromBCDE ;
А отсюда мы узнаём, что второе число, сидящее в регистрах B,C,D,E, размещено СТРОГО НАОБОРОТ: в B экспонента, в C знак и старшая мантисса, и так далее. Как интересно: хардверной проблемы Big-endian/little-endian тут ещё быть не могло, т.к процессор 8-битный, но программят его уже ровно так!
В общем, проверяем экспоненту левого числа на ноль (т.е минимально возможное значение). Если так, число заведомо ноль, и мы возвращаемся из процедуры. Да, у Intel 8080 были "условные вызовы и условные возвраты". RZ - оно самое, Return if Zero.
Потом проверяем на ноль экспоненту в правом числе, и если так, то применяем "Tail call". Т.е FLoadFromBCDE это функция, но мы её не вызываем, а прыгаем в неё, поэтому она по завершении вернётся уже не к нам, а туда, откуда вызывали нас, что экономит и место, и такты. Вот эта процедура:
0A12: EB FLoadFromBCDE XCHG 0A13: 226F01 SHLD FACCUM 0A16: 60 MOV H,B 0A17: 69 MOV L,C 0A18: 227101 SHLD FACCUM+2 0A1B: EB XCHG 0A1C: C9 RET
Команда XCHG (exchange) меняет местами пару (H,L) с парой (D,E).
Затем SHLD (store H, L Direct) помещает пару (H,L) по адресу памяти, указанному в команде непосредственно. В нашем случае - в первые два байта "аккумулятора".
Потом в H,L заносятся "ручками" регистры B,C и помещаются в следующие 2 позиции. Наконец, старые значения (H,L) восстанавливаются (а регистры D,E "затираются" - на их месте копии B,C). При вызове из FADD/FSUB это нахрен не нужно (они уже затёрты), но где-то в других местах это может быть важно.
Возвращаемся к нашему FADD. Оба числа проверили на ноль, и если хоть одно было нулевым, уже вернулись. Проверка на ноль - это не просто "оптимизация", это необходимость, т.к ноль это особый случай. Если его не рассматривать, то вместо нуля получится число 2-127 (неявная единица в мантиссе!)
Дальше код по сравнению экспонент двух НЕНУЛЕВЫХ чисел:
081C: 90 SUB B ;A=rhs.exponent-lhs.exponent. 081D: D22C08 JNC L082C ;If rhs' exponent >= lhs'exponent, jump ahead. 0820: 2F CMA ;Two's complement the exponent 0821: 3C INR A ;difference, so it's correct. 0822: EB XCHG ; 0823: CD020A CALL FPush ;Push old rhs 0826: EB XCHG ; 0827: CD120A CALL FLoadFromBCDE ;rhs = old lhs 082A: C1 POP B ;lhs = old rhs. 082B: D1 POP D ; 082C: F5 L082C PUSH PSW ;Preserve exponent diff 082D: CD370A CALL FUnpackMantissas
С прошлого раза в регистре A уже лежала экспонента правого числа, теперь вычитаем экспоненту левого. Если результат меньше нуля, т.е в "аккумуляторе" число заведомо меньшее по модулю, чем в регистрах, то два числа хитрым образом меняются местами. Но для начала исправляется разность экспонент, по сути, она берётся по модулю. Для этого сначала стоит команда CMA (complement A) - инвертировать каждый бит A, и затем ещё прибавляем единичку. Классика...
Затем XCHG - опять меняем местами (D,E) и (H,L), и вызываем FPush, вот она:
0A02: EB FPush XCHG 0A03: 2A6F01 LHLD FACCUM 0A06: E3 XTHL 0A07: E5 PUSH H 0A08: 2A7101 LHLD FACCUM+2 0A0B: E3 XTHL 0A0C: E5 PUSH H 0A0D: EB XCHG 0A0E: C9 RET
а здесь в обратную сторону exchange, потом два младших бита "аккумулятора" загружаются в (H,L). XTHL меняет местами два байта в стеке с двумя байтами из (H,L), т.е адрес возврата из FPush сейчас оказался в (H,L), а 2 байта числа - в стеке. Затем через PUSH H мы помещаем адрес возврата "выше", и потом такой же манёвр проделываем с оставшимися двумя байтами числа. Наконец, XCHG ещё разок меняет (D,E) и (H,L). Похоже, эти "общие процедуры" уверены, что (D,E) можно затереть, проблемы не будет, а (H,L) НЕ ТРОЖЬ! А поскольку у нас была обратная ситуация, её саму ещё обрамили командами XCHG.
Далее вызывается команда FLoadFromBCDE и заменяет аккумулятор на наше левое число, лежавшее в B,C,D,E. И наконец, через POP B, POP D обмен двумя числами заканчивается. Сейчас в FACCUM лежит число предположительно большее, чем в BCDE, но, если экспоненты совпадали, это может быть не так.
Лежащая в A разность экспонент (теперь уже по модулю) помещается в стек (у Intel 8080 нет PUSH A, вместо этого PUSH PSW, тут окромя A заносится слово состояния, Program Status Word).
Наконец, запускается процедура UnpackMantissas:
0A37: 217101 FUnpackMantissas LXI H,FACCUM+2 0A3A: 7E MOV A,M ; 0A3B: 07 RLC ;Move FACCUM's sign to bit 0. 0A3C: 37 STC ;Set MSB of FACCUM mantissa, 0A3D: 1F RAR ;FACCUM's sign is now in carry. 0A3E: 77 MOV M,A ; 0A3F: 3F CMC ;Negate FACCUM's sign. 0A40: 1F RAR ;Bit 7 of A is now FACCUM's sign. 0A41: 23 INX H ;Store negated FACCUM sign at FTEMP_SIGN. 0A42: 23 INX H ; 0A43: 77 MOV M,A ; 0A44: 79 MOV A,C ; 0A45: 07 RLC ;Set MSB of BCDE mantissa, 0A46: 37 STC ;BCDE's sign is now in carry. 0A47: 1F RAR ; 0A48: 4F MOV C,A ; 0A49: 1F RAR ;Bit 7 of A is now BCDE's sign 0A4A: AE XRA M ;XORed with FTEMP_SIGN. 0A4B: C9 RET ;
Загружаем старший байт мантиссы из FAccum, где и хранится знак. Делаем "вращение на месте влево", т.е старший бит занимает место самого младшего и попутно попадает в Carry. Затем в Carry принудительно устанавливаем единицу - и теперь вращаем вправо, но на этот раз через перенос. Т.е лежавший в младшем бите знак всё-таки попадает в Carry, а само число возвращается в норму, только вместо знака - всегда единица, и это "неявная старшая единица" мантиссы. Из регистра A возвращаем это дело в память.
Инвертируем Carry, снова впихиваем его старшим битом A - и сохраняем это значение в FTemp в память.
Ровно таким же макаром поступаем со второй старшей мантиссой, лежащей в регистре C, и, по сути, делаем от двух знаков чисел XNOR, т.е результат будет единицей, если знаки совпадают, и нулём - если они разные. Замечу: знак числа из FACCUM лежит себе тихо в FTEMP, а результат XNOR лежит пока в регистре A, и более нигде.
Продолжаем. Число, лежащее в FAccum, трогать не нужно, а вот лежащее в B,C,D,E, возможно, надо сдвинуть вправо. Знаки пока убрали в сторонку, а на их место поставили "неявные старшие единицы", теперь уже явные Следующий фрагмент кода:
0830: 67 MOV H,A ;H=sign relationship 0831: F1 POP PSW ;A=exponent diff. 0832: CDC908 CALL FMantissaRtMult ;Shift lhs mantissa right by (exponent diff) places.
Нужно ли складывать или вычитать числа (старший бит A) - пока сохраняем в регистре H, а сами возвращаем в A разность экспонент. Затем вызываем FMAntissaRtMult (shift Right Multiple times), вот она:
08C9: 0600 FMantissaRtMult MVI B,00h ;Initialise extra mantissa byte 08CB: 3C INR A 08CC: 6F MOV L,A 08CD: AF RtMultLoop XRA A 08CE: 2D DCR L 08CF: C8 RZ 08D0: CDD608 CALL FMantissaRtOnce 08D3: C3CD08 JMP RtMultLoop
Некое "топтание на одном месте" - к разнице экспонент прибавили единицу, чтобы потом всегда проверять результат на ноль после вычитания единицы. Странность в том, что можно было бы проверять флаг C, он при декременте (DCR) тоже устанавливается. Тогда INR A можно было бы не делать. И вообще какая-то пляска непонятная. У нас в стеке лежала разность экспонент, в A лежал знак. В итоге мы знак перетащили в H, а разность экспонент в итоге - в L. Чего бы для начала не сделать POP H, потом mov L, A, это ещё до вызова FMantissaRtMult, и тут две строчки уже не нужны.
Ну, дальше всё понятно: вызываем FMantissaRtOnce столько раз, на сколько бит надо сдвинуть. Зачем обнуление A ровно тут, внутри цикла (XRA A) - тоже без понятия. В FMantissaRtOnce он всё равно заместится, разве что по возврату мы ожидаем ноль. так не проще ли его за пределы функции вывести? Я посмотрел: функция вызывается дважды, и только в нашем FAdd ожидается A=0. Чужая душа - потёмки...
Глянем на FMantissaRtOnce:
08D6: 79 FMantissaRtOnce MOV A,C 08D7: 1F RAR 08D8: 4F MOV C,A 08D9: 7A MOV A,D 08DA: 1F RAR 08DB: 57 MOV D,A 08DC: 7B MOV A,E 08DD: 1F RAR 08DE: 5F MOV E,A 08DF: 78 MOV A,B ;NB: B is the extra 08E0: 1F RAR ;mantissa byte. 08E1: 47 MOV B,A ; 08E2: C9 RET ;
Да, такой код уже видели. 4 байта (3, которые были и ещё один дополнительный) двигаем вправо, используя "вращение вправо через перенос".
В коде для 8051 был куда более совершенный сдвиг вправо: если разность экспонент была более 24, мы сразу прекращали работу, считая второе число нулевым. А потом делали некую разновидность Barrel Shifter: если необходим сдвиг на 16 бит, это делали "побайтово", затем на 8 бит "побайтово", и только оставшиеся 0..7 - сдвиганием по единичке. Здесь ничего этого нет - двигаем по единичке, причём даже нет проверки на разность экспонент превышающую 24. Эта штука может и 254 итерации провернуть, и не поморщится!
Продолжим: мантиссу "меньшего" числа сдвинули сколько надо. Смотрим дальше:
0835: B4 ORA H ;A=0 after last call, so this tests 0836: 216F01 LXI H,FACCUM ;the sign relationship. 0839: F24D08 JP FSubMantissas ;Jump ahead if we need to subtract. 083C: CDA908 CALL FAddMantissas ;
Признак "складывать или вычитать" поместили в A, с установкой флагов, а регистр H начинает указывать на младший байт мантиссы "аккумулятора".
Если "результат положительный", т.е в старшем бите A сидит нолик, делаем вычитание мантисс, причём это прыжок, а не вызов функции, т.к на этот FSubMantissas мы прыгаем исключительно отсюда, и он сам знает, что делать дальше. А FAddMantissas вызывается, т.к она используется где-то ещё и должна "знать", куда вернуться. Глянем, для начала, на неё:
08A9: 7E FAddMantissas MOV A,M 08AA: 83 ADD E 08AB: 5F MOV E,A 08AC: 23 INX H 08AD: 7E MOV A,M 08AE: 8A ADC D 08AF: 57 MOV D,A 08B0: 23 INX H 08B1: 7E MOV A,M 08B2: 89 ADC C 08B3: 4F MOV C,A 08B4: C9 RET
Сложили младшие байты, результат вернули в регистр E. Прибавили адрес "указателя" (H,L) - и принимаемся за следующий байт. Тут сложение с переносом (ADC), и таких два. В итоге в C,D,E и флаге переноса лежит итоговый результат.
И посмотрим следующий за сложением код:
083F: D27E08 JNC FRoundUp ;Jump ahead if that didn't overflow. 0842: 23 INX H ;Flip the sign in FTEMP_SIGN. (А ВОТ И НЕТ! На байт экспоненты перемещаемся) 0843: 34 INR M ; 0844: CAA408 JZ Overflow ;Error out if exponent overflowed. 0847: CDD608 CALL FMantissaRtOnce;Shift mantissa one place right 084A: C37E08 JMP FRoundUp ;Jump ahead.
Этот кусочек следит за переполнением. Если его не случилось, сразу прыгаем дальше, в FRoundUp (округление). В противном случае, прибавляем один к экспоненте, проверяем, не переполнилась ли она (стала из 255 нулём) - ежели так, прыгаем в Overflow, а тот, в свою очередь, вызовет Error - выскажет пользователю всё, что о нём думает. Если экспонента не переполнилась, ещё надо сдвинуть мантиссу на единичку вправо (эту процедуру уже разбирали) - и также прыгаем в FRoundUp.
Рассмотрим теперь ветвь вычитания:
084D: AF FSubMantissas XRA A ;B=0-B 084E: 90 SUB B ; 084F: 47 MOV B,A ; 0850: 7E MOV A,M ;E=(FACCUM)-E 0851: 9B SBB E ; 0852: 5F MOV E,A ; 0853: 23 INX H ; 0854: 7E MOV A,M ;D=(FACCUM+1)-D 0855: 9A SBB D 0856: 57 MOV D,A 0857: 23 INX H 0858: 7E MOV A,M ;C=(FACCUM+2)-C 0859: 99 SBB C ; 085A: 4F MOV C,A ;
Тут используется и четвёртый, вспомогательный байт мантиссы (лежит в регистре B, и только у "меньшего" числа. Раньше в B лежала экспонента, но экспонента "меньшего" числа уже не нужна!). А в целом, принцип тот же самый, обычное вычитание в столбик. Результат лежит в регистрах С,D,E,B (в порядке старшинства), и ещё в Carry может быть единица, если мы всё-таки не угадали, что из чего вычитать.
Смотрим дальше:
085B: DCB508 FNormalise CC FNegateInt ;
CC - это Call if Carry. То самое - не угадали со знаком, и здесь просто поменять знак не получится. К примеру, в 1-байтном случае, если мы ошиблись и из 0 вычли 1, то сейчас результатом будет 0xFF (т.е 255) и Carry=1. Просто записать это как "-255" - ответ, очевидно, неверен! Нужно сменить знак числу "в дополнительном коде", т.е превратить 255 в 1, ну и знак "-" снаружи приделать. Сейчас посмотрим, как это делается:
08B5: 217301 FNegateInt LXI H,FTEMP 08B8: 7E MOV A,M 08B9: 2F CMA 08BA: 77 MOV M,A 08BB: AF XRA A 08BC: 6F MOV L,A 08BD: 90 SUB B 08BE: 47 MOV B,A 08BF: 7D MOV A,L 08C0: 9B SBB E 08C1: 5F MOV E,A 08C2: 7D MOV A,L 08C3: 9A SBB D 08C4: 57 MOV D,A 08C5: 7D MOV A,L 08C6: 99 SBB C 08C7: 4F MOV C,A 08C8: C9 RET
Первые 4 строки меняют знак результата, который хранится в FTemp. Приходится его загрузить в A, инвертировать и поместить назад. Затем A обнуляется через XOR, и этот нолик заносится в L. Дальше мы, по сути "в столбик" вычитаем получившееся число из нуля. Чтобы каждый раз занулить A, но не потерять перенос, нельзя это делать через XOR, вот для того и был нужен нолик в L: ведь mov A,L никаких флагов не меняет. Обращаем мы таким способом 4-байтовую мантиссу.
Продолжаем с вычитанием: если со знаком не угадали, это мы исправили, вычитание завершено, но результат не нормирован - может быть толпа нулей слева.
085E: 2600 MVI H,00h ; 0860: 79 MOV A,C ;Test most-significant bit of mantissa 0861: B7 ORA A ;and jump ahead if it's 1. 0862: FA7E08 JM FRoundUp ; 0865: FEE0 NormLoop CPI 0xE0 ;If we've shifted 32 times, 0867: CABE09 JZ FZero ;then the number is 0. 086A: 25 DCR H ; 086B: 78 MOV A,B ;Left-shift extra mantissa byte 086C: 87 ADD A ; 086D: 47 MOV B,A ; 086E: CD9008 CALL FMantissaLeft ;Left-shift mantissa. 0871: 7C MOV A,H ; 0872: F26508 JP NormLoop ;Loop
Здесь поначалу тоже творится нечто странное. Решено было подсчитывать, сколько уже прошло сдвигов в регистре H, причём считать "в минус", т.е 0, 255, 254, ... 0xE0 = 224. Или: 0, -1, ..., -32.
Вариант "совсем сдвигать не надо" рассмотрен отдельно ещё перед циклом - тогда мы прыгаем в FRoundUp (на нём мы остановились и рассматривая ветвь сложения).
Странность, что далее тот же самый A сравнивается с числом 224 (или -32), хотя это вообще не то сейчас, это старший бит мантиссы! Да, определённая логика есть: нас интересует равенство, а тут равенства заведомо не может быть, ведь если старший бит был единицей, мы уже отсюда свалили!
Потом уже, войдя в цикл, вычитаем единичку из H, сдвигаем влево дополнительный байт мантиссы, путём прибавления к самому себе, и затем вызываем FMantissaLeft, вот она:
0890: 7B FMantissaLeft MOV A,E 0891: 17 RAL 0892: 5F MOV E,A 0893: 7A MOV A,D 0894: 17 RAL 0895: 57 MOV D,A 0896: 79 MOV A,C 0897: 8F ADC A 0898: 4F MOV C,A 0899: C9 RET
Здесь есть своя хитрость. Для сдвига влево двух байт, E и D, использована команда RAL (вращение влево через перенос), а вот для последнего - ADC A (к регистру A прибавляется он же самый, и ещё перенос). Разница между ними - во времени исполнения (ADC чуть дольше) а ещё во флагах, которые меняются. ADC устанавливает не только Carry (как RAL), но ещё и Zero, Sign и прочие.
Именно Sign нас интересует - это, по сути, старший бит результата. Если он установится в единицу, значит, мы, наконец-то, сдвинулись достаточно. Если нет, мы продолжаем итерации по сдвигу, но если их пройдёт уже 32 штуки, то выпрыгнем в FZero, причём это снова Tail Call - там нам установят нулевой результат и выполнение FADD/FSUB завершится - возврат будет сразу туда, откуда они вызваны.
Если же всё успешно, будет ещё небольшой код:
0875: 217201 LXI H,FACCUM+3 ; 0878: 86 ADD M ; 0879: 77 MOV M,A ;Since A was a -ve number, that certainly should 087A: D2BE09 JNC FZero ;have carried, hence the extra check for zero. 087D: C8 RZ ;?why?
Хотя мы считали "число итераций" в регистре H, каждый раз это значение ещё переносилось в A. Сейчас указываем (H,L) на экспоненту "аккумулятора", и прибавляем её к "отрицательному числу итераций". Тем самым, экспонента приобретает правильное значение. Но за такое количество сдвигов она могла стать отрицательной - тогда через Tail call устанавливаем нулевое значение и выходим. И наконец, могла получиться экспонента строго равная нулю, в таком случае ничего выставлять и не надо - если у числа экспонента нулевая, это уже сразу считаем нулём. Поэтому просто Return if Zero, т.е тоже возврат.
О РАДОСТЬ: МЫ ДОБРАЛИСЬ ДО FRoundUp! Сюда мы приходим по обеим ветвям - и при сложении, и при вычитании. Вот он:
087E: 78 FRoundUp MOV A,B ;A=extra mantissa byte 087F: 217201 LXI H,FACCUM+3 ; 0882: B7 ORA A ;If bit 7 of the extra mantissa byte 0883: FC9A08 CM FMantissaInc ;is set, then round up the mantissa.
Проверяем старший бит дополнительного байта мантиссы. Если так, вызываем функцию FMantissaInc:
089A: 1C FMantissaInc INR E 089B: C0 RNZ 089C: 14 INR D 089D: C0 RNZ 089E: 0C INR C 089F: C0 RNZ 08A0: 0E80 MVI C,80h ;Mantissa overflowed to zero, so set it 08A2: 34 INR M ;to 1 and increment the exponent. 08A3: C0 RNZ ;And if the exponent overflows... 08A4: 1E0A Overflow MVI E,0Ah 08A6: C3D501 JMP Error
RNZ - это Return if Not Zero. В общем, к младшему байту мантиссы прибавляем единичку. Если он стал нулём, продолжаем дальше (ситуация наподобие 999 + 1: в младшем разряде вышел ноль, значит, надо и к следующему единичку прибавлять). Сначала ковыряем 3 байта мантиссы. Если даже старший байт стал нулём, мы "ручками" возвращаем ему старший бит и ещё прибавляем единицу к экспоненте. Если же и она стала нулём - мы "своим ходом" оказываемся на метке Overflow, откуда нас и возьмут за белы рученьки.
Заключительные операции
0886: 46 MOV B,M ;B=exponent 0887: 23 INX H ; 0888: 7E MOV A,M ;A=FTEMP_SIGN 0889: E680 ANI 0x80 ; 088B: A9 XRA C ;Bit 7 of C is always 1. Thi 088C: 4F MOV C,A ; 088D: C3120A JMP FLoadFromBCDE ;Exit via copying BCDE to FACCUM.
На этом моменте у нас мантисса лежала в C,D,E,B (в B самый младший "вспомогательный" байт), итоговая экспонента по адресу FAccum+3, и ещё итоговый знак по адресу FTemp.
Сейчас экспоненту всовываем в B, затем в старший бит A запихиваем знак (а остальные биты нас не волнуют), через XOR объединяем его со старшим байтом мантиссы. Там у нас стояла "неявная старшая единица мантиссы" - вот для чего мы её восстанавливали в FMantissaInc - чтобы был понятен результат этого XOR'а.
Ну и всё - тем самым в регистрах B,C,D,E получился упакованный результат. Осталось только перетащить его в FAccum, чем займётся уже известная нам FLoadFromBCDE. Это снова Tail Call, так что возврат из неё - это и будет завершение FADD/FSUB.
Такая вот хреновина...
Вероятно, она чуть более экономична по занимаемому пространству - даже когда мы весь исполняющийся код посчитали, получилось 274 байта. Но здесь гораздо больше процедур, многие из которых задействованы и в других местах этой "математической библиотеки".
Но прямо "идеальной" её не назовёшь - тут и неприятный "наихудший случай", где сложение/вычитание может выполняться ОЧЕНЬ ДОЛГО, и этот хитрючий костыль с наложением команд, который, как оказывается, нахрен здесь не нужен, и ещё пара мест, где сделано неоптимально. Например, что только после 32 безуспешных сдвигов влево выводится заключение "да это же ноль". Вроде как хватило бы и 24 сдвигов, ну если совсем неуютно - то 25. Ведь самые младшие биты могут получиться только если одно число довольно прилично сдвинули вправо. Но если оно так сдвинуто, то при вычитании не может получиться ноль во всех старших разрядах!
Грубо говоря, у нас было число 1000 0000 (двоичное) и второе, 1111 1111, но с экспонентой на единичку меньше. Его мы сдвинули вправо и получили:
1000 0000 - 111 1111 1 --------------- 1
Да, здесь старший байт занулился, но всего один "дополнительный" бит. А если сдвиг на две позиции вправо?
1000 0000 - 11 1111 11 --------------- 100 0000 11
Всё, взяли самое маленькое при текущей экспоненте, вычли самое большое при меньшей на 2 - тут такого зануления быть уже не может.
(хотелось добавить вариант windows must die или что-то похожее - считается, что этот успешно заработавший бейсик и заложил основание для Microsoft, но тогда результат немного ожидаем :))
|
</> |