Bagian 4: QFTASM dan Cogol
Tinjauan Arsitektur
Singkatnya, komputer kita memiliki arsitektur RISC Harvard asinkron 16-bit. Saat membangun prosesor dengan tangan, arsitektur RISC ( komputer dengan instruksi terbatas ) praktis merupakan persyaratan. Dalam kasus kami, ini berarti jumlah opcode kecil dan, yang jauh lebih penting, bahwa semua instruksi diproses dengan cara yang sangat mirip.
Sebagai referensi, komputer Wireworld menggunakan arsitektur yang dipicu transportasi , di mana satu-satunya instruksi adalah MOV
dan perhitungan dilakukan dengan menulis / membaca register khusus. Meskipun paradigma ini mengarah pada arsitektur yang sangat mudah diimplementasikan, hasilnya juga batas tidak dapat digunakan: semua operasi aritmatika / logika / kondisional memerlukan tiga instruksi. Jelas bagi kami bahwa kami ingin menciptakan arsitektur yang jauh lebih esoteris.
Agar prosesor kami tetap sederhana sekaligus meningkatkan kegunaan, kami membuat beberapa keputusan desain penting:
- Tidak ada register. Setiap alamat dalam RAM diperlakukan sama dan dapat digunakan sebagai argumen untuk operasi apa pun. Dalam arti tertentu, ini berarti semua RAM dapat diperlakukan seperti register. Ini berarti tidak ada instruksi pemuatan / penyimpanan khusus.
- Dalam nada yang sama, pemetaan memori. Segala sesuatu yang dapat ditulis atau dibaca dari berbagi skema pengalamatan terpadu. Ini berarti bahwa penghitung program (PC) adalah alamat 0, dan satu-satunya perbedaan antara instruksi reguler dan instruksi aliran kontrol adalah bahwa instruksi aliran kontrol menggunakan alamat 0.
- Data bersifat serial dalam transmisi, paralel dalam penyimpanan. Karena sifat "elektron" berbasis komputer kita, penambahan dan pengurangan secara signifikan lebih mudah untuk diterapkan ketika data ditransmisikan dalam bentuk serial little-endian (bit paling signifikan pertama). Selain itu, data serial menghilangkan perlunya bus data yang rumit, yang keduanya sangat lebar dan tidak praktis waktu (agar data tetap bersama, semua "jalur" bus harus mengalami keterlambatan perjalanan yang sama).
- Arsitektur Harvard, artinya pembagian antara memori program (ROM) dan memori data (RAM). Meskipun ini mengurangi fleksibilitas prosesor, ini membantu dengan optimasi ukuran: panjang program jauh lebih besar dari jumlah RAM yang kita perlukan, jadi kita dapat membagi program menjadi ROM dan kemudian fokus pada mengompresi ROM , yang jauh lebih mudah bila hanya baca.
- Lebar data 16-bit. Ini adalah kekuatan terkecil dari dua yang lebih luas dari papan Tetris standar (10 blok). Ini memberi kami rentang data -32768 hingga +32767 dan panjang program maksimum 65536 instruksi. (2 ^ 8 = 256 instruksi sudah cukup untuk hal-hal paling sederhana yang mungkin kita inginkan pengolah mainan, tetapi tidak Tetris.)
- Desain asinkron. Daripada memiliki jam pusat (atau, setara, beberapa jam) yang menentukan waktu komputer, semua data disertai dengan "sinyal jam" yang bergerak secara paralel dengan data saat mengalir di sekitar komputer. Jalur tertentu mungkin lebih pendek dari yang lain, dan sementara ini akan menimbulkan kesulitan untuk desain jam pusat, desain asinkron dapat dengan mudah menangani operasi variabel-waktu.
- Semua instruksi berukuran sama. Kami merasa bahwa arsitektur di mana setiap instruksi memiliki 1 opcode dengan 3 operan (nilai tujuan) adalah pilihan yang paling fleksibel. Ini mencakup operasi data biner serta gerakan bersyarat.
- Sistem mode pengalamatan sederhana. Memiliki berbagai mode pengalamatan sangat berguna untuk mendukung hal-hal seperti array atau rekursi. Kami berhasil menerapkan beberapa mode pengalamatan penting dengan sistem yang relatif sederhana.
Ilustrasi arsitektur kami terdapat di pos ikhtisar.
Fungsi dan Operasi ALU
Dari sini, itu adalah masalah menentukan fungsi apa yang harus dimiliki prosesor kami. Perhatian khusus diberikan pada kemudahan implementasi serta fleksibilitas dari setiap perintah.
Gerakan Bersyarat
Gerakan bersyarat sangat penting dan berfungsi sebagai aliran kontrol skala kecil dan besar. "Skala kecil" mengacu pada kemampuannya untuk mengontrol pelaksanaan pemindahan data tertentu, sementara "skala besar" mengacu pada penggunaannya sebagai operasi lompatan bersyarat untuk mentransfer aliran kontrol ke setiap bagian kode yang sewenang-wenang. Tidak ada operasi lompatan khusus karena, karena pemetaan memori, gerakan bersyarat dapat menyalin data ke RAM biasa dan menyalin alamat tujuan ke PC. Kami juga memilih untuk tidak menggunakan gerakan tak bersyarat dan lompatan tak bersyarat untuk alasan yang sama: keduanya dapat diimplementasikan sebagai gerakan bersyarat dengan kondisi yang sulit diubah ke TRUE.
Kami memilih untuk memiliki dua jenis gerakan bersyarat: "bergerak jika tidak nol" ( MNZ
) dan "bergerak jika kurang dari nol" ( MLZ
). Secara fungsional, MNZ
jumlah untuk memeriksa apakah bit dalam data adalah 1, sedangkan MLZ
jumlah untuk memeriksa apakah bit tanda adalah 1. Mereka berguna untuk persamaan dan perbandingan, masing-masing. Alasan kami memilih dua ini lebih dari yang lain seperti "bergerak jika nol" ( MEZ
) atau "bergerak jika lebih besar dari nol" ( MGZ
) adalah yang MEZ
memerlukan pembuatan sinyal BENAR dari sinyal kosong, sementara itu MGZ
adalah pemeriksaan yang lebih kompleks, yang membutuhkan tanda bit menjadi 0 sementara setidaknya satu bit lainnya menjadi 1.
Hitung
Instruksi terpenting berikutnya, dalam hal memandu desain prosesor, adalah operasi aritmatika dasar. Seperti yang saya sebutkan sebelumnya, kami menggunakan data serial little-endian, dengan pilihan endianness ditentukan oleh kemudahan operasi penjumlahan / pengurangan. Dengan meminta bit terkecil datang terlebih dahulu, unit aritmatika dapat dengan mudah melacak carry bit.
Kami memilih untuk menggunakan representasi komplemen 2 untuk angka negatif, karena ini membuat penambahan dan pengurangan lebih konsisten. Perlu dicatat bahwa komputer Wireworld menggunakan komplemen 1.
Penambahan dan pengurangan adalah sejauh mana dukungan aritmatika asli komputer kami (selain pergeseran bit yang akan dibahas nanti). Operasi lain, seperti multiplikasi, terlalu rumit untuk ditangani oleh arsitektur kita, dan harus diimplementasikan dalam perangkat lunak.
Operasi Bitwise
Prosesor kami memiliki AND
,, OR
dan XOR
instruksi yang melakukan apa yang Anda harapkan. Daripada memiliki NOT
instruksi, kami memilih untuk memiliki instruksi "dan-tidak" ( ANT
). Kesulitan dengan NOT
instruksi itu lagi bahwa ia harus membuat sinyal dari kurangnya sinyal, yang sulit dengan automata seluler. The ANT
instruksi mengembalikan 1 hanya jika bit argumen pertama adalah 1 dan bit argumen kedua adalah 0. Jadi, NOT x
ini setara dengan ANT -1 x
(serta XOR -1 x
). Selain itu, ANT
serbaguna dan memiliki keuntungan utama dalam menutupi: dalam hal program Tetris kami menggunakannya untuk menghapus tetromino.
Pergeseran Bit
Operasi bit-shifting adalah operasi paling kompleks yang ditangani oleh ALU. Mereka mengambil dua input data: nilai untuk digeser dan jumlah untuk digeser. Terlepas dari kerumitannya (karena jumlah pergeseran yang bervariasi), operasi ini sangat penting untuk banyak tugas penting, termasuk banyak operasi "grafis" yang terlibat dalam Tetris. Pergeseran bit juga akan berfungsi sebagai dasar untuk algoritma multiplikasi / divisi yang efisien.
Prosesor kami memiliki tiga operasi shift bit, "shift left" ( SL
), "shift right logical" ( SRL
), dan "shift arithmetic right" ( SRA
). Dua bit pertama bergeser ( SL
dan SRL
) mengisi bit baru dengan semua nol (artinya angka negatif yang bergeser ke kanan tidak akan lagi menjadi negatif). Jika argumen kedua dari pergeseran berada di luar kisaran 0 hingga 15, hasilnya adalah nol, seperti yang Anda harapkan. Untuk bit shift terakhir SRA
,, bit shift mempertahankan tanda input, dan karenanya bertindak sebagai pembagian yang benar oleh dua.
Instruksi Pipelining
Sekarang saatnya berbicara tentang beberapa detail arsitektur yang rumit. Setiap siklus CPU terdiri dari lima langkah berikut:
1. Ambil instruksi saat ini dari ROM
Nilai PC saat ini digunakan untuk mengambil instruksi yang sesuai dari ROM. Setiap instruksi memiliki satu opcode dan tiga operan. Setiap operan terdiri dari satu kata data dan satu mode pengalamatan. Bagian-bagian ini terpisah satu sama lain karena dibaca dari ROM.
Opcode adalah 4 bit untuk mendukung 16 opcode unik, yang 11 ditugaskan:
0000 MNZ Move if Not Zero
0001 MLZ Move if Less than Zero
0010 ADD ADDition
0011 SUB SUBtraction
0100 AND bitwise AND
0101 OR bitwise OR
0110 XOR bitwise eXclusive OR
0111 ANT bitwise And-NoT
1000 SL Shift Left
1001 SRL Shift Right Logical
1010 SRA Shift Right Arithmetic
1011 unassigned
1100 unassigned
1101 unassigned
1110 unassigned
1111 unassigned
2. Tulis hasil (jika perlu) dari instruksi sebelumnya ke RAM
Bergantung pada kondisi instruksi sebelumnya (seperti nilai argumen pertama untuk gerakan bersyarat), penulisan dilakukan. Alamat penulisan ditentukan oleh operan ketiga dari instruksi sebelumnya.
Penting untuk dicatat bahwa menulis terjadi setelah instruksi mengambil. Ini mengarah pada pembuatan slot penundaan cabang di mana instruksi segera setelah instruksi cabang (operasi apa pun yang menulis ke PC) dieksekusi sebagai pengganti instruksi pertama pada target cabang.
Dalam kasus tertentu (seperti lompatan tanpa syarat), slot cabang penundaan dapat dioptimalkan jauh. Dalam kasus lain tidak bisa, dan instruksi setelah cabang harus dibiarkan kosong. Selanjutnya, jenis slot tunda ini berarti bahwa cabang harus menggunakan target cabang yang 1 alamat kurang dari instruksi target yang sebenarnya, untuk memperhitungkan kenaikan PC yang terjadi.
Singkatnya, karena output instruksi sebelumnya ditulis ke RAM setelah instruksi berikutnya diambil, lompatan bersyarat harus memiliki instruksi kosong setelah mereka, atau PC tidak akan diperbarui dengan benar untuk lompatan.
3. Baca data untuk argumen instruksi saat ini dari RAM
Seperti disebutkan sebelumnya, masing-masing dari tiga operan terdiri dari kata data dan mode pengalamatan. Kata data adalah 16 bit, sama lebarnya dengan RAM. Mode pengalamatan adalah 2 bit.
Mode pengalamatan dapat menjadi sumber kompleksitas yang signifikan untuk prosesor seperti ini, karena banyak mode pengalamatan dunia nyata melibatkan perhitungan multi-langkah (seperti menambahkan offset). Pada saat yang sama, mode pengalamatan serbaguna memainkan peran penting dalam kegunaan prosesor.
Kami berusaha untuk menyatukan konsep menggunakan angka hard-kode sebagai operan dan menggunakan alamat data sebagai operan. Ini mengarah pada pembuatan mode pengalamatan berbasis counter: mode pengalamatan sebuah operan hanyalah angka yang menunjukkan berapa kali data harus dikirim di sekitar loop pembacaan RAM. Ini mencakup pengalamatan langsung, langsung, tidak langsung, dan tidak langsung ganda.
00 Immediate: A hard-coded value. (no RAM reads)
01 Direct: Read data from this RAM address. (one RAM read)
10 Indirect: Read data from the address given at this address. (two RAM reads)
11 Double-indirect: Read data from the address given at the address given by this address. (three RAM reads)
Setelah dereferencing ini dilakukan, tiga operan instruksi memiliki peran yang berbeda. Operan pertama biasanya argumen pertama untuk operator biner, tetapi juga berfungsi sebagai kondisi ketika instruksi saat ini adalah gerakan bersyarat. Operan kedua berfungsi sebagai argumen kedua untuk operator biner. Operan ketiga berfungsi sebagai alamat tujuan untuk hasil instruksi.
Karena dua instruksi pertama berfungsi sebagai data sementara yang ketiga berfungsi sebagai alamat, mode pengalamatan memiliki interpretasi yang sedikit berbeda tergantung pada posisi mana mereka digunakan. Misalnya, mode langsung digunakan untuk membaca data dari alamat RAM tetap (karena satu RAM diperlukan), tetapi mode langsung digunakan untuk menulis data ke alamat RAM tetap (karena tidak ada RAM yang diperlukan).
4. Hitung hasilnya
Opcode dan dua operan pertama dikirim ke ALU untuk melakukan operasi biner. Untuk operasi aritmatika, bitwise, dan shift, ini berarti melakukan operasi yang relevan. Untuk gerakan bersyarat, ini berarti mengembalikan operan kedua.
Opcode dan operan pertama digunakan untuk menghitung kondisi, yang menentukan apakah akan menulis hasilnya atau tidak ke memori. Dalam kasus gerakan kondisional, ini berarti menentukan apakah bit dalam operan adalah 1 (untuk MNZ
), atau menentukan apakah bit tanda adalah 1 (untuk MLZ
). Jika opcode bukan gerakan bersyarat, maka penulisan selalu dilakukan (kondisinya selalu benar).
5. Tambahkan penghitung program
Akhirnya, penghitung program dibaca, ditambahkan, dan ditulis.
Karena posisi kenaikan PC antara membaca instruksi dan instruksi menulis, ini berarti bahwa instruksi yang menambah PC oleh 1 adalah no-op. Instruksi yang menyalin PC ke dirinya sendiri menyebabkan instruksi berikutnya dieksekusi dua kali berturut-turut. Tetapi, berhati-hatilah, banyak instruksi PC secara berurutan dapat menyebabkan efek kompleks, termasuk perulangan tak terbatas, jika Anda tidak memperhatikan pipa instruksi.
Quest for Tetris Assembly
Kami menciptakan bahasa rakitan baru bernama QFTASM untuk prosesor kami. Bahasa assembly ini berkorespondensi 1-ke-1 dengan kode mesin dalam ROM komputer.
Setiap program QFTASM ditulis sebagai serangkaian instruksi, satu instruksi per baris. Setiap baris diformat seperti ini:
[line numbering] [opcode] [arg1] [arg2] [arg3]; [optional comment]
Daftar Opcode
Seperti dibahas sebelumnya, ada sebelas opcode yang didukung oleh komputer, yang masing-masing memiliki tiga operan:
MNZ [test] [value] [dest] – Move if Not Zero; sets [dest] to [value] if [test] is not zero.
MLZ [test] [value] [dest] – Move if Less than Zero; sets [dest] to [value] if [test] is less than zero.
ADD [val1] [val2] [dest] – ADDition; store [val1] + [val2] in [dest].
SUB [val1] [val2] [dest] – SUBtraction; store [val1] - [val2] in [dest].
AND [val1] [val2] [dest] – bitwise AND; store [val1] & [val2] in [dest].
OR [val1] [val2] [dest] – bitwise OR; store [val1] | [val2] in [dest].
XOR [val1] [val2] [dest] – bitwise XOR; store [val1] ^ [val2] in [dest].
ANT [val1] [val2] [dest] – bitwise And-NoT; store [val1] & (![val2]) in [dest].
SL [val1] [val2] [dest] – Shift Left; store [val1] << [val2] in [dest].
SRL [val1] [val2] [dest] – Shift Right Logical; store [val1] >>> [val2] in [dest]. Doesn't preserve sign.
SRA [val1] [val2] [dest] – Shift Right Arithmetic; store [val1] >> [val2] in [dest], while preserving sign.
Mengatasi Mode
Setiap operan berisi nilai data dan gerakan pengalamatan. Nilai data dijelaskan oleh angka desimal dalam kisaran -32768 hingga 32767. Mode pengalamatan dijelaskan oleh awalan satu huruf ke nilai data.
mode name prefix
0 immediate (none)
1 direct A
2 indirect B
3 double-indirect C
Kode Contoh
Urutan Fibonacci dalam lima baris:
0. MLZ -1 1 1; initial value
1. MLZ -1 A2 3; start loop, shift data
2. MLZ -1 A1 2; shift data
3. MLZ -1 0 0; end loop
4. ADD A2 A3 1; branch delay slot, compute next term
Kode ini menghitung urutan Fibonacci, dengan alamat RAM 1 yang berisi istilah saat ini. Dengan cepat meluap setelah 28657.
Kode abu-abu:
0. MLZ -1 5 1; initial value for RAM address to write to
1. SUB A1 5 2; start loop, determine what binary number to covert to Gray code
2. SRL A2 1 3; shift right by 1
3. XOR A2 A3 A1; XOR and store Gray code in destination address
4. SUB B1 42 4; take the Gray code and subtract 42 (101010)
5. MNZ A4 0 0; if the result is not zero (Gray code != 101010) repeat loop
6. ADD A1 1 1; branch delay slot, increment destination address
Program ini menghitung kode Gray dan menyimpan kode dalam alamat berurutan mulai dari alamat 5. Program ini menggunakan beberapa fitur penting seperti pengalamatan tidak langsung dan lompatan bersyarat. Ini berhenti setelah kode Gray yang dihasilkan 101010
, yang terjadi untuk input 51 di alamat 56.
Penerjemah Online
El'endia Starman telah menciptakan juru bahasa online yang sangat berguna di sini . Anda dapat melangkah melalui kode, mengatur breakpoints, melakukan penulisan manual ke RAM, dan memvisualisasikan RAM sebagai tampilan.
Cogol
Setelah arsitektur dan bahasa assembly didefinisikan, langkah selanjutnya pada sisi "perangkat lunak" proyek adalah penciptaan bahasa tingkat yang lebih tinggi, sesuatu yang cocok untuk Tetris. Jadi saya membuat Cogol . Namanya adalah plesetan dari "COBOL" dan akronim untuk "C of Game of Life", meskipun perlu dicatat bahwa Cogol adalah untuk C seperti apa komputer kita dengan komputer yang sebenarnya.
Cogol ada pada level di atas bahasa assembly. Secara umum, sebagian besar baris dalam program Cogol masing-masing sesuai dengan satu baris perakitan, tetapi ada beberapa fitur penting dari bahasa ini:
- Fitur dasar termasuk variabel bernama dengan penugasan dan operator yang memiliki sintaks yang lebih mudah dibaca. Misalnya,
ADD A1 A2 3
menjadi z = x + y;
, dengan variabel pemetaan kompiler ke alamat.
- Konstruksi perulangan seperti
if(){}
, while(){}
, dan do{}while();
sehingga compiler menangani bercabang.
- Array satu dimensi (dengan aritmatika pointer), yang digunakan untuk papan Tetris.
- Subrutin dan tumpukan panggilan. Ini berguna untuk mencegah duplikasi potongan kode besar, dan untuk mendukung rekursi.
Kompiler (yang saya tulis dari awal) sangat mendasar / naif, tetapi saya telah mencoba mengoptimalkan beberapa konstruksi bahasa untuk mencapai panjang program yang dikompilasi.
Berikut ini beberapa ikhtisar singkat tentang bagaimana berbagai fitur bahasa bekerja:
Tokenisasi
Kode sumber tokenized secara linear (single-pass), menggunakan aturan sederhana tentang karakter mana yang boleh berdekatan dalam token. Ketika karakter ditemukan yang tidak dapat berdekatan dengan karakter terakhir dari token saat ini, token saat ini dianggap selesai dan karakter baru memulai token baru. Beberapa karakter (seperti {
atau ,
) tidak dapat berdekatan dengan karakter lain dan karenanya merupakan token mereka sendiri. Lainnya (seperti >
atau =
) hanya boleh berdekatan dengan karakter lain dalam kelas mereka, dan dengan demikian dapat membentuk token seperti >>>
, ==
, atau >=
, tapi tidak seperti =2
. Karakter spasi putih memaksa batas antara token tetapi tidak sendiri termasuk dalam hasil. Karakter yang paling sulit untuk dipatahkan adalah-
karena keduanya bisa mewakili pengurangan dan negasi unary, dan dengan demikian memerlukan beberapa casing khusus.
Parsing
Parsing juga dilakukan secara single-pass. Compiler memiliki metode untuk menangani setiap konstruksi bahasa yang berbeda, dan token dikeluarkan dari daftar token global karena mereka dikonsumsi oleh berbagai metode kompiler. Jika kompiler pernah melihat token yang tidak diharapkan, itu menimbulkan kesalahan sintaksis.
Alokasi Memori Global
Kompiler memberikan setiap variabel global (kata atau larik) alamat RAM yang ditunjuknya sendiri. Kita perlu mendeklarasikan semua variabel menggunakan kata kunci my
sehingga kompiler tahu untuk mengalokasikan ruang untuk itu. Jauh lebih keren daripada variabel global bernama adalah manajemen memori alamat awal. Banyak instruksi (terutama kondisional dan banyak akses array) memerlukan alamat "awal" sementara untuk menyimpan perhitungan menengah. Selama proses kompilasi, kompiler mengalokasikan dan tidak mengalokasikan alamat awal yang diperlukan. Jika kompilator membutuhkan lebih banyak alamat awal, ia akan mendedikasikan lebih banyak RAM sebagai alamat awal. Saya percaya ini tipikal untuk program yang hanya membutuhkan beberapa alamat awal, meskipun setiap alamat awal akan digunakan berkali-kali.
IF-ELSE
Pernyataan
Sintaks untuk if-else
pernyataan adalah bentuk C standar:
other code
if (cond) {
first body
} else {
second body
}
other code
Ketika dikonversi ke QFTASM, kode tersebut diatur seperti ini:
other code
condition test
conditional jump
first body
unconditional jump
second body (conditional jump target)
other code (unconditional jump target)
Jika tubuh pertama dieksekusi, tubuh kedua dilewati. Jika tubuh pertama dilewati, tubuh kedua dieksekusi.
Dalam perakitan, tes kondisi biasanya hanya pengurangan, dan tanda hasilnya menentukan apakah akan melakukan lompatan atau mengeksekusi tubuh. Sebuah MLZ
instruksi digunakan untuk menangani ketidaksetaraan seperti >
atau <=
. Sebuah MNZ
instruksi yang digunakan untuk menangani ==
, karena melompat di atas tubuh ketika perbedaan tersebut tidak nol (dan oleh karena itu ketika argumen tidak sama). Persyaratan multi-ekspresi saat ini tidak didukung.
Jika else
pernyataan dihilangkan, lompatan tanpa syarat juga dihilangkan, dan kode QFTASM terlihat seperti ini:
other code
condition test
conditional jump
body
other code (conditional jump target)
WHILE
Pernyataan
Sintaks untuk while
pernyataan juga merupakan bentuk C standar:
other code
while (cond) {
body
}
other code
Ketika dikonversi ke QFTASM, kode tersebut diatur seperti ini:
other code
unconditional jump
body (conditional jump target)
condition test (unconditional jump target)
conditional jump
other code
Pengujian kondisi dan lompatan bersyarat berada di akhir blok, yang berarti mereka dieksekusi kembali setelah setiap eksekusi blok. Ketika kondisi dikembalikan palsu tubuh tidak diulang dan loop berakhir. Selama awal eksekusi loop, aliran kontrol melompati body loop ke kode kondisi, sehingga tubuh tidak pernah dieksekusi jika kondisinya salah saat pertama kali.
Sebuah MLZ
instruksi digunakan untuk menangani ketidaksetaraan seperti >
atau <=
. Tidak seperti selama if
pernyataan, MNZ
instruksi digunakan untuk menangani !=
, karena ia melompat ke tubuh ketika perbedaannya tidak nol (dan karena itu ketika argumen tidak sama).
DO-WHILE
Pernyataan
Satu-satunya perbedaan antara while
dan do-while
adalah bahwa do-while
badan loop pada awalnya tidak dilewati sehingga selalu dieksekusi setidaknya sekali. Saya biasanya menggunakan do-while
pernyataan untuk menyimpan beberapa baris kode assembly ketika saya tahu loop tidak perlu dilewati sepenuhnya.
Array
Array satu dimensi diimplementasikan sebagai blok memori yang berdekatan. Semua array adalah panjang tetap berdasarkan deklarasi mereka. Array dinyatakan seperti ini:
my alpha[3]; # empty array
my beta[11] = {3,2,7,8}; # first four elements are pre-loaded with those values
Untuk array, ini adalah pemetaan RAM yang memungkinkan, menunjukkan bagaimana alamat 15-18 dicadangkan untuk array:
15: alpha
16: alpha[0]
17: alpha[1]
18: alpha[2]
Alamat berlabel alpha
diisi dengan pointer ke lokasi alpha[0]
, jadi dalam kasus ini alamat 15 berisi nilai 16. alpha
Variabel dapat digunakan di dalam kode Cogol, mungkin sebagai penunjuk tumpukan jika Anda ingin menggunakan array ini sebagai tumpukan .
Mengakses elemen-elemen dari array dilakukan dengan array[index]
notasi standar . Jika nilai index
konstanta, referensi ini secara otomatis diisi dengan alamat absolut elemen itu. Kalau tidak, ia melakukan beberapa aritmatika pointer (hanya tambahan) untuk menemukan alamat absolut yang diinginkan. Dimungkinkan juga untuk mengindeks sarang, seperti alpha[beta[1]]
.
Subrutin dan Memanggil
Subrutin adalah blok kode yang dapat dipanggil dari berbagai konteks, mencegah duplikasi kode dan memungkinkan untuk pembuatan program rekursif. Berikut adalah program dengan subrutin rekursif untuk menghasilkan angka Fibonacci (pada dasarnya algoritma paling lambat):
# recursively calculate the 10th Fibonacci number
call display = fib(10).sum;
sub fib(cur,sum) {
if (cur <= 2) {
sum = 1;
return;
}
cur--;
call sum = fib(cur).sum;
cur--;
call sum += fib(cur).sum;
}
Subrutin dideklarasikan dengan kata kunci sub
, dan subrutin dapat ditempatkan di mana saja di dalam program. Setiap subrutin dapat memiliki banyak variabel lokal, yang dideklarasikan sebagai bagian dari daftar argumennya. Argumen ini juga bisa diberi nilai default.
Untuk menangani panggilan rekursif, variabel lokal dari subrutin disimpan di stack. Variabel statis terakhir dalam RAM adalah penunjuk tumpukan panggilan, dan semua memori setelah itu berfungsi sebagai tumpukan panggilan. Ketika subrutin dipanggil, ia membuat bingkai baru di tumpukan panggilan, yang mencakup semua variabel lokal serta alamat return (ROM). Setiap subrutin dalam program ini diberikan alamat RAM statis tunggal untuk berfungsi sebagai pointer. Pointer ini memberikan lokasi panggilan "saat ini" dari subrutin dalam tumpukan panggilan. Referensi variabel lokal dilakukan menggunakan nilai pointer statis ini ditambah offset untuk memberikan alamat variabel lokal tertentu. Juga terdapat dalam tumpukan panggilan adalah nilai sebelumnya dari penunjuk statis. Sini'
RAM map:
0: pc
1: display
2: scratch0
3: fib
4: scratch1
5: scratch2
6: scratch3
7: call
fib map:
0: return
1: previous_call
2: cur
3: sum
Satu hal yang menarik tentang subrutin adalah mereka tidak mengembalikan nilai tertentu. Sebaliknya, semua variabel lokal dari subrutin dapat dibaca setelah subrutin dilakukan, sehingga berbagai data dapat diekstraksi dari panggilan subrutin. Ini dilakukan dengan menyimpan pointer untuk panggilan spesifik dari subrutin, yang kemudian dapat digunakan untuk memulihkan variabel lokal dari dalam frame stack (baru-baru ini deallocated).
Ada beberapa cara untuk memanggil subrutin, semuanya menggunakan call
kata kunci:
call fib(10); # subroutine is executed, no return vaue is stored
call pointer = fib(10); # execute subroutine and return a pointer
display = pointer.sum; # access a local variable and assign it to a global variable
call display = fib(10).sum; # immediately store a return value
call display += fib(10).sum; # other types of assignment operators can also be used with a return value
Sejumlah nilai dapat diberikan sebagai argumen untuk panggilan subrutin. Setiap argumen yang tidak disediakan akan diisi dengan nilai standarnya, jika ada. Argumen yang tidak disediakan dan tidak memiliki nilai default tidak dihapus (untuk menyimpan instruksi / waktu) sehingga berpotensi mengambil nilai apa pun di awal subrutin.
Pointer adalah cara mengakses beberapa variabel lokal dari subrutin, walaupun penting untuk dicatat bahwa pointer hanya sementara: data yang ditunjuk oleh pointer akan dihancurkan ketika panggilan subrutin lain dibuat.
Label debugging
Setiap {...}
blok kode dalam program Cogol dapat didahului dengan label deskriptif multi-kata. Label ini dilampirkan sebagai komentar dalam kode perakitan yang dikompilasi, dan dapat sangat berguna untuk debugging karena memudahkan untuk menemukan potongan kode tertentu.
Optimalisasi Slot Cabang Delay
Untuk meningkatkan kecepatan kode yang dikompilasi, kompiler Cogol melakukan beberapa optimasi slot keterlambatan yang sangat mendasar sebagai umpan akhir atas kode QFTASM. Untuk lompatan tak bersyarat apa pun dengan slot tunda cabang kosong, slot tunda dapat diisi dengan instruksi pertama di tujuan lompat, dan tujuan lompatan bertambah satu untuk menunjuk ke instruksi berikutnya. Ini umumnya menghemat satu siklus setiap kali lompatan tanpa syarat dilakukan.
Menulis kode Tetris di Cogol
Program Tetris terakhir ditulis dalam Cogol, dan kode sumber tersedia di sini . Kode QFTASM yang dikompilasi tersedia di sini . Untuk kenyamanan, permalink disediakan di sini: Tetris di QFTASM . Karena tujuannya adalah untuk golf kode perakitan (bukan kode Cogol), kode Cogol yang dihasilkan sulit digunakan. Banyak bagian dari program biasanya terletak di subrutin, tetapi subrutin tersebut sebenarnya cukup pendek sehingga duplikasi kode menyimpan instruksi di atascall
pernyataan. Kode akhir hanya memiliki satu subrutin selain kode utama. Selain itu, banyak array telah dihapus dan diganti dengan daftar variabel individual yang panjangnya setara, atau dengan banyak angka yang dikodekan dalam program. Kode QFTASM terakhir yang dikompilasi berada di bawah 300 instruksi, meskipun hanya sedikit lebih panjang dari sumber Cogol itu sendiri.