Podcast on MIPS Instruction Set Architecture Fundamentals
MIPS Instruction Set Architecture Fundamentals: A Guide
Podcast
Návod na MIPS: Od základů k funkcím
Délka: 25 minut
Kapitoly
Co je to MIPS?
Registry: VIP salonek procesoru
Kdy vstupuje do hry paměť?
Urychlení: Okamžité operandy
Logické operace a větvení
Volání funkcí a zásobník
Různé formáty a souhrn
The Magic of Zero
Numbers in Binary
Flipping Signs
Instructions as Code
RISC vs. CISC
The x86 Family Tree
Performance Myths
Assembling the Pieces
Linking and Loading
A Practical Example
Final Takeaways
Přepis
James: Zde je jedna věc, která potrápí 80 % studentů u zkoušek z architektury počítačů – přesně vědět, kdy použít registr a kdy přistupovat k paměti v MIPS assembleru. Dnes se postaráme o to, abyste patřili mezi těch 20 %, kteří to už nikdy neudělají špatně.
Olivia: Přesně tak. Je to ten klíčový rozdíl mezi kódem, který jen funguje, a kódem, který je rychlý a efektivní. A my vám ukážeme, jak na to.
James: Posloucháte Studyfi Podcast. Takže, Olivie, pojďme začít od úplného začátku. Co je to MIPS a proč bychom se o něj měli zajímat?
Olivia: Skvělá otázka. Představte si, že procesor ve vašem počítači mluví svým vlastním jazykem. MIPS je jedním z těchto jazyků – je to instrukční sada. Je to soubor příkazů, kterým procesor rozumí.
James: Jako slovník pro počítač?
Olivia: Přesně! A MIPS je navržen tak, aby byl velmi jednoduchý a pravidelný. To je jeho první velký princip: jednoduchost upřednostňuje pravidelnost.
James: Co to znamená v praxi?
Olivia: Znamená to, že většina instrukcí má stejný formát. Například aritmetické operace, jako je sčítání, vždy používají tři operandy: dva zdroje a jeden cíl. Takže add a, b, c znamená, že vezmete b, přičtěte k němu c a výsledek uložíte do a.
James: Aha, takže žádné složité variace. Vždy cíl, zdroj1, zdroj2.
Olivia: Přesně. Tato pravidelnost usnadňuje hardwaru rychlé provádění instrukcí. Žádná překvapení.
James: Dobře, takže tyhle a, b a c… to nejsou proměnné jako v C nebo Pythonu, že? Kde se nacházejí?
Olivia: Ne tak docela. Nacházejí se v registrech. Představte si registry jako malou, super rychlou sadu pracovních ploch přímo v procesoru. MIPS jich má 32.
James: Proč je prostě neukládáme do hlavní paměti? V té je přece mnohem víc místa.
Olivia: To je druhý klíčový princip designu: menší je rychlejší. Přístup k datům v registru je řádově rychlejší než jejich načítání z hlavní paměti. Je to jako mít ingredience přímo na kuchyňské lince místo toho, abyste pro každou z nich museli běžet do obrovského skladu.
James: Dobře, tenhle příměr se mi líbí. Takže když vidím v MIPS kódu něco jako $t0 nebo $s1, tak to jsou názvy pro tyto registry?
Olivia: Přesně tak. Konvence je, že registry $t – jako $t0 až $t9 – jsou pro dočasné hodnoty, které nepotřebujete dlouhodobě uchovávat. Registry $s – jako $s0 až $s7 – jsou pro uložené hodnoty. Pokud použijete $s registr ve funkci, musíte zajistit, aby na jejím konci měl stejnou hodnotu jako na začátku.
James: Takže pokud mám C kód f = (g + h) - (i + j), jak by to vypadalo v MIPS s použitím registrů?
Olivia: Řekněme, že g a h jsou v registrech $s1 a $s2. Nejprve je sečtete a výsledek uložíte do dočasného registru: add $t0, $s1, $s2.
James: OK, takže $t0 nyní drží g + h.
Olivia: Správně. Pak sečtete i a j – řekněme z $s3 a $s4 – do jiného dočasného registru: add $t1, $s3, $s4. A nakonec odečtete jeden dočasný registr od druhého a výsledek uložíte do konečného cíle, $s0: sub $s0, $t0, $t1.
James: Rozumím. Všechny operace probíhají mezi registry. Žádný přímý přístup do paměti.
Olivia: A teď se dostáváme k jádru věci – k té 80% pasti. Paměť používáte pro data, která jsou příliš velká nebo příliš složitá na to, aby se vešla do registrů. Věci jako pole, struktury nebo dynamická data.
James: Takže pokud chci pracovat s prvkem pole, nemůžu to udělat přímo?
Olivia: Ne. Musíte ho nejprve načíst z paměti do registru. K tomu slouží instrukce load word, neboli lw. A když s ním skončíte, musíte ho uložit zpět do paměti pomocí store word, neboli sw.
James: Takže je to dvoukrokový proces: načíst, zpracovat, uložit.
Olivia: Přesně. Představte si kód v C: g = h + A. Kde A je pole celých čísel.
James: Dobře, takže g a h jsou pravděpodobně v registrech, řekněme $s1 a $s2. Ale A je v paměti.
Olivia: Ano. A paměť je adresována v bajtech. Každé celé číslo v MIPS má 4 bajty. Takže pro přístup k osmému prvku pole (A) potřebujete offset 8 krát 4, což je 32 bajtů od začátku pole.
James: Aha, takže to není jen A + 8.
Olivia: Správně. Pokud je základní adresa pole A v registru $s3, instrukce pro načtení A bude: lw $t0, 32($s3). To znamená: jdi na adresu v $s3, přičti k ní 32 bajtů a načti 4bajtové slovo, které tam najdeš, do registru $t0.
James: A pak už jenom provedu sčítání v registrech: add $s1, $s2, $t0. Teď už to dává smysl! Překladač musí používat registry co nejvíce, aby se vyhnul těmto pomalým operacím s pamětí.
Olivia: A to je ta optimalizace, o které jsme mluvili! Pouze zřídka používané proměnné se „odkládají“ do paměti.
James: Co když chci jen přičíst konstantu, třeba inkrementovat proměnnou o 4? Musím tu čtyřku nejdřív načíst z paměti?
Olivia: To je skvělá otázka a vede nás k třetímu principu designu: zrychli běžné případy. Přičítání malých konstant je velmi časté. Místo načítání z paměti můžete konstantu vložit přímo do instrukce.
James: Jak to funguje?
Olivia: Použijete instrukci add immediate, neboli addi. Takže addi $s3, $s3, 4 vezme hodnotu v $s3, přičte k ní 4 a výsledek uloží zpět do $s3. Rychlé a efektivní.
James: To je chytré. A existuje i subi pro subtract immediate?
Olivia: Kupodivu ne. Prostě použijete addi s negativní konstantou. Takže addi $s2, $s1, -1 funguje naprosto v pořádku.
James: Méně instrukcí, které si musím pamatovat. To se mi líbí.
Olivia: Přesně. Kromě aritmetiky má MIPS také logické operace. To jsou instrukce pro bitové manipulace: and, or, sll (shift left logical) a srl (shift right logical).
James: K čemu jsou dobré?
Olivia: Jsou neuvěřitelně užitečné pro maskování bitů – když chcete například zkontrolovat nebo změnit jen několik bitů v rámci 32bitového slova, aniž byste ovlivnili zbytek. sll, posun doleva, je také super rychlý způsob, jak násobit mocninami dvou.
James: A co rozhodování? Jako příkazy if a while smyčky?
Olivia: K tomu slouží instrukce pro větvení. Nejdůležitější jsou beq, což znamená branch on equal (větev, pokud se rovná), a bne, branch on not equal (větev, pokud se nerovná).
James: Takže beq $s0, $s1, L1 skočí na návěští L1, pokud jsou hodnoty v $s0 a $s1 stejné?
Olivia: Přesně. Pokud nejsou stejné, program prostě pokračuje na další řádek. S těmito dvěma instrukcemi v kombinaci s nepodmíněným skokem j (jump) můžete sestavit jakýkoliv if-else příkaz nebo while smyčku.
James: Takže MIPS nemá instrukce jako branch if less than?
Olivia: Nemá, a to záměrně. Zjišťování rovnosti a nerovnosti je hardwarově mnohem rychlejší než porovnávání „menší než“ nebo „větší než“. Místo toho zkombinujete instrukci slt, set on less than, s bne.
James: Jak to funguje?
Olivia: slt $t0, $s1, $s2 nastaví $t0 na 1, pokud je $s1 menší než $s2, a na 0 v opačném případě. Pak můžete jednoduše použít bne $t0, $zero, L a skočit, pokud byl výsledek 1. Je to kompromis v návrhu – dvě rychlejší instrukce jsou lepší než jedna pomalá.
James: Dobře, takže umíme počítat, pracovat s pamětí a dělat rozhodnutí. Jak je to s voláním funkcí, neboli procedur?
Olivia: To je další klíčová část. Když voláte funkci, musíte udělat několik věcí: předat jí parametry, skočit na její kód a pak se ujistit, že se dokáže vrátit tam, odkud jste ji zavolali.
James: Zní to složitě.
Olivia: Je v tom systém. Pro volání funkce použijete instrukci jal, jump and link. Ta udělá dvě věci: skočí na adresu funkce A uloží adresu *následující* instrukce do speciálního registru $ra, return address.
James: Takže funkce ví, kam se má vrátit!
Olivia: Přesně. A když funkce skončí, jednoduše použije instrukci jr $ra, jump register, aby skočila zpět na adresu uloženou v $ra.
James: A co parametry a návratové hodnoty?
Olivia: Na to existují konvence. Parametry se obvykle předávají v registrech $a0 až $a3. Návratové hodnoty se ukládají do $v0 a $v1.
James: Ale co když funkce, kterou volám, chce také použít registry $t0 nebo $s1? Přepíše mi moje data?
Olivia: Výborná otázka! A tady přichází na řadu zásobník. Zásobník je oblast v paměti, kam si funkce může dočasně „uklidit“ hodnoty z registrů, které potřebuje použít.
James: Jako dočasné úložiště.
Olivia: Přesně. Než funkce použije nějaký $s registr, musí jeho původní hodnotu uložit na zásobník. A těsně předtím, než se vrátí, musí tuto hodnotu ze zásobníku obnovit. Je to jako si půjčit něčí nástroj – musíte ho vrátit ve stejném stavu, v jakém jste si ho vzali.
James: A pokud funkce volá další funkci?
Olivia: Pak se to stává ještě důležitějším. Musí na zásobník uložit nejen $s registry, ale i svůj vlastní návratový registr $ra, protože vnořené volání jal by ho přepsalo!
James: Páni, je v tom hodně co sledovat. Správa zásobníku zní jako klíčová dovednost.
Olivia: Je to naprosto zásadní pro správnou funkci jakéhokoliv netriviálního programu v assembleru. Ale jakmile pochopíte ten princip – ulož, použij, obnov – stane se to druhou přirozeností.
James: Zmínili jsme spoustu různých instrukcí – add, lw, beq, jal. Jak počítač ví, co je co? Jsou všechny zakódovány stejně?
Olivia: Ne, a to je chytrá část designu. Instrukce jsou 32bitová čísla, ale jsou rozděleny do polí. Například R-typ instrukce, jako add, má pole pro zdrojové registry, cílový registr a kód operace. I-typ instrukce, jako lw nebo addi, má zase pole pro registry a 16bitovou okamžitou hodnotu nebo offset.
James: Takže formát instrukce odpovídá tomu, co dělá.
Olivia: Přesně. A to se vrací k prvnímu principu: jednoduchost a pravidelnost. Existuje jen několik formátů, což opět zjednodušuje hardware.
James: Takže abychom to shrnuli. MIPS je jednoduchý, pravidelný jazyk procesoru. Operace se provádějí na datech v super rychlých registrech.
Olivia: Ano. K paměti přistupujeme pouze tehdy, když musíme – pomocí instrukcí load a store – a pamatujeme si, že offsety jsou v bajtech.
James: A pro rozhodování používáme větvení a pro funkce volání s linkem, přičemž zásobník používáme pro bezpečné uložení stavu. Je to tak?
Olivia: Naprosto jsi to pochopil! A teď už víte, jak se vyhnout té 80% pasti. Přemýšlejte o tom, kde jsou vaše data. Pokud mohou být v registru, nechte je tam. Přístup do paměti je drahý a měl by být poslední možností, ne první.
James: Skvělé. To byl fantastický přehled, Olivie. Cítím se mnohem jistější. Myslím, že jsme připraveni přejít k dalšímu tématu.
James: ...and that's how the temporary registers give us that flexibility. But Olivia, there's one special register I've heard about that seems... almost too simple. The zero register?
Olivia: It does sound simple, doesn't it? But it's incredibly useful! Think of register zero, or $zero, as a constant source of the number zero. You can't change it, you can't write to it. It's always just... zero.
James: Okay, so why is that a big deal?
Olivia: It really simplifies common operations. For example, if you want to move the value from one register, say $s1, to another, $t2, you can just add $s1 to zero and store it in $t2. It’s a super-efficient hardware trick.
James: That is clever. It’s like having a permanent tool in your toolbox. So we have registers... but how do we represent numbers, especially negative ones?
Olivia: Great question. The simplest way is with unsigned integers. For a 32-bit number, you can represent anything from 0 all the way up to about 4.2 billion. It’s all positive.
James: But life isn't always positive, right? We need our negative numbers!
Olivia: Exactly! For that, MIPS uses a system called two's complement. Here's the key idea: the most significant bit, which is bit 31, acts as a sign bit. If it's a 1, the number is negative. If it's a 0, it's non-negative.
James: So you just flip one bit to make a number negative? It can't be that easy.
Olivia: It's close! It’s a really elegant process called signed negation. To make a number negative, you first complement it—that means you flip all the zeros to ones and all the ones to zeros—and then you just add one.
James: Wow. And that works every time?
Olivia: It does! This system also leads to another important concept: sign extension. If you need to represent a number using more bits, you just replicate the sign bit to fill the new space. Zeros for positive, ones for negative. It keeps the value the same.
James: Okay, so we have registers and our numbers sorted. How do we turn an instruction like add $t0, $s1, $s2 into something the computer actually gets?
Olivia: That's where we get to the core of it—machine code. Every instruction is encoded into a 32-bit binary word. MIPS keeps things simple with just a few main formats. Think of them as different sentence structures for the computer.
James: So, what's one of the common formats?
Olivia: A really common one is the R-format, for register-type instructions. That 32-bit word is broken into small fields. You have fields for the operation code, the source registers, and the destination register. It's a blueprint for the hardware.
James: And you mentioned there are others?
Olivia: Right, like the I-format for immediate instructions, which include a constant number directly in the instruction itself. The key takeaway is that this regularity makes the hardware much simpler and faster.
James: So it's all about good, efficient design. That makes sense. Now, I've seen these long binary codes also written in something called hexadecimal... what's that all about?
James: So last time we were talking about how compilers can be super clever with optimizing our code. That brings up a bigger question, though.
Olivia: It sure does. We've been talking *about* code, but what does the processor actually *understand*? What's its native language?
James: Exactly! It’s not C++ or Python, that's for sure.
Olivia: Definitely not. The processor's language is its Instruction Set Architecture, or ISA. Think of it as the ultimate vocabulary list for a CPU. Every single thing a processor can do is defined by its ISA.
James: So are all processors fluent in the same language?
Olivia: Great question. No, there are different language families, just like with humans. Two of the biggest are RISC and CISC.
James: Okay, what's the difference there?
Olivia: RISC stands for Reduced Instruction Set Computer. Think of ARM or MIPS processors—the kind you find in your phone. They have a small, highly optimized vocabulary of simple instructions.
James: So, a limited vocabulary but they speak it very, very fast?
Olivia: Exactly. Each instruction does one simple thing. Now, on the other side, you have CISC. That's a Complex Instruction Set Computer.
James: And I'm guessing that's the opposite. A huge vocabulary?
Olivia: You got it. The most famous example is the Intel x86 architecture, which powers most desktops and laptops. A single CISC instruction can do the work of several RISC instructions.
James: Which one is better then? It seems like CISC would be more efficient, right? Fewer instructions to get the job done.
Olivia: It sounds that way, but it's one of the biggest debates in computer architecture! It's a classic tortoise and hare situation.
James: So tell me more about this x86 family. It sounds like it has a long history.
Olivia: Oh, it does. It's all about backward compatibility. It started way back in 1978 with the 16-bit 8086 processor.
James: That's ancient history in computer terms!
Olivia: It is! Then came the 80386, which was a huge leap to 32-bits. After that, we got Pentium, which added multimedia instructions. Then AMD came along and extended it to 64-bits with AMD64.
James: So Intel had to follow AMD's lead on that one?
Olivia: They did! They adopted it as EM64T. This is a key point—if Intel didn't maintain compatibility, one of its competitors would, and developers would flock to that platform.
James: Wow. So the ISA we use today in our laptops is a direct descendant of a chip from the 70s? That's wild.
Olivia: It is! But that long family tree comes with baggage. The x86 instruction encoding is notoriously complex. Instructions can be different lengths, which makes the processor's job harder.
James: Okay, so that brings me back to my question. Is a powerful, complex instruction really better for performance?
Olivia: That's a classic fallacy. You'd think fewer instructions means faster programs, but it's not that simple.
James: How so?
Olivia: A complex instruction might take many, many clock cycles to execute. And that complexity can slow down *all* the instructions, even the simple ones.
James: Ah, so the processor is bogged down by its own giant vocabulary.
Olivia: Pretty much. Modern compilers are incredibly smart. They can take our simple, readable code and break it down into a stream of simple, fast RISC-like instructions that fly through the CPU.
James: So the lesson for us is to not try and be too clever?
Olivia: Exactly. Don't waste your time writing in assembly language thinking you'll beat the compiler. You won't. Write clear, simple code, and let the compiler do its job.
James: That's a relief. It's one less thing to worry about when you're trying to build something great.
Olivia: That's the edge. Focus on the logic, not on outsmarting the silicon. So, to recap, the ISA is the CPU's language, with RISC and CISC being the two main philosophies. And more complex doesn't always mean faster.
James: Got it. That actually clears up a lot. Now, this makes me wonder about how these instructions are actually handled inside the chip. How does pipelining fit into all this?
James: And that really clarifies how instructions are put together. But that brings up a big question, Olivia. How does a program I write in C or Java actually... you know, run?
Olivia: That’s the million-dollar question! It's a whole journey. A compiler or assembler first translates your code into machine instructions, creating something called an object module.
James: An object module... sounds like a building block.
Olivia: Exactly! Think of it this way: you have your code, maybe some pre-written library code... they're all separate blocks. The object module contains the translated instructions, data, and a symbol table to keep track of everything.
James: So the assembler is like the factory that makes the blocks?
Olivia: You got it. And it even has clever shortcuts called pseudoinstructions. For instance, a simple 'move' command is really an 'add' command in disguise. It's a little figment of the assembler's imagination to make the programmer's life easier.
James: I like an assembler with a good imagination.
Olivia: Next, a program called a linker takes all these object modules and merges them. It resolves all the labels and patches everything together into one final, executable file.
James: And *that's* the file I double-click to run?
Olivia: Precisely. When you do, the loader takes over. It reads the file, creates a virtual address space in memory, and copies your program's instructions and data into it.
James: What about things like libraries that lots of programs use?
Olivia: Great question. That’s where dynamic linking comes in. Instead of bundling every library into your executable—which makes it huge—dynamic linking loads the library only when your program actually calls it. It's much more efficient.
James: Can we see how this looks with actual code?
Olivia: Let's look at a simple C function to swap two numbers in an array. In assembly, you'd see instructions to calculate the memory address of the array element, load the values into temporary registers, and then write them back in swapped positions.
James: So each line of C becomes several lines of very specific machine instructions.
Olivia: Exactly. Every step is deliberate. And understanding that process is crucial, especially for more advanced topics like the parallel processor exercises you'll see, which really test your grasp of memory and registers.
James: So to recap... our code is assembled into object modules. A linker stitches them into an executable. Then a loader puts it into memory to run. That's a fantastic overview, Olivia.
Olivia: It’s the magic behind the curtain! Once you see the steps, it’s not so mysterious.
James: Well, that's all the time we have. From computer architecture to algorithms, we've covered a lot. Thanks for guiding us, Olivia. And to our listeners—keep that curiosity burning. You've got this. This is James, for the Studyfi Podcast, signing off.