x86-64 vs RISC-V

Tur berpemandu dua ISA besar, bukan perang: sejarah, gaya encoding, dan konsekuensinya bagi pemrogram.

Daftar isi

Topik ini berisi tur berpemandu ke dua ISA, singkatan dari instruction set architecture yang berarti kontrak instruksi antara perangkat keras dan perangkat lunak, yaitu x86-64 dan RISC-V, tanpa maksud memenangkan salah satunya. Tur berhenti di tiga tempat, yaitu silsilah kedua arsitektur, gaya encoding yang menjadi wajah khas masing-masing, dan satu fungsi C kecil yang diperlihatkan dalam dua pencahayaan berbeda. Seluruh buktinya keluaran nyata, mulai dari disassembly, yaitu pembacaan balik kode mesin menjadi daftar instruksi, hasil kerja gcc, hingga jalannya program RISC-V pada simulator.

Dua silsilah yang berbeda#

x86-64 adalah keturunan langsung dari 8086, prosesor 16 bit yang diperkenalkan Intel pada 1978. Setiap generasi berikutnya mewarisi tugas yang sama, yaitu tetap menjalankan perangkat lunak lama, sehingga pola instruksi 16 bit dan 32 bit masih hidup di dalam prosesor 64 bit hari ini. Perluasan 64 bit diperkenalkan AMD pada 2003 dengan nama AMD64, kemudian diadopsi Intel, dan kini dipakai hampir seluruh laptop, desktop, serta server. Nama x86-64, x64, dan AMD64 menunjuk keluarga instruksi yang sama.

RISC-V lahir dari arah sebaliknya. Proyek ini dimulai di University of California, Berkeley pada 2010 sebagai arsitektur yang dirancang ulang dari nol tanpa beban kompatibilitas, dan kini dikelola RISC-V International, badan standar terbuka yang berkedudukan di Swiss. Dasarnya bernama RV32I, yaitu set instruksi 32 bit yang kecil, sedangkan kemampuan tambahan datang sebagai ekstensi yang dirinci pada tabel di bawah.

Cara mengelola standarnya pun berbeda. x86-64 hanya dapat diimplementasikan dengan lisensi dari pemiliknya, sedangkan RISC-V dirilis sebagai open standard, yaitu spesifikasi yang bebas diimplementasikan siapa pun tanpa royalti. ARM, penyedia prosesor ponsel, berdiri di antara keduanya dengan model lisensi yang menyerupai jalur x86. Tabel berikut merangkum perbedaan yang dibedah pada bagian-bagian berikutnya.

Aspekx86-64RISC-V
Encodingpanjang instruksi bervariasi 1 hingga 15 bytetetap 4 byte, ekstensi C menambah bentuk 2 byte
Dekoderrumit dan mahal, membongkar instruksi lama menjadi operasi internal sederhanasederhana karena batas tiap instruksi selalu jelas
Ukuran kodepadat pada operasi yang menyentuh memorilebih panjang pada RV32I, memampat kembali dengan ekstensi C
Ekstensibilitasditambahkan oleh Intel dan AMD sesuai kebutuhan keduanyaekstensi resmi M, A, F, D, C, V, dan G, ditambah ekstensi kustom
Model bisnislisensi tertutup dari pemilik patennyaopen standard bebas royalti
Ekosistemmatang, hampir semua perangkat lunak desktop dan server tersedialebih muda, dukungan Linux dan toolchain terus bertambah

Dua baris pertama tabel adalah keputusan teknis yang saling berkaitan, yaitu kerumitan dekoder dan panjang encoding, sedangkan baris sisanya menyangkut organisasi di balik arsitektur. Tiga bagian berikutnya membedah perbedaan itu dengan keluaran nyata, dimulai dari fungsi C kecil yang sama pada kedua sisi.

Satu fungsi C, dua pencahayaan#

Perbandingan yang paling adil dimulai dari fungsi yang sama pada kedua sisi, lalu hasil keduanya dibaca langsung tanpa jembatan penengah. Berkas berikut memuat dua fungsi kecil, yaitu tambah_skor yang menambahkan bonus ke sel memori yang ditunjuk pointer lalu mengembalikan nilainya, dan konstanta yang mengembalikan nilai besar 100000 untuk menyorot cara tiap arsitektur menanamkan konstanta besar ke dalam instruksi. Kompilasi menargetkan x86-64 dengan gcc, dan disassembly-nya dibaca dengan objdump.

c tambah-skor.c
// tidak-dijalankan
// tambah-skor.c - fungsi kecil yang sama dalam dua pencahayaan ISA

int tambah_skor(int *skor, int bonus) {
    *skor = *skor + bonus;
    return *skor;
}

int konstanta(void) {
    return 100000;
}
bash
# kompilasi ke berkas objek dengan opsi yang menstabilkan keluaran
gcc -O1 -fno-pie -no-pie -fno-asynchronous-unwind-tables -c tambah-skor.c -o tambah-skor.o
# tampilkan disassembly berkas objek
objdump -d tambah-skor.o
text Keluaran

tambah-skor.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <tambah_skor>:
   0:	89 f0                	mov    %esi,%eax
   2:	03 07                	add    (%rdi),%eax
   4:	89 07                	mov    %eax,(%rdi)
   6:	c3                   	ret

0000000000000007 <konstanta>:
   7:	b8 a0 86 01 00       	mov    $0x186a0,%eax
   c:	c3                   	ret

Kolom tengah keluaran memuat byte instruksi sebagaimana dilihat prosesor, dan panjangnya langsung terlihat tidak seragam. Instruksi ret hanya satu byte, instruksi add dan pasangan mov-nya dua byte, sedangkan mov $0x186a0,%eax menempati lima byte karena konstanta 100000 membutuhkan empat byte tambahan di dalam instruksinya. Instruksi add (%rdi),%eax memperlihatkan ciri khas x86-64, yaitu operand memori, yakni kemampuan aritmetika mengambil satu operand langsung dari memori tanpa instruksi muat terpisah. Badan tambah_skor karena itu hanya menempati tujuh byte untuk empat instruksi.

Notasi keluaran di atas mengikuti gaya AT&T yang dipakai objdump, yaitu operand tujuan ditulis paling kanan, sehingga add (%rdi),%eax berarti menambahkan isi alamat pada rdi ke dalam eax. Alamat pada kolom kiri bergerak 0, 2, 4, 6, lalu 7 dan c, karena setiap instruksi membawa panjangnya sendiri. Tidak ada tanda batas antarinstruksi pada deretan byte-nya, dan pembacaannya menuntut penafsiran dari awal berkas.

Sisi RISC-V melalui simulator#

Sandbox tempat topik ini ditulis tidak memiliki toolchain RISC-V native, yaitu rangkaian compiler dan assembler yang menerjemahkan C menjadi instruksi RISC-V untuk dijalankan langsung. Padanan RV32I dari tambah_skor karenanya ditulis tangan dan dijalankan pada simulator kecil yang disertakan dalam bundle sebagai sim.py, yang menafsirkan subset RV32I beserta ecall gaya RARS, yaitu layanan pencetakan dan keluar program yang nomornya ditaruh pada register a7. Daftar instruksinya bukan keluaran compiler, melainkan daftar yang disusun agar bentuknya terlihat jernih.

python sim.py
#!/usr/bin/env python3
"""sim.py — simulator RV32I subset untuk pembelajaran arsitektur komputer.

Pemakaian:
    python3 sim.py [--trace] program.s
    python3 sim.py            (tanpa argumen: cetak bantuan)

Cakupan:
  - Instruksi dasar RV32I (aritmetika, logika, shift, branch, jump,
    load/store, lui, auipc) beserta pseudo-instruksi umum.
  - Direktif: .text, .data, .globl, .align, .word, .byte, .asciz, .space.
  - ecall gaya RARS: a7=1 cetak bilangan bulat (a0), a7=4 cetak string
    di alamat a0, a7=11 cetak karakter (a0), a7=10 keluar.
  - sp (x2) diinisialisasi ke 0x10000; memori di-nol-kan; pc mulai 0.
"""
from __future__ import annotations

import re
import sys

MASK32 = 0xFFFFFFFF
STACK_TOP = 0x10000
STEP_LIMIT = 1_000_000

REGS = {"zero": 0, "ra": 1, "sp": 2, "gp": 3, "tp": 4,
        "t0": 5, "t1": 6, "t2": 7, "s0": 8, "fp": 8, "s1": 9,
        "a0": 10, "a1": 11, "a2": 12, "a3": 13, "a4": 14,
        "a5": 15, "a6": 16, "a7": 17,
        "s2": 18, "s3": 19, "s4": 20, "s5": 21, "s6": 22,
        "s7": 23, "s8": 24, "s9": 25, "s10": 26, "s11": 27,
        "t3": 28, "t4": 29, "t5": 30, "t6": 31}
for _i in range(32):
    REGS[f"x{_i}"] = _i

INT_RE = re.compile(r"^[+-]?(0[xX][0-9a-fA-F]+|0[bB][01]+|\d+)$")
MEMOP_RE = re.compile(r"^([+-]?(?:0[xX][0-9a-fA-F]+|0[bB][01]+|\d+))\s*\(\s*([a-zA-Z0-9]+)\s*\)$")


class SimError(Exception):
    pass


def reg(name: str) -> int:
    key = name.strip().lower()
    if key not in REGS:
        raise SimError(f"register tidak dikenal `{name}`")
    return REGS[key]


def to_int(text: str) -> int:
    s = text.strip().replace("_", "")
    if not INT_RE.match(s):
        raise SimError(f"angka tidak valid `{text}`")
    neg = s.startswith("-")
    s = s.lstrip("+-")
    value = int(s, 0)
    return -value if neg else value


def signed(value: int) -> int:
    value &= MASK32
    return value - (1 << 32) if value & 0x80000000 else value


def u32(value: int) -> int:
    return value & MASK32


class Program:
    def __init__(self) -> None:
        self.labels: dict[str, int] = {}
        self.instructions: list[tuple[int, str, str]] = []  # (pc, teks, mnemonic)
        self.data: dict[int, int] = {}  # alamat byte -> nilai byte


def parse_source(text: str) -> Program:
    prog = Program()
    pc = 0
    dataptr = None  # None = sedang tidak menandai alamat data berikutnya
    pending: list[str] = []

    def flush_labels() -> None:
        for name in pending:
            if name in prog.labels:
                raise SimError(f"label duplikat `{name}`")
            prog.labels[name] = dataptr if dataptr is not None else pc

    for lineno, raw in enumerate(text.split("\n"), start=1):
        line = raw.split("#", 1)[0].strip()
        if not line:
            continue
        pending = []
        while True:
            m = re.match(r"^([A-Za-z_.][\w.]*)\s*:\s*(.*)$", line)
            if not m:
                break
            pending.append(m.group(1))
            line = m.group(2).strip()
        if not line:
            flush_labels()
            continue
        parts = line.split(None, 1)
        head = parts[0].lower()
        rest = parts[1].strip() if len(parts) > 1 else ""
        if head.startswith("."):
            flush_labels()
            if head in (".text", ".data", ".globl", ".global", ".section"):
                if head == ".text":
                    dataptr = None
                elif head == ".data":
                    dataptr = pc
                continue
            if head == ".align":
                align = 1 << to_int(rest)
                if dataptr is not None:
                    while dataptr % align:
                        prog.data[dataptr] = 0
                        dataptr += 1
                else:
                    pad = (align - pc % align) % align
                    pc += pad
                continue
            if head in (".word", ".byte", ".asciz", ".string", ".space", ".zero"):
                addr = pc if dataptr is None else dataptr
                addr_end = addr
                if head == ".space" or head == ".zero":
                    addr_end = addr + to_int(rest)
                elif head in (".asciz", ".string"):
                    s = rest.strip()
                    if not (s.startswith('"') and s.endswith('"')):
                        raise SimError(f"baris {lineno}: .asciz butuh string berkutip")
                    body = s[1:-1].encode("latin-1", "backslashreplace").decode("latin-1")
                    for ch in body + "\x00":
                        prog.data[addr_end] = ord(ch) & 0xFF
                        addr_end += 1
                else:
                    width = 4 if head == ".word" else 1
                    items = [t.strip() for t in rest.split(",") if t.strip()]
                    for item in items:
                        value = to_int(item) & (MASK32 if width == 4 else 0xFF)
                        for k in range(width):
                            prog.data[addr_end] = (value >> (8 * k)) & 0xFF
                            addr_end += 1
                if dataptr is None:
                    pc = addr_end
                else:
                    dataptr = addr_end
                continue
            raise SimError(f"baris {lineno}: direktif tidak didukung `{head}`")
        # instruksi
        flush_labels()
        if dataptr is not None:
            raise SimError(f"baris {lineno}: instruksi setelah label data")
        prog.instructions.append((pc, line, head))
        pc += 4
    return prog


def operands(rest: str) -> list[str]:
    if not rest:
        return []
    return [t.strip() for t in rest.split(",") if t.strip()]


class Sim:
    def __init__(self, prog: Program, trace: bool) -> None:
        self.prog = prog
        self.trace = trace
        self.regs = [0] * 32
        self.regs[2] = STACK_TOP
        self.mem: dict[int, int] = dict(prog.data)
        self.pc = 0
        self.out: list[str] = []

    def read_byte(self, addr: int) -> int:
        return self.mem.get(addr & MASK32, 0)

    def read(self, addr: int, width: int) -> int:
        if addr % width:
            raise SimError(f"akses tidak sejajar: alamat 0x{addr & MASK32:08x} lebar {width}")
        value = 0
        for k in range(width):
            value |= self.read_byte(addr + k) << (8 * k)
        return value

    def write(self, addr: int, width: int, value: int) -> None:
        if addr % width:
            raise SimError(f"akses tidak sejajar: alamat 0x{addr & MASK32:08x} lebar {width}")
        for k in range(width):
            self.mem[(addr + k) & MASK32] = (value >> (8 * k)) & 0xFF

    def label(self, name: str) -> int:
        key = name.strip()
        if key not in self.prog.labels:
            raise SimError(f"label tidak dikenal `{name}`")
        return self.prog.labels[key]

    def emit(self, text: str) -> None:
        self.out.append(text)

    def do_ecall(self) -> bool:
        service = signed(self.regs[17])
        a0 = self.regs[10]
        if service == 1:
            self.emit(str(signed(a0)))
        elif service == 4:
            chars = []
            addr = a0
            while True:
                byte = self.read_byte(addr)
                if byte == 0:
                    break
                chars.append(chr(byte))
                addr += 1
            self.emit("".join(chars))
        elif service == 11:
            self.emit(chr(a0 & 0xFF))
        elif service == 10:
            return True
        else:
            raise SimError(f"ecall layanan {service} tidak didukung")
        return False

    def step(self) -> bool:
        """Jalankan satu instruksi; kembalikan True bila program selesai."""
        index = self.pc // 4
        if index < 0 or index >= len(self.prog.instructions):
            raise SimError(f"pc 0x{self.pc & MASK32:08x} berada di luar instruksi "
                           "(program berakhir tanpa ecall exit?)")
        pc, text, mnem = self.prog.instructions[index]
        if self.trace:
            print(f"pc=0x{pc & MASK32:08x}  {text}")
        args = operands(text.split(None, 1)[1] if len(text.split(None, 1)) > 1 else "")
        r = self.regs

        def imm_load(rd: int, value: int) -> None:
            r[rd] = u32(value)

        if mnem in ("addi", "slti", "sltiu", "xori", "ori", "andi", "slli", "srli", "srai"):
            rd, rs1 = reg(args[0]), reg(args[1])
            imm = to_int(args[2])
            if mnem == "addi":
                imm_load(rd, r[rs1] + imm)
            elif mnem == "slti":
                imm_load(rd, 1 if signed(r[rs1]) < imm else 0)
            elif mnem == "sltiu":
                imm_load(rd, 1 if r[rs1] < u32(imm) else 0)
            elif mnem == "xori":
                imm_load(rd, r[rs1] ^ imm)
            elif mnem == "ori":
                imm_load(rd, r[rs1] | imm)
            elif mnem == "andi":
                imm_load(rd, r[rs1] & imm)
            elif mnem == "slli":
                imm_load(rd, r[rs1] << (imm & 31))
            elif mnem == "srli":
                imm_load(rd, r[rs1] >> (imm & 31))
            else:
                imm_load(rd, signed(r[rs1]) >> (imm & 31))
        elif mnem in ("add", "sub", "sll", "slt", "sltu", "xor", "srl", "sra", "or", "and"):
            rd, rs1, rs2 = reg(args[0]), reg(args[1]), reg(args[2])
            if mnem == "add":
                imm_load(rd, r[rs1] + r[rs2])
            elif mnem == "sub":
                imm_load(rd, r[rs1] - r[rs2])
            elif mnem == "sll":
                imm_load(rd, r[rs1] << (r[rs2] & 31))
            elif mnem == "slt":
                imm_load(rd, 1 if signed(r[rs1]) < signed(r[rs2]) else 0)
            elif mnem == "sltu":
                imm_load(rd, 1 if r[rs1] < r[rs2] else 0)
            elif mnem == "xor":
                imm_load(rd, r[rs1] ^ r[rs2])
            elif mnem == "srl":
                imm_load(rd, r[rs1] >> (r[rs2] & 31))
            elif mnem == "sra":
                imm_load(rd, signed(r[rs1]) >> (r[rs2] & 31))
            elif mnem == "or":
                imm_load(rd, r[rs1] | r[rs2])
            else:
                imm_load(rd, r[rs1] & r[rs2])
        elif mnem == "lui":
            imm_load(reg(args[0]), to_int(args[1]) << 12)
        elif mnem == "auipc":
            imm_load(reg(args[0]), pc + (to_int(args[1]) << 12))
        elif mnem in ("beq", "bne", "blt", "bge", "bltu", "bgeu"):
            rs1, rs2 = reg(args[0]), reg(args[1])
            target = self.label(args[2])
            taken = {"beq": r[rs1] == r[rs2], "bne": r[rs1] != r[rs2],
                     "blt": signed(r[rs1]) < signed(r[rs2]),
                     "bge": signed(r[rs1]) >= signed(r[rs2]),
                     "bltu": r[rs1] < r[rs2], "bgeu": r[rs1] >= r[rs2]}[mnem]
            if taken:
                self.pc = target
                return False
        elif mnem == "jal":
            if len(args) == 1:
                rd, target = 1, self.label(args[0])
            else:
                rd, target = reg(args[0]), self.label(args[1])
            imm_load(rd, pc + 4)
            self.pc = target
            return False
        elif mnem == "jalr":
            if len(args) == 1:
                rd, base, off = 1, reg(args[0]), 0
            else:
                rd, base = reg(args[0]), reg(args[1])
                off = to_int(args[2]) if len(args) > 2 else 0
            dest = (r[base] + off) & MASK32 & ~1
            imm_load(rd, pc + 4)
            self.pc = dest
            return False
        elif mnem in ("lb", "lh", "lw", "lbu", "lhu"):
            rd, m = reg(args[0]), MEMOP_RE.match(args[1])
            if not m:
                raise SimError(f"operand memori tidak valid `{args[1]}`")
            addr = u32(r[reg(m.group(2))] + to_int(m.group(1)))
            width = {"lb": 1, "lbu": 1, "lh": 2, "lhu": 2, "lw": 4}[mnem]
            value = self.read(addr, width)
            if mnem in ("lb", "lh") and value & (1 << (width * 8 - 1)):
                value -= 1 << (width * 8)
            imm_load(rd, value)
        elif mnem in ("sb", "sh", "sw"):
            rs2, m = reg(args[0]), MEMOP_RE.match(args[1])
            if not m:
                raise SimError(f"operand memori tidak valid `{args[1]}`")
            addr = u32(r[reg(m.group(2))] + to_int(m.group(1)))
            self.write(addr, {"sb": 1, "sh": 2, "sw": 4}[mnem], r[rs2])
        elif mnem == "ecall":
            if self.do_ecall():
                return True
        elif mnem == "ebreak":
            return True
        else:
            # pseudo-instruksi
            if mnem == "nop":
                pass
            elif mnem == "li":
                imm_load(reg(args[0]), to_int(args[1]))
            elif mnem == "la":
                imm_load(reg(args[0]), self.label(args[1]))
            elif mnem == "mv":
                imm_load(reg(args[0]), r[reg(args[1])])
            elif mnem == "not":
                imm_load(reg(args[0]), ~r[reg(args[1])])
            elif mnem == "neg":
                imm_load(reg(args[0]), -r[reg(args[1])])
            elif mnem == "seqz":
                imm_load(reg(args[0]), 1 if r[reg(args[1])] == 0 else 0)
            elif mnem == "snez":
                imm_load(reg(args[0]), 1 if r[reg(args[1])] != 0 else 0)
            elif mnem == "sltz":
                imm_load(reg(args[0]), 1 if signed(r[reg(args[1])]) < 0 else 0)
            elif mnem == "j":
                self.pc = self.label(args[0])
                return False
            elif mnem == "jr":
                self.pc = r[reg(args[0])] & MASK32 & ~1
                return False
            elif mnem == "ret":
                self.pc = r[1] & MASK32 & ~1
                return False
            elif mnem == "call":
                target = self.label(args[0])
                imm_load(1, pc + 4)
                self.pc = target
                return False
            elif mnem in ("beqz", "bnez", "bgez", "bltz", "bgtz", "blez"):
                rs1 = reg(args[0])
                taken = {"beqz": r[rs1] == 0, "bnez": r[rs1] != 0,
                         "bgez": signed(r[rs1]) >= 0, "bltz": signed(r[rs1]) < 0,
                         "bgtz": signed(r[rs1]) > 0, "blez": signed(r[rs1]) <= 0}[mnem]
                if taken:
                    self.pc = self.label(args[1])
                    return False
            elif mnem in ("bgt", "ble", "bgtu", "bleu"):
                rs1, rs2 = reg(args[0]), reg(args[1])
                taken = {"bgt": signed(r[rs1]) > signed(r[rs2]),
                         "ble": signed(r[rs1]) <= signed(r[rs2]),
                         "bgtu": r[rs1] > r[rs2], "bleu": r[rs1] <= r[rs2]}[mnem]
                if taken:
                    self.pc = self.label(args[2])
                    return False
            else:
                raise SimError(f"instruksi tidak dikenal `{mnem}`")
        self.pc = u32(self.pc + 4)
        return False

    def run(self) -> None:
        for _ in range(STEP_LIMIT):
            if self.step():
                return
        raise SimError("batas langkah terlampaui (kemungkinan perulangan tak berujung)")


def main() -> None:
    argv = sys.argv[1:]
    trace = "--trace" in argv
    argv = [a for a in argv if a != "--trace"]
    if not argv:
        print("Pemakaian: python3 sim.py [--trace] program.s")
        return
    try:
        with open(argv[0], encoding="utf-8") as fh:
            source = fh.read()
    except OSError as exc:
        print(f"sim: {exc}", file=sys.stderr)
        raise SystemExit(1)
    try:
        prog = parse_source(source)
        if not prog.instructions:
            raise SimError("tidak ada instruksi untuk dijalankan")
        sim = Sim(prog, trace)
        sim.run()
        for line in sim.out:
            print(line)
    except SimError as exc:
        print(f"sim: {exc}", file=sys.stderr)
        raise SystemExit(1)


if __name__ == "__main__":
    main()
text Keluaran
Pemakaian: python3 sim.py [--trace] program.s

Program berikut memuat padanan RV32I dari fungsi yang sama. Nilai awal skor dan bonus diletakkan pada bagian .data, lalu badan programnya memuat keduanya ke register, menjumlahkan, dan menyimpan hasilnya kembali ke memori. Dua instruksi terakhir hanya melayani pencetakan hasil melalui ecall dan tidak menjadi bagian perhitungannya.

asm tambah-skor.s
# tidak-dijalankan
# tambah-skor.s - padanan RV32I dari fungsi tambah_skor pada tambah-skor.c

        .data
skor:   .word 20                # nilai awal skor di memori
bonus:  .word 7                 # nilai bonus di memori

        .text
        .globl main
main:
        la t0, skor             # alamat sel skor ke t0
        lw t1, 0(t0)            # muat skor dari memori ke register
        la t2, bonus            # alamat sel bonus ke t2
        lw t3, 0(t2)            # muat bonus dari memori ke register
        add t1, t1, t3          # jumlahkan keduanya di register
        sw t1, 0(t0)            # simpan hasil kembali ke memori
        mv a0, t1               # hasil ke a0 untuk dicetak
        li a7, 1                # layanan cetak bilangan
        ecall                   # cetak 27
        li a7, 10               # layanan keluar
        ecall
bash
# menjalankan padanan RV32I melalui simulator
python3 sim.py tambah-skor.s
text Keluaran
27

Bentuknya kini memperlihatkan pola load/store, yaitu prinsip yang memisahkan akses memori dari aritmetika sehingga instruksi add hanya menerima operand register. Instruksi lw memuat skor dari memori, add menjumlahkannya dengan bonus di register, dan sw menyimpan hasilnya kembali, sedangkan x86-64 menyelesaikan penambahan serupa hanya dengan satu instruksi add yang menyentuh memori. Baris la, li, dan mv adalah pseudo-instruksi, yaitu bentuk singkat yang dijabarkan assembler menjadi instruksi dasar, sehingga jumlah baris berkas tidak sama dengan jumlah instruksi mesinnya.

Panjang instruksi: bervariasi versus tetap#

Perbedaan encoding paling mudah dilihat dari jejak program counter. Opsi --trace pada simulator mencetak satu baris untuk setiap instruksi yang dieksekusi beserta alamatnya, sehingga langkah program counter dapat dibaca persis. Perintahnya tetap memakai berkas yang sama, hanya opsi pelacaknya ditambahkan.

bash
# jalankan ulang dengan pelacakan program counter
python3 sim.py --trace tambah-skor.s
text Keluaran
pc=0x00000000  la t0, skor
pc=0x00000004  lw t1, 0(t0)
pc=0x00000008  la t2, bonus
pc=0x0000000c  lw t3, 0(t2)
pc=0x00000010  add t1, t1, t3
pc=0x00000014  sw t1, 0(t0)
pc=0x00000018  mv a0, t1
pc=0x0000001c  li a7, 1
pc=0x00000020  ecall
pc=0x00000024  li a7, 10
pc=0x00000028  ecall
27

Alamat pada keluaran ini naik dengan langkah tetap, yaitu 0x00, 0x04, 0x08, dan seterusnya sampai 0x28, karena setiap instruksi RV32I menempati tepat 4 byte. Kontrasnya dengan alamat disassembly x86-64 pada bagian sebelumnya yang melangkah 1, 2, dan 5 byte mengikuti panjang tiap instruksinya. Instruksi x86-64 dapat panjang 1 sampai 15 byte tanpa tanda batas antarinstruksi, sehingga dekoder yang besar diperlukan untuk menemukan awal setiap instruksinya. Prosesor x86-64 modern bahkan menerjemahkan hasilnya terlebih dahulu menjadi micro-op, yaitu operasi internal sederhana, sebelum dieksekusi. Dekoder RISC-V tidak memikul beban serupa karena batas instruksinya dapat ditemukan tanpa menafsirkan isinya.

Ukuran kode menjadi konsekuensi langsungnya. Badan tambah_skor versi x86-64 menempati 7 byte, sedangkan empat instruksi padanannya pada RV32I menempati 16 byte. Konstanta 100000 mempertajam perbedaan itu, karena x86-64 menanamkannya dalam satu instruksi 5 byte, tetapi RV32I yang hanya menyediakan immediate, yaitu konstanta yang menyertai instruksi, selebar 12 bit, sehingga konstanta itu harus disusun dari pasangan lui dan addi. Ekstensi C mempersempit jurang ini dengan memampatkan instruksi yang sering dipakai menjadi 2 byte. Perbandingan ukuran di atas berlaku untuk dua contoh kecil ini dan bukan tolok ukur umum, karena hasilnya berubah mengikuti compiler serta tingkat optimasinya.

Konsekuensi bagi pemrogram#

Bagi pemrogram C, kedua ISA berperilaku hampir serupa di atas permukaan. Kode sumber yang sama dikompilasi ke keduanya tanpa penyuntingan pada kasus umum, karena compiler yang menyesuaikan diri dengan kontrak setiap ISA. Perbedaan baru terasa ketika bahasa assembly dibaca atau ditulis, misalnya saat menganalisis disassembly atau menelusuri error tingkat rendah. Pada titik itu, kerja dengan x86-64 berarti berurusan dengan operand memori dan instruksi panjang bervariasi, sedangkan kerja dengan RISC-V berarti register eksplisit dan langkah 4 byte yang seragam.

Model bisnisnya juga berdampak pada pemrogram. Ekosistem x86-64 menyediakan hampir semua perangkat lunak desktop dan server yang sudah ada, sehingga jangkauan perangkatnya menjadi kekuatan utamanya, tetapi implementasinya terikat lisensi pemiliknya. Ekosistem RISC-V lebih muda dan belum menyediakan padanan untuk setiap aplikasi, meskipun dukungan Linux, gcc, dan LLVM terus bertambah. Sebagai gantinya, open standard membuka pintu bagi siapa pun untuk membuat inti sendiri, menambahkan ekstensi khusus, atau memakai inti kelas pendidikan tanpa perjanjian lisensi.

Karena itu, pilihan antara keduanya bukan soal yang menang, melainkan soal kecocokan dengan pekerjaannya. Program yang harus berjalan pada banyak komputer lama nyaman di x86-64, sedangkan perangkat yang membutuhkan inti sederhana, mudah dimodifikasi, atau bebas royalti menemukan jawabannya pada RISC-V. Kontrak keduanya sama sifatnya, yaitu menetapkan register, instruksi, dan perilaku memori, sehingga cara membaca bahasa mesin yang dipraktikkan pada topik ini berlaku pada keduanya.

Kesalahan umum#

Tiga kekeliruan berikut paling sering muncul ketika perbandingan ISA mulai dipraktikkan.

  1. Membaca operand gaya AT&T dengan urutan terbalik. Pada keluaran objdump, operand tujuan berada paling kanan, sehingga mov %esi,%eax memindahkan isi esi ke eax dan bukan sebaliknya. Kebiasaan yang mencegah salah baca adalah memeriksa arah pada satu instruksi yang hasilnya sudah diketahui sebelum menafsirkan sisanya.
  2. Menghitung ukuran kode dari jumlah baris berkas .s. Baris la t0, skor pada daftar RV32I adalah pseudo-instruksi yang oleh assembler nyata dijabarkan menjadi instruksi dasar, dan pasangan lui dengan addi untuk konstanta besar menambah instruksi yang tidak terlihat pada C-nya. Penghitungan baru sah setelah bentuk instruksi dasarnya jelas.
  3. Menyamakan kebebasan royalti dengan kebebasan biaya total. Open standard menghapus biaya lisensi ISA, tetapi merancang, memverifikasi, dan memproduksi chip tetap pekerjaan yang mahal. Kesimpulan yang tepat menyangkut model bisnisnya yang terbuka, bukan perkiraan bahwa perangkat kerasnya pasti lebih murah.

Ringkasan#

  • x86-64 mewarisi 8086 dan menuntut kompatibilitas penuh, sehingga instruksinya bervariasi 1 hingga 15 byte dan dekoder harus membelah deretan byte tanpa tanda batas.
  • RISC-V RV32I memakai instruksi tetap 4 byte, memisahkan akses memori dari aritmetika dengan pola load/store, dan menyusun konstanta besar dari pasangan lui dan addi.
  • Operand memori membuat satu instruksi x86-64 dapat menambahkan isi memori langsung ke register, sedangkan padanan RV32I membutuhkan pasangan lw dan sw yang mengapitnya.
  • x86-64 terikat lisensi dan memiliki ekosistem paling matang, sedangkan RISC-V berupa open standard bebas royalti yang dapat diperluas melalui ekstensi M, A, F, D, C, dan V.

Referensi#