Znovu smekam klobouk, jak se ti to podarilo navrhnout. Ta instrukcni sada se mi libi. Hlavne instrukce ldis a ddsto v kombinaci s registrem PC.
Kdyz pises o hardwarove delicce - nedala by se udelat delicka, ktera by udelala 4 bity na instrukci? Prvni instrukce by udelala prvni ctyri bity vysledku a naplnila by pomocne stinove registry. Z nich by potom nasledujici tri instukce mohly kazda udelat dalsi 4 bity deleni. Macro DIV by pak vygenerovalo 4 instrukce a deleni by trvalo 4 cykly.
Priznam se bez muceni, kombinacni delicku neumim navrhnout a nevim, jestli by to slo, ale pokud ano, mohlo by to usetrit velke mnozstvi bloku.
nedala by se udelat delicka, ktera by udelala 4 bity na instrukci?
Takhle z hlavy nevím, nepřemýšlel jsem o tom. Já jsem při psaní svých demonstračních a testovacích prostředí potřeboval dělení snad jen na převod binárních hodnot registrů na dekadické číslo pro zobrazení. A taky jsem si chtěl vyzkoušet algoritmus dělení v assembleru. Kdybych to chtěl zrychlit, tak jako první možnost bych zkusil implementaci z assembleru přepsat přímo do hardwaru, cyklus pro jednotlivé bity výsledku bych přidal do konečného automatu v řadiči CPU. Odhaduju, že taková instrukce by mohla být tak 5krát pomalejší než ostatní aritmetické instrukce.
Trochu zkouším svojí pozornost
Skok na adresu uloženou v registru rX lze zapsat jako LD pc, rX, buď nepodmíněnou nebo podmíněnou variantu.
Zdá se mi, že toto by měla být sekvence pro skok na funkci, jejíž adresa je uvedené v paměti na adrese, na kterou odkazuje rX. Tedy například konečné fáze volání přes VMT tabulku v C++.
Jednoduchý skok na adresu uvedenou v registru by pak měl být jen MV pc, rX.
Co se pak týče celočíselného dělení, tak to je celkem zásadní problém a i současná výkonnostní špička vykazuje latence okolo 10 až 12 cyklů na 32-bit dělění. Často přitom tato jednotka zvládá startovat další dělení jen jednou za čtyři cykly a podobně. Intel před lety přišel se schopností počítat dva bity v jednom cyklu a i to byl celkem velký posun vpřed... Takže souhlasím, že celočíselné dělění s garantovaným výsledkem, který se nikdy neliší od dělění s oříznutím na integer dolů je velký problém. Při určité minimální toleranci, která je u plovoucí řádové čárky se aproximace kombinovaná s iterací řeší mnohem lépe.
Pro zajímavost: Sám pro výpočty v naší motion control jednotce LX_RoCoN potřebuji jak dělění tak sinus a cosinus. Dělění jsem vyřešil jako reciprocal, kdy nějdříve zjistím, kolik je nulových bitů v daném čísle, pak ho posunu, spočítám převrácenou hodnotu čísla v rozsahu jedné poloviny MAX_INT a pak provedu korekci. Port firmware ze staršího řešení s architekturou TUMBL (ořezaný open source Microblaze) na RISC-V běžící jako komutační Park, Clarke PMSM koprocesor vedle ARMu na Zynqu obsahuje danou sekvenci na řádce 532 až 554. Vlastní reciprocal je vyhodnocený na paměťově mapované periferii aproximátoru, který používá tabulku s aproximací polynomy druhého řádu a chyba převrácené hodnoty je někde okolo 2^(-20). Funkce se nachází ve vlastní navržené jednoctce lx-fncapprox pro sin, cos, reciprocal. Přitom tabulky jsou zkombinované do dvou vektorů o šířce 36 bitů, které přesně odpovídají vlastnostem BRAM na použitých FPGA (Xilinx XC6SLX9 na LX_RoCoN a Xynq na MZ_APO). Jednotka pak idexaci do tabulky a výpočet polynomu provádí za tři cykly, proto ty NOPy.
Já se přiznám, že mě systém v této instrukční sadě překvapil. Přijde mi to jako velmi, velmi zajímavé - princip je krásně jednoduchý a jednotný. Na druhou stranu asi by nebylo triviální v tom psát přímo, muselo by se minimálně přes makroassembler, protože v té záplavě LD a MV něco číst musí být velmi nepohodlné. Makra JMP, RTI, SUB apod. by čitelnosti hodně napomohla, myslím.
Jak by se např. kopíroval blok paměti o dané délce? Např. na 6502 byl adresní mód zákl. adresa plus registr X. Takže stačilo naplnit X požadovanou délkou, dekrementovat X a smyčka na zero flag už fungovala "sama". Tady sice krásně můžu inkrementovat nebo dekrementovat přímo registry obsahující src a dst, ale zase přijdu o tu "indikaci nulou". To budu muset použít další registr ukazatel konce a v každém kroku smyčky to s ním porovnávat??
Na druhou stranu asi by nebylo triviální v tom psát přímo, muselo by se minimálně přes makroassembler
O makroassembleru bude v dalších pokračováních. Nebo se podívejte na GitHub: definice základních maker (pseudoinstrukcí).
Jak by se např. kopíroval blok paměti o dané délce?
Asi jsem kopírování bloků paměti nepotřeboval, ale podprogram memset_b je podobný algoritmus. Kdyby v r2 byla adresa zdrojového bloku, tak by stačilo do cyklu přidat instrukce LD a INC1:
memcpy: .testz r1 .retz ld r10, r2 stob r0, r10 inc1 r2, r2 inc1 r0, r0 dec1 r1, r1 .jmp memset_b
Díky. A na ten další díl se opravdu těším. Nechcete je vydávat častěji? :)
Takhle musím "podvádět" a koukat napřed na github :) Moc pěkně jsou tam věci zdokumentované, všechna čest. Bohužel jsem zatím neměl dost času to pročíst pořádně, tak se právě těším na ten "sumář" vždy tady v seriálu.
Dál jsou moje úvahy, neberte to jako kritiku, ale jako důkaz toho, jak zajímavě jste to všechno podal, že jste mě přiměl se nad tím trochu víc zamyslet :)
S těmi makry jsem to tak nějak myslel. Jen tedy nevím, jestli implementace všeho je korektní. Např. ten nop pomocí mv nemění náhodou flagy?
Druhý problém je pak s disassemblerem: musí být mnohem složitější. Pokud bych po něm chtěl i převod na ta makra, tak už možná bude tak složitý, že se nevejde ani do celé paměti toho počítače (32kB) a bude muset běhat jen externě na nějakém "větším bráškovi". Zatímco na osmibitech zabíral tak neznatelně paměti, že se s většinou programů vešel do RAM naráz a umožňoval jejich základní debugging bez pomoci jiného stroje.
Kopírování paměti jste nepotřeboval? Asi jste na tom ještě nedělal nějak více grafiku, že? :) A opět: takto nějak jsem si tu smyčku představoval, a přijde mi to trošku jako "plýtvání" - možná je to ale tím, jak jsem "odkojený" M6502: kdybych na takovouto operaci potřeboval čtyři 16bitové registry, tak nevím, co bych tehdy dělal, krom pláče v koutě :) Musely mi stačit dva 8bitové, a z toho ani jeden plnohodnotný (univerzální). A ve smyčce jen tři instrukce místo sedmi. Ale to je právě asi tím úhlem pohledu: když o registry není nouze a jsou "přirozeně" 16bitové, tak se asi programuje jiným stylem...
Např. ten nop pomocí mv nemění náhodou flagy?
Instrukce MV (a podobně např. EXCH, LD, STO) nemění flagy. Z tohoto pohledu může být problematické makro set0 implementované pomocí XOR, které flagy nastavuje.
Druhý problém je pak s disassemblerem
Disassembler jsem vůbec neřešil.
...a bude muset běhat jen externě na nějakém "větším bráškovi"
Celé to mám postavené tak, že assembler a debugger běží vedle na Linuxu a komunikují přes sériový port. Navíc debugger je momentálně jediný způsob, jak do MB50 nahrát a spustit program.
Asi jste na tom ještě nedělal nějak více grafiku, že?
Ne, dělal jsem jen zobrazování textu. Tam je kopírování specifické, protože se kopíruje blok 8 B z fontu do videopaměti s krokem 32 B.
přijde mi to trošku jako "plýtvání" - možná je to ale tím, jak jsem "odkojený" M6502
Já jsem začínal na Z80. Ta má sice registrů víc, ale i tak je potřeba hodně přemýšlet a šetřit. Ale zrovna na kopírování bloků paměti tam byly speciální instrukce. Tady jsem snažil zjednodušit programování právě tím, že bude k dispozici hodně registrů a všechny s plnou šířkou slova.
Dál jsou moje úvahy, neberte to jako kritiku, ale jako důkaz toho, jak zajímavě jste to všechno podal, že jste mě přiměl se nad tím trochu víc zamyslet
Já (konstruktivní) kritiku vítám. A Vaše úvahy, dotazy a připomínky jdou k jádru věci. Já jsem nad návrhem taky docela dlouho přemýšlel, než jsem dospěl k řešení, které mi připadalo rozumné. To ale neznamená, že je nejlepší nebo jediné možné. Je tam dost kompromisů a taky občas pokusy udělat věci jinak, než je obvyklé. Určitě je tam i dost chyb, o některých vím, o jiných ne. Např. už minule se tu diskutovalo, že registr ia neměl být obecný registr, ale CSR. A taky už bylo zmíněno, že při pokusu o implementaci pipeliningu nebo dalších optimalizací známých z výkonnějších CPU by se nejspíš celý návrh na různých místech rozsypal.
Těch věcí tam bude víc:
- Třeba při programování na vyšší úrovni bude běžný zápis a čtení ze struktury. Na to je důležitá relativní adresace. Pokud mi něco neuniklo, tak aktuálně by bylo potřeba LDIS, ADD a LD/ST, místo jednoho LD/ST s relativním offsetem. Ta by taky eliminovala potřebu LDIS/DTSTO - ten příklad se stack se v jazycích vyšší úrovně stejně nepoužívá.
- Malé konstanty pro operace obecně. I když je třeba říct, že jednoduchý LDIS (či ekvivalent) výše tu potřebu trochu eliminuje.
- Na rozšíření by se dal eliminovat část conditions u FLAGS - reálně se používají stejně jen čtyři (? - carry, zero, overflow, ...).
Ale chápu to tak, že cílem bylo postavit jednoduchý CPU, který bude relativně jednoduché implementovat, což se asi podařilo.
Třeba při programování na vyšší úrovni bude běžný zápis a čtení ze struktury.
Tohle už je za hranicí mého projektu. Kdybych takové operace chtěl dělat, ať už v assembleru nebo v kompilátoru nějakého vyššího programovacího jazyka, tak bych si na ty tři instrukce (LDIS+ADD+LD/STO) vyrobil makro. Teprve, kdybych to potřeboval zrychlit, tak bych uvažoval o přidání dalších instrukcí.
ten příklad se stack se v jazycích vyšší úrovně stejně nepoužívá
Tohle jsem nepochopil: tvrdíte, že ve vyšších jazycích se nepoužívá stack?
Malé konstanty pro operace obecně.
Chtěl jsem udržet jednotný dvoubajtový formát instrukcí obsahující opcode a dva registry. Tam už na konstanty není místo.
Na rozšíření by se dal eliminovat část conditions u FLAGS
Redukce počtu flagů, které lze testovat z 8 na 4 by v kódu instrukce ušetřila jeden bit, navíc jen v podmíněných instrukcích.
Ale chápu to tak, že cílem bylo postavit jednoduchý CPU
Přesně tak. A každá další instrukce znamená vymyslet, jak se má chovat, přidat její popis do README, implementovat v CPU (přidat do ALU a/nebo do konečného automatu řadiče) a přidat do assembleru.
Předpokládám, že je to míněno tak, že se k zásobníku nepřistupuje přes operace push a pop. Ono tím, jak vytvářejí řetězce závislostí instrukcí, tak jsou vysloveně problematické na out-of-order zpracování. Vždy se tedy zásobníkem posunuje o větší inkrement s rezervou na přípravu argumentů funkcí. Na x86_64 je podle System V Application Binary Interface AMD64 Architecture Processor Supplement je třeba povinné zarovnání stacku na 16 byte a dokonce 128 byte nad, tedy adresami pod, koncem stacku je rezervovaný po přípravu argumentům funkcím a lokální proměnné které nepřežijí volání funkce.
Pokud pak není využité alloca (allocate memory that is automatically freed tedy s opuštěním funkce) nebo lokální pole s délkou určenou argumenty, tak se lokální proměnné adresují relativně přímo proti zásobníku. Pokud se předpokládá, že se ukazatel zásobníku bude relativně k zásobníkovému rámci funkce pohybovat nebo jsou standardní rámce vyžadované pro snadnější ladění, tak je na začátku na pozici zásobníku nad (pod) návratovou adresu z funkce uložený předchozí stav frame pointeru (fp), ten pak ukazuje na tuto pozici a adresuje se proti němu (na x86 registr bottom pointer - bp). Na RISC architekturách se pak návratová adresa z funkce neukládá instrukcí call (/bsr/jal jak jí kdo pojmenuje) na zásobník, ale do k tomu určenému registru - return address ra, link registr lr atd...
Takže relativní adresování v nějakém rozumném rozsahu proti fp a sp je nutností. Přitom i při přístupu přes ukazatele do struktur je to také potřeba. RISC-V rozsah offsetů nechává na -2048 až +2047.
Dokonce i v 16 bitovém, compressed RIC-V "C" kódování jsou obsažené instrukce pro zarovaná čtení a zápisy přes registr sp pro načtení 32-bit, 64-bit (na 128-bitové verzi i pro 128 bit) hodnoty dané dokonce 6 bitovým ofsetem k sp, který je škálovaný délkou přístupu. Přes subset dalších 8 registrů (s0, s1, a0 až a5) je pak možné v komresené sadě přistupovat jen s offsetem 5 bitů. Pro běžnou aritmetiku je pak nabízený šestibitový immediate. Je celkem zajímavé, co do 16 bitů kódování dokázali dostat, ale na druhou stranu kompletní sada to není, je potřeba mít k dispozici i 32-bit kódované instrukce, třena pro řízení stavu a přístupy k CSR.
Díky za upřesnění, s tím stack jsem to skutečně myslel tak, že se alokuje jednorázově.
Zaujal mě ten offset u RISC-V - skoro bych zvážil udělat ten rozsah asymetricky. Typicky se přistupuje ke kladným offsetům a záporné možná v rosahu 1-2 prvků (krát jejich velikost). U těch 12-bitových je to možná jedno, ale v compressed jenom 5 bitů bude často limitující. Nebo udělat compressed pouze kladné (možná už to tak je?)...
Je to tak, offset u komprimovaných instrukcí pracujících proti zásobníku je zero extended
C.LWSP loads a 32-bit value from memory into register rd. It computes an effective address by adding the zero-extended offset, scaled by 4, to the stack pointer, x2. It expands to lw rd, offset(x2). C.LWSP is only valid when rd≠x0 the code points with rd=x0 are reserved.
Zásobník je pak při vstupu do podprogramu povinně zarovnaný na 128 bitů, tedy 16 byte. Přitom prostor nad (v adresách pod) sp funkci nepatří. U x86 to asi i celkem dost pro to offsetování s krátkou hodnotou řeší tour red-zone nad stackem. Ale to je dost komplikací jinde.
Zajímavé jsou pak na optimalizaci délky kódu navržené instrukce v rozšíření Zcmp: push, pop and paired register atomic move. I když zde to již trochu přestává být RISC architektura. Jedná se o kompromis pro embedded svět, kde se každý byte Flash počítá.