Kısım I · Makine · Bölüm 4

Kapılardan Hesaba

Toplayıcı ve ALU (Arithmetic: adders and the ALU)

Neden burada · Neye dayanıyor

Neden burada: Bölüm 3'te kapıları tek tek tanıdık: her biri bitleri alıp bir karar çıkaran küçük bir parça. Ama bilgisayara "hesap makinesi" diyebilmemiz için kararların ötesinde bir şey gerekiyor: sayılarla işlem yapmak. Bu bölümün iddiası şaşırtıcıdır: toplama, kapıların uygun biçimde bağlanmasından kendiliğinden doğar; yeni bir fiziksel keşif gerekmez. Bir toplayıcıyı kurunca, çıkarmayı, negatif sayıları ve en sonunda "hangi işlemi yapayım?" diye soran bir seçici ile bütün bunları tek bir parçada toplayan ALU'yu elde ederiz.

Neye dayanıyor: Bölüm 3: VE, VEYA, DEĞİL, NAND ve özellikle XOR (3.4); iki bitin farklı olduğunu söyleyen o kapı bu bölümde toplamanın kalbi olacak. Ve Bölüm 2: ikilik sayı sistemi, basamak değerleri (2.1), bit sayısının sınırlı olduğu (2.2, 2.3).

  1. Kat 3–10İşlemci, makine dili, diller, işletim sistemi, web, uygulamaHepsi, bu bölümde kurulan hesap devresine yaslanır.
  2. Kat 2Devreler: hesap ve hafızaKapılardan kurulan toplayıcılar (bu bölüm) ve hatırlayan devreler (Bölüm 5).
  3. Kat 1Transistör ve mantık kapılarıBölüm 3.
  4. Kat 0Elektrik ve bitBölüm 2.

Bu bölümün sonunda şunları bileceksin: iki ikilik sayıyı elle toplamayı ve "elde"nin nereden geldiğini; bunu yapan devreyi (yarım toplayıcı, tam toplayıcı, zincirleme toplayıcı) kapılardan nasıl kuracağını; negatif sayıların ikiye tümleyen yöntemiyle nasıl yazıldığını ve neden çıkarma için ayrı bir devreye gerek kalmadığını; taşmanın ne olduğunu ve bir roketi nasıl düşürdüğünü; JavaScript'te 0.1 + 0.2 işleminin neden 0.30000000000000004 verdiğini; ve bütün bunları tek parçada toplayan ALU'nun nasıl çalıştığını.

4.1 İkilik toplama ve elde

İlkokulda toplamayı sağdan sola, basamak basamak yapmayı öğrendin. 47 + 38'de önce 7 + 8 = 15 yaparsın; 5'i yazar, 1'i "elde" olarak bir sonraki basamağa taşırsın. İkilik toplama tamamen aynı yöntemdir; Nand2Tetris'in ifadesiyle "ilkokulda öğrendiğimiz aynı yöntem": sağdaki en az anlamlı bitten başlar, her basamakta çıkan eldeyi (carry) bir soldaki bit çiftine ekler, en soldaki bit çiftine kadar sürdürürsün.1 Tek fark, ikilikte yalnızca dört temel toplama olması:

aba + b (ikilik)Toplam bitiElde
000000
010110
100110
111001

Son satır, bütün konunun anahtarı: 1 + 1 = 2 ve ikilikte 2 sayısı 10 diye yazılır (Bölüm 2.1'deki "ikilik 10 on değil ikidir" uyarısı). Yani sonuç iki bitlik: sağdaki bit (toplam biti) 0, soldaki bit (elde) 1. Şimdi iki gerçek örnek. Nand2Tetris'teki iki örneği kullanıyorum; birincisinde sonuç dört bite sığıyor, ikincisinde sığmıyor:1

   elde:  0 0 0 1                elde:  1 1 1 1
          1 0 0 1   (9)                 1 0 1 1   (11)
        + 0 1 0 1   (5)               + 0 1 1 1   (7)
        ---------                     ---------
          1 1 1 0   (14)            1 0 0 1 0   (18)

Soldaki toplamı sağdan izle: 1 + 1 = 10 (0 yaz, elde 1). Sonraki basamak: 0 + 0 + elde 1 = 1 (elde yok). Sonraki: 0 + 1 = 1. Sonraki: 1 + 0 = 1. Sonuç 1110 = 14 ✓ (9 + 5). Sağdaki toplamda ise en soldaki toplamadan da bir elde çıkıyor: 18 sayısı 10010, yani beş bit gerektirir; elimizdeki yer dört bit. En soldaki bitin bir üstüne taşan bu elde, hesabın sığmadığı anlamına gelir. Buna taşma diyeceğiz (4.3). Şimdilik bir şeyi fark et: her basamakta aynı üç şeye bakıyoruz: iki girdi biti ve gelen elde. Bu üç bitin toplamı 0, 1, 2 ya da 3 olabilir ve sonucu iki bitle yazarız: bir toplam biti ve bir elde. İşte bütün devre bunu yapacak.

4.2 Yarım toplayıcı, tam toplayıcı, çok bitlik toplayıcı

Yukarıdaki tabloya (0+0, 0+1, 1+0, 1+1) dikkatle bak. Toplam biti sütununa: 0, 1, 1, 0. Bu Bölüm 3.4'ün XOR tablosudur: iki bit farklıysa toplam biti 1. Elde sütununa: 0, 0, 0, 1. Bu da VE'nin tablosudur: yalnızca ikisi de 1 ise elde var. Yani:

Bu iki kapıyı yan yana koyunca ortaya çıkan devreye yarım toplayıcı (half adder) denir: iki bit alır, iki bit verir (toplam ve elde).1 "Yarım" çünkü önceki basamaktan gelen eldeyi hesaba katmaz; en sağdaki basamak için yeterlidir (orada gelen elde yok), ama ikinci basamaktan itibaren yetmez.

Tam toplayıcı

Soldaki her basamakta üç bit toplanır: a, b ve bir sağdaki basamaktan gelen elde. Üç biti toplayan devreye tam toplayıcı (full adder) denir. İki çıkışı vardır: toplam ve elde.1 Doğruluk tablosu 2³ = 8 satırdır (3.3'teki ilke):

abgelen eldeeldetoplam
00000
00101
01001
01110
10001
10110
11010
11111

Bu tabloyu sıfırdan kapılarla kurmak yerine bir kestirme var: iki yarım toplayıcıyı art arda bağla. Önce a ile b'yi topla; çıkan toplamı gelen eldeyle bir daha topla. İki ayrı eldenin (iki yarım toplayıcının elde çıkışları) en fazla biri 1 olabilir (neden? iki biti toplayınca elde 1 çıkıyorsa toplam biti 0'dır, ikinci toplamada üç bitin toplamı en çok 3'tür, sonuç iki bite sığar), bu yüzden ikisini VEYA ile birleştirmek yeterlidir. Kodla:

const xor = (a, b) => a ^ b;                    // Bölüm 3.4'teki XOR
const and = (a, b) => a & b;                    // Bölüm 3.3'teki VE
const or  = (a, b) => a | b;                    // Bölüm 3.3'teki VEYA

function halfAdder(a, b) {
  return { sum: xor(a, b), carry: and(a, b) };   // toplam = XOR, elde = VE
}

function fullAdder(a, b, c) {
  const h1 = halfAdder(a, b);                    // 1. önce a + b
  const h2 = halfAdder(h1.sum, c);               // 2. çıkan toplam + gelen elde
  return { sum: h2.sum, carry: or(h1.carry, h2.carry) };  // 3. iki elde'nin VEYA'sı
}

Satır satır: ilk üç satır önceki bölümün kapılarını isimlendirir (JavaScript'in ^, &, | işlemleri bit düzeyindedir). halfAdder iki biti alır ve yukarıdaki iki formülü uygular. fullAdder iki yarım toplayıcıyı zincirler: h1 a ve b'nin toplamıdır, h2 bu toplamın c ile toplamıdır; toplam biti h2.sum'dır, elde ise iki elde çıkışından herhangi biri 1 ise 1'dir. Bu kodu sekiz girdi kombinasyonunun hepsinde çalıştırdım ve çıktı yukarıdaki tabloyla birebir tuttu.

Çok bitlik toplayıcı: elde zinciri

Şimdi tam toplayıcıyı yan yana dizeriz. Her basamak için bir tam toplayıcı; her birinin elde çıkışı bir soldaki toplayıcının "gelen elde" girdisine bağlanır. Böylece 4 bitlik, 16 bitlik, 64 bitlik toplayıcılar elde ederiz; Nand2Tetris'in dediği gibi, aynı mantık herhangi bir bit sayısına ölçeklenir: "daha fazlası aynı iş".1 Böyle bir devreye çok bitlik toplayıcı (multi-bit adder, "adder") denir. Kitap, 16 bitlik toplayıcıyı Add16 diye adlandırır ve tek bir özel sürümünü de ekler: bir sayıya 1 ekleyen Inc16.1

function add4(x, y) {            // x ve y: dört bitlik diziler, soldaki en anlamlı bit
  let elde = 0;                  // en sağda gelen elde yok
  const sonuc = [0, 0, 0, 0];
  for (let i = 3; i >= 0; i--) {  // sağdan sola, her basamak için bir tam toplayıcı
    const r = fullAdder(x[i], y[i], elde);
    sonuc[i] = r.sum;            // bu basamağın toplam biti
    elde = r.carry;              // eldeyi bir soldaki basamağa taşı
  }
  return { bits: sonuc, carry: elde };   // en soldan çıkan elde = taşma işareti
}
// add4([1,0,0,1], [0,1,0,1]) → bits [1,1,1,0], carry 0   (9 + 5 = 14)
// add4([1,0,1,1], [0,1,1,1]) → bits [0,0,1,0], carry 1   (11 + 7 = 18, dört bite sığmaz)

Bu kod 4 bit için 16 × 16 = 256 girdi çiftinin hepsinde doğru toplam verdiğini gösterdi (toplam + 16 × son elde = x + y). Önemli bir detay: elde zincir halinde ilerler; yani en soldaki basamağın sonucu, sağındakilerin hepsi hesaplanmadan kesinleşmez. Gerçek devrede her basamak bir önceki basamağın elde çıkışını bekler, ve her kapının biraz zaman alması nedeniyle 64 basamaklı bir toplayıcıda bu bekleme uzar. Nand2Tetris da kendi tasarımının "elde bitinin en az anlamlı bit çiftinden en anlamlı bit çiftine yayılması sırasında uzun gecikmeler" yüzdüğünü ve bunun "elde ileri bakışı" (carry look-ahead) denen mantık devreleriyle hızlandırılabileceğini söyler. Toplama bu kadar sık yapılan bir işlem olduğundan, bu iyileştirme bütün bilgisayarın hızını etkiler.2 Biz tasarımın ayrıntısına girmiyoruz; bilmen gereken, hızın bir hesabın mantıksal doğruluğundan ayrı, mühendislik bir mesele olduğu.

Uygulamamızda

Uygulamamızda bir doğru cevap 10 puan veriyorsa, kodda puan = puan + 10 yazarsın. Bu tek satırın en altında, yukarıdaki zincirin bir benzeri çalışır: iki sayının bitleri tam toplayıcılardan geçer ve toplam çıkar. Seri (streak) bir gün arttığında seri = seri + 1 de aynıdır; Nand2Tetris'in Inc16 parçası tam bunun için vardır. Puanın nerede durduğu (bellek) Bölüm 5'te; nasıl toplandığı bu bölümde. Bu satırı bir sonraki ders de daha aşağı indirecek: Bölüm 7'de aynı puan = puan + 10 satırının işlemci komutlarına çevrilmiş halini göreceksin.

4.3 Negatif sayılar: ikiye tümleyen; taşma

Şimdiye kadar yalnızca sıfır ve pozitif sayılar yazdık. Ama puan düşebilir (yanlış cevapta puan kaybı), bakiye eksiye gidebilir. Peki bir bit dizisi eksi bir sayıyı nasıl gösterir? Eksi işareti için ayrı bir "−" sembolümüz yok; elimizde yalnızca bitler var. Çözümlerden biri, bitlerin en solundakini "işaret biti" yapmaktır. Ama bu yöntem toplayıcı için sorun çıkarır: negatif sayıları toplarken ayrı kurallar gerekir. Neredeyse bütün bilgisayarların kullandığı çözüm başka ve çok daha zekice: ikiye tümleyen (two's complement).13

Fikir: sayaç geri sarar

Arabanın kilometre sayacını düşün. Sayaç 0000'dayken geri sararsan 9999 gösterir. Yani sayaç 4 haneli olduğu için "−1" ile "9999" aynı konumdur. İkilik dünyada da aynı: n bitlik bir sayaç, 0000'dan geri sararsa 1111'e gider. İkiye tümleyende bunu bir kural yaparız: −1'i 1111 diye yazarız. Biçimsel tanım: n bitte bir x sayısının negatifi, 2n − x olarak yazılır.1 Dört bitte 24 = 16 ve −2 = 16 − 2 = 14 = 1110. Dört bitlik tabloya bakalım:

OnlukDört bitOnlukDört bit
00000-81000
10001-71001
20010-61010
30011-51011
40100-41100
50101-31101
60110-21110
70111-11111

Tabloda üç şeye bak. Bir: 16 desenin yarısı pozitif/sıfır (0 ile 7), yarısı negatif (−8 ile −1). Genel olarak n bitle −2n−1 ile 2n−1 − 1 arası yazılır: 8 bitte −128…127, 16 bitte −32.768…32.767, 32 bitte −2.147.483.648…2.147.483.647.1 İki: bütün pozitiflerin kodu 0 ile başlar, bütün negatiflerin kodu 1 ile başlar; yani en soldaki bit "işaret" gibi davranır, ama özel bir işlem gerektirmez. Üç: bir sayının negatifini bulmanın kolay yolu: bütün bitleri ters çevir, sonuca 1 ekle. 5 = 0101 → ters çevir 1010 → 1 ekle 1011 = −5 ✓ (tabloda −5 = 1011).

Neden güzel: toplayıcı aynı kalır

İkiye tümleyenin değerini gösteren örnek: (−2) + (−3). Dört bitte −2 = 1110, −3 = 1101. Bunları sıradan ikilik toplamayla topla, taşan bitleri at:

   1 1 1 0   (-2)
 + 1 1 0 1   (-3)
 ---------
 1 1 0 1 1   → soldaki taşan bit atılır → 1 0 1 1 = -5

Toplayıcı, sayıların negatif olduğunu bilmeden aynı devreyle doğru sonucu verdi.1 Çıkarma da bedavaya gelir: x − y = x + (−y); yani y'nin bitlerini ters çevir, 1 ekle, sonra topla. 5 − 3'ü dene: 5 = 0101, −3 = 1101. Topla: 0101 + 1101 = 1 0010; en soldaki taşan bit atılır, kalan 0010 = 2 ✓. Bottom-Up CS'nin sonuç cümlesi: tasarımcının yalnızca toplama devresi yapması yeter; çıkarma, negatifi alıp toplamakla, çarpma tekrarlı toplamayla, bölme de tekrarlı çıkarmayla yapılabilir.3 Yani bütün temel aritmetik, toplama devresine indirgenir.

Taşma (overflow)

Bu zarif sistemin bedeli, Bölüm 2'de gördüğümüz sınırdır: bit sayısı sabittir. Taşma (overflow), bir hesabın sonucunun, tuttuğumuz bit sayısına sığmadığı durumdur. Kilometre sayacı gibi: 9999 km'den sonra 1 daha eklenince 0000'a döner; sayaç "kendini sıfırlamış" görünür, ama hiçbir şey bozulmamıştır, sadece sayaç sonunda yetmedi. İkiye tümleyende bu, işaretli bir sayının ucundan öbür ucuna sarmasıdır. Dört bitte 7 + 1: 0111 + 0001 = 1000 = −8. Aynı olay 8 bitte 127 + 1 = −128, 16 bitte 32.767 + 1 = −32.768, 32 bitte 2.147.483.647 + 1 = −2.147.483.648 olarak yaşanır (bu sayıları hesapla doğruladım). CS50'nin notlarında da buna dikkat çekilir: 32 bitlik işaretli bir int'in en büyük değeri yaklaşık 2,1 milyardır; üstüne çıkarsan beklenmedik bir değer saklanır.4 Sezgisel işaret: iki pozitif sayıyı toplayıp negatif bir sonuç çıkıyorsa, taşma olmuştur.

Gerçek hayattan bir taşma: Ariane 5

4 Haziran 1996'da Avrupa'nın yeni roketi Ariane 5 ilk uçuşunda, kalkıştan yaklaşık 37 saniye sonra rotasından çıktı, parçalandı ve patladı. CNES ve ESA'nın kurduğu soruşturma kurulunun raporuna göre yazılımın ataletsel referans sisteminde (roketin yönünü ölçen bilgisayar), 64 bitlik bir ondalıklı sayının 16 bitlik işaretli bir tamsayıya dönüştürülmesi sırasında hata çıkmıştı: sayı, 16 bitlik işaretli tamsayıda yazılabilecek en büyük değerden büyüktü. Rapor, bu dönüşüm komutlarının hataya karşı korunmadığını, aynı yerdeki benzer dönüşümlerin ise korunduğunu belirtir.5 Ders: taşma "teorik" bir kenar durum değil; yazılımı çalıştıran bit sayısı, gerçek dünyada bir sınırdır ve sınır aşıldığında ne olacağına önceden karar verilmesi gerekir (hata vermek mi, sarmak mı, daha büyük bir tip seçmek mi).

Dillerin bu konuda farklı davrandığını bilmen iyidir. Go'nun dil şartnamesine göre işaretli tamsayılarda taşma çalışma zamanı hatası vermez; sonuç, tamsayının gösterimi ve işlemle belirlenen biçimde "sarar".6 PostgreSQL'de ise 4 baytlık integer türünün aralığı −2.147.483.648 ile +2.147.483.647'dir; 8 baytlık bigint ise yaklaşık ±9,2 × 1018'e kadar gider.7 Bunların hepsi, bu bölümde gördüğümüz n bitlik ikiye tümleyen aralıklarıdır (32 ve 64 bit).

Uygulamamızda

Diyelim ki uygulamanın veritabanında her kullanıcının toplam puanı 32 bitlik bir integer sütununda duruyor. Tek kullanıcı için sorun yok: 2,1 milyar puana ulaşmak zor. Ama "tüm kullanıcıların puanlarının toplamı" gibi bir istatistik sorgusunda sınır çok hızlı yaklaşır: 10 milyon kullanıcının ortalama 500 puanı varsa toplam 5 milyar eder ve 2.147.483.647'yi aşar. Hangi türün seçileceği (integer mi, bigint mi) bir tasarım kararıdır ve cevabı bu bölümde: kaç bit, o kadar sınır. Agent'ın yazdığı bir veritabanı şemasında integer görünce kendine sorabilirsin: "bu sayı hiç 2,1 milyarı aşabilir mi?" (Veritabanlarını Bölüm 38–39'da ayrıntılı göreceğiz.)

4.4 Ondalıklı sayılar: kayan nokta

Şimdiye kadar yalnızca tamsayılarla çalıştık. Peki 3,14 ya da 0,1 nasıl yazılır? Aynı basamak değeri mantığını noktanın sağına uzatırız: ondalık noktadan sonra yarım (2−1), çeyrek (2−2), sekizde bir (2−3)... Örneğin ikilik 0,101 = 1/2 + 0/4 + 1/8 = 0,625; ondalıktaki 0,625 ile aynı değerdir.8 Sorun şu: bazı kesirler ikilikte bitmez.

Onluk sistemde 1/3'ü düşün: 0,333333… sonsuza gider; ne kadar basamak yazarsan yaz, tam 1/3 olmaz. İkilikte bu sorun 1/10 için çıkar: 0,1 sayısı ikilikte 0,0001100110011001100… diye sonsuza gider. Python belgelerinin ifadesiyle, nokta sonrası hangi sayıda bit kullanırsan kullan yalnızca bir yaklaşıklık elde edersin.8 Bilgisayarın saklayabildiği bit sayısı sınırlı olduğu için, 0,1'i değil, ona en yakın ikilik kesri saklar.

Kayan nokta nasıl saklanır

Bilgisayar ondalıklı sayıyı bilimsel gösterimin ikiliğiyle tutar: kayan nokta (floating point). Mantık: sayıyı "işaret × (1,xxxx…) × 2üs" biçiminde yaz; ondalık noktanın yeri üs ile "kayar", böylece çok küçük ve çok büyük sayılar aynı bit sayısıyla yazılabilir. Standart (IEEE 754) çift duyarlıklı biçimde 64 bit şöyle paylaşılır: 1 bit işaret, 11 bit üs, 52 bit "mantis" (sayının asıl rakamları). JavaScript'teki her Number tam olarak böyle bir 64 bitlik değerdir.9 Mantisin 52 + 1 bit olması yüzünden JavaScript'te tamsayılar da yalnızca −(253 − 1) ile 253 − 1 arasında tam olarak temsil edilir (Number.MAX_SAFE_INTEGER = 9.007.199.254.740.991).9

0.1 + 0.2 sorusu

console.log(0.1 + 0.2);          // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3);  // false

Nedeni artık biliyorsun: 0,1 ve 0,2 ikilikte bitmeyen kesirler; bilgisayar ikisinin de en yakın 64 bitlik yaklaşığını saklıyor. Python belgelerinin gösterdiği gibi, saklanan 0,1 aslında 0,1000000000000000055511151231257827… değerindedir (tam olarak 3602879701896397 / 255).8 0,2'nin saklanan değeri 0,2000000000000000111022302462515654… olur. İkisini toplayınca sonuç 0,3000000000000000444089209850062616…; 0,3'ün kendi yaklaşığı ise biraz daha küçüktür, bu yüzden 0.1 + 0.2 === 0.3 yanlış çıkar. Ekrana yazarken en kısa yeterli gösterimi seçen yazdırma 0,30000000000000004 gösterir. Bu bir JavaScript hatası değildir: Python belgelerinin dediği gibi, aynı şeyi ikilik kayan noktayı kullanan her dilde görürsün.8

Pratik kurallar

Donanım tarafı için bir not: kayan noktalı hesabı yapan devre, tamsayı ALU'sundan ayrıdır. CPU'da tamsayı ve kayan nokta için ayrı yazmaç kümeleri ve ayrı hesap birimleri vardır; Bottom-Up CS'nin ifadesiyle kayan nokta CPU içinde farklı biçimde ele alınır.2 Nand2Tetris'in tasarladığı ALU kayan noktayı hiç içermez; bu tür işlemleri yazılıma bırakır (4.5).2

Uygulamamızda

Bir hukuk dersindeki "ilerleme yüzdesi" (ör. 7 sorudan 3'ü tamamlandı = %42,857…) kayan noktayla tutulabilir; yüzde zaten yaklaşık bir gösterimdir. Ama puan, can ve seri tamsayı olmalıdır: "ödül 0,1 puan" gibi bir tasarım, toplarken sapmalar biriktirir ve iki kullanıcının eşit puanlı olup olmadığı sorusu yanlış cevaplanabilir. Bunun güvenli yolu, puanı en küçük birimle (ör. onda bir puan = 1 birim) tamsayı olarak saklayıp ekranda bölerek göstermektir. Prototipinde puanları number ile tutuyorsan (JavaScript'te tamsayı da number'dır) küçük tamsayılarla bir sorun yaşamazsın; kesirli bir değer eklediğinde bu bölümdeki sorunla karşılaşırsın.

4.5 ALU: hangi işlemi yapacağını seçen devre; kombinasyonel devre

Toplayıcıyı kurduk, negatifi ve çıkarmayı bedavaya aldık. Peki VE, VEYA, DEĞİL gibi işlemleri de yapmak istiyorsak? İşlemci elbette sadece toplamaz. Çözüm, bütün bu işlemleri tek bir parçada toplayıp, hangisinin yapılacağını seçen bir devre yapmaktır. Bu parçaya ALU (Arithmetic Logic Unit, aritmetik mantık birimi) denir. Nand2Tetris'in sözleriyle: bilgisayar mimarisinin merkezi işlemcidir ve işlemcinin merkezi ALU'dur.10

Kombinasyonel devre: hafızasız devre

Şimdiye kadar yaptığımız her devre (kapılar, toplayıcılar) aynı türdendir: kombinasyonel devre (combinational logic). Çıktısı yalnızca o anki girdilere bağlıdır; zamanı bilmez, geçmişi hatırlamaz. Girdiler değişince çıktı da değişir, o kadar.11 Bu, bir hesap makinesinin tuşlarına basıp sonucu görmek gibidir: girdiyi çektiğin anda sonuç da yok olur. (Bölüm 5'te bunun tam tersi olan ardışık devreleri, hafızalı devreleri kuracağız; ALU'nun sonucunu tutmak için onlara ihtiyaç var.)

Nand2Tetris'in ALU'su

Kitabın ALU'su 16 bitlik iki sayıyı (x ve y) alır, bir sonuç (out) verir ve altı kontrol biti (control bits) ile hangi işlemin yapılacağı söylenir: zx, nx, zy, ny, f, no. Her bit tek bir basit eylemi tetikler:10

Bu altı basit adımın birleşimi, 6 bit için 26 = 64 olasılığın içinden kitabın listelediği 18 kullanışlı fonksiyonu verir.10 Aynı mantığı bir JavaScript işleviyle yazalım:

function alu(x, y, zx, nx, zy, ny, f, no) {   // x, y: 16 bitlik işaretli sayılar
  const M = 0xFFFF;                           // 16 bitin hepsi 1: maske
  x &= M; y &= M;
  if (zx) x = 0;                              // x'i sıfırla
  if (nx) x = ~x & M;                         // x'in bitlerini ters çevir
  if (zy) y = 0;
  if (ny) y = ~y & M;
  let out = f ? (x + y) & M : (x & y);        // toplama ya da VE; taşan bit atılır
  if (no) out = ~out & M;                     // sonucu ters çevir
  const sayi = out >> 15 ? out - 0x10000 : out;   // 16 bitlik ikiye tümleyen olarak oku
  return { out: sayi, zr: sayi === 0 ? 1 : 0, ng: sayi < 0 ? 1 : 0 };
}

Satır satır: M 16 bitlik bir "maske"dir; & M ile her işlemden sonra yalnızca ilk 16 biti tutarız (taşan bitleri atarız, tıpkı gerçek 16 bitlik devrede olduğu gibi). Sonra altı bit sırayla uygulanır. En sonda sonuç ikiye tümleyen olarak (4.3) okunur ve iki ek çıkış hesaplanır: zr (sonuç sıfır mı?) ve ng (sonuç negatif mi?). Bu iki "durum çıkışı", işlemcinin bir sonraki bölümde karar vermek için kullanacağı bilgidir (Bölüm 6–7: bir şart sağlanıyorsa programda başka yere atla).10

Birkaç satırı elle (ve yukarıdaki koda bakarak) izleyelim. Kitabın örneğiyle x − 1: zx=0, nx=0, zy=1, ny=1, f=1, no=0. x olduğu gibi kalır. y önce sıfırlanır (zy), sonra ters çevrilir (ny): 000…0 ters çevrilince 111…1 olur; ikiye tümleyende bu −1'dir. Toplama seçili (f=1), yani x + (−1) = x − 1. Çıktı ters çevrilmez (no=0).10 Aşağıdaki tablo, çalıştırarak doğruladığım birkaç satırı gösteriyor (x = 5, y = 9 için):

zxnxzynyfnooutSonuç (x = 5, y = 9; x = 12, y = 10 son iki satır)
1010100sabit 0
1111111sabit 1
111010−1sabit −1
001101!x5 → −6 (ters çevrilmiş)
001111−x5 → −5
001110x − 15 → 4
000010x + y5 + 9 → 14
010011x − y5 − 9 → −4 (ng = 1)
000000x & y12 & 10 → 8
010101x | y12 | 10 → 14

Son satırdaki hile (x | y) ilginç: ALU'da VEYA işlemi yok! Ama De Morgan'ın bildik kuralıyla x VEYA y = DEĞİL( DEĞİL x VE DEĞİL y ); yani iki girdiyi ters çevir, VE yap, sonucu ters çevir. Kontrol bitleri bunu tek seferde söyler. Bu tasarımın zarafeti: işlemleri tek tek donanıma yazmak yerine, birkaç basit dönüşümün birleşimiyle çoğunu üretiriz; kitabın deyişiyle basit ve zarif tasarım, donanım işinde ucuz ve güçlü sistem demektir.10 Dikkat: bu ALU çarpma, bölme ve kayan nokta içermez; bunlar yazılımla (daha yavaş) yapılır. Bir ALU'nun ne kadar işlev sunacağı bir maliyet/performans kararıdır: donanımda yapmak pahalı ama hızlı, yazılımda yapmak ucuz ama yavaştır.2 Gerçek işlemciler daha geniş bir işlem kümesini donanımda sunar ve birden çok ALU'ya sahiptir; bunları Bölüm 6'da göreceğiz.

Bölüm 3 ile bağlantı

Bu bölümdeki her şey Bölüm 3'ün kapılarından çıktı: yarım toplayıcı bir XOR ile bir VE; tam toplayıcı iki yarım toplayıcı ile bir VEYA; çok bitlik toplayıcı tam toplayıcıların zinciri; ALU ise toplayıcı, VE ve basit dönüşümlerin (sıfırla, ters çevir) birleşimi. Bölüm 3.4'te "NAND her şeyi kurar" demiştik; bu bölüm, o "her şey"in içinde toplamanın da olduğunu gösterdi. Yani hesap yapmak için transistör ve kapıdan başka hiçbir yeni malzeme gerekmedi.

Bu bölümde neler kuruldu

İkilik toplama, ilkokul yöntemiyle sağdan sola yapılır ve her basamakta bir elde doğar. Bu iş bir yarım toplayıcı (toplam = XOR, elde = VE) ile başlar; üç bit toplayan tam toplayıcı iki yarım toplayıcı ve bir VEYA'dır; tam toplayıcıları zincirleyince çok bitlik toplayıcı olur ve elde basamaktan basamağa yayılır. Negatif sayılar ikiye tümleyen ile yazılır (bitleri ters çevir, 1 ekle); böylece çıkarma, toplayıcıyla yapılır. Bit sayısı sabit olduğundan sonuç sığmayabilir: bu taşmadır (Ariane 5 örneği). Ondalıklı sayılar kayan nokta ile saklanır; birçok kesir (0,1 gibi) ikilikte bitmediğinden 0.1 + 0.2 tam 0,3 olmaz. ALU, altı kontrol bitiyle hangi işlemin yapılacağını seçen, kombinasyonel yani hafızasız bir devredir.

Terimler sözlükte, Bölüm 4 başlığı altında. Sırada Bölüm 5: toplayıcı unutkan; sonucu tutmak için zaman ve hafıza gerekiyor.

Birincil okuma

Nisan & Schocken, The Elements of Computing Systems, Bölüm 2: Boolean Arithmetic (Nand2Tetris sitesindeki ücretsiz bölüm), özellikle 2.1 Background (ikilik toplama, ikiye tümleyen) ve 2.2 Specification (yarım/tam toplayıcı, ALU). Derinleşmek için: Computer Science from the Bottom Up, Bölüm 2: "Types and Number Representation" (negatif sayılar, kayan nokta) ve Python belgeleri: Floating-Point Arithmetic: Issues and Limitations (0.1 sorusunun en açık anlatımı).

Takıldığın her yeri yapay zekâ öğretmenine sor. Bir öneri: ondan sana dört bitlik ikiye tümleyen toplama soruları versin (ör. 6 − 3, 5 − 7, 7 + 2); sen 3 adımlı yöntemi (ters çevir, 1 ekle, topla, taşanı at) uygula; taşma olan bir örneği ayrıca bulmasını iste.

Bölüm sonu soruları

Kitaba bakmadan cevapla. Yanlış yaparsan geri bildirim seni ilgili alt bölüme yönlendirir.

1. İkilik 1 + 1 toplamının sonucu nedir?
İkilikte 1 + 1 = 2 ve 2, 10 diye yazılır: sağdaki bit (toplam) 0, soldaki bit (elde) 1. Bkz. 4.1.
2. Yarım toplayıcıda toplam biti ve elde hangi kapılarla bulunur?
İki bitin toplam biti, yalnızca farklıysa 1 olduğu için XOR; elde ise yalnızca ikisi de 1 iken 1 olduğu için VE'dir. Bkz. 4.2.
3. Tam toplayıcı, yarım toplayıcıdan neden farklıdır?
Tam toplayıcı üç bit toplar: a, b ve sağdaki basamaktan gelen elde. Böylece ikinci basamaktan itibaren çok bitlik toplama mümkün olur. Bkz. 4.2.
4. Dört bitlik ikiye tümleyende 5 (0101) sayısının eksilisi hangisidir?
Bütün bitleri ters çevir (1010), 1 ekle (1011). Tablo da aynısını verir: −5 = 1011. Bkz. 4.3.
5. 8 bitlik işaretli tamsayıda 127'ye 1 eklenirse ne çıkar?
8 bitte en büyük işaretli değer 127'dir; bir fazlası ikiye tümleyende en küçük değere sarar: −128. Bu taşmadır. Bkz. 4.3.
6. Ariane 5 kazasında yazılımın doğrudan sorunu neydi?
Soruşturma raporuna göre 64 bitlik bir ondalıklı sayı 16 bitlik işaretli tamsayıya çevrilirken değer sığmadı ve hata oluştu. Bkz. 4.3.
7. JavaScript'te 0.1 + 0.2 neden tam 0,3 çıkmaz?
0,1 ve 0,2 ikilikte bitmeyen kesirlerdir; bilgisayar ikisinin de en yakın 64 bitlik yaklaşığını saklar ve toplam 0,3'ün yaklaşığıyla aynı değildir. Bkz. 4.4.
8. ALU'nun "kombinasyonel" olması ne anlama gelir?
Kombinasyonel devrenin hafızası yoktur: girdi değişince çıktı değişir, geçmişe bakmaz. Hatırlayan devreler Bölüm 5'in konusu. Bkz. 4.5.
9. Kendi cümlenle: bir bilgisayar çıkarmayı neden ayrı bir devre kurmadan, toplayıcıyla yapabilir? 5 − 3 işlemini dört bitte adım adım göster.
Örnek cevabı göster

İkiye tümleyende bir sayının negatifi, bitleri ters çevirip 1 eklemekle bulunur ve negatif sayılar da toplayıcıdan geçince doğru sonuç verir; bu yüzden x − y = x + (−y) olur ve ayrı bir çıkarma devresi gerekmez. 5 = 0101, 3 = 0011; 3'ün negatifi: ters çevir 1100, 1 ekle 1101. Topla: 0101 + 1101 = 1 0010; en soldaki taşan bit atılır, sonuç 0010 = 2 ✓.

10. Bir uygulamada 10 milyon kullanıcının ortalama 500 puanı var. Tüm puanların toplamını 32 bitlik işaretli bir tamsayıda tutmak neden sorun? Hangi tür güvenli olur? Hesabı göster.
Örnek cevabı göster

10.000.000 × 500 = 5.000.000.000. 32 bitlik işaretli tamsayının en büyük değeri 2.147.483.647'dir; 5 milyar bunu aşar, yani taşar ve sarar (negatif ya da hatalı bir değer çıkar, ya da veritabanı hata verir). 64 bitlik işaretli tamsayı (ör. PostgreSQL'de bigint) yaklaşık 9,2 × 1018'e kadar gider; bu toplam rahatça sığar.

Kaynaklar

  1. Nisan & Schocken, The Elements of Computing Systems, Bölüm 2 (Boolean Arithmetic), 2.1 Background (ikilik toplama, taşma, ikiye tümleyen, aralık, x − y = x + (−y)) ve 2.2.1 Adders (yarım toplayıcı, tam toplayıcı, Add16, Inc16).
  2. Aynı kaynak, 2.2.2 ALU ve 2.4 Perspective: elde yayılma gecikmesi, carry look-ahead; ALU'da çarpma, bölme ve kayan nokta olmaması; maliyet/performans dengesi. Kayan nokta için ayrıca Ian Wienand, Computer Science from the Bottom Up, Bölüm 3: CPU'da tamsayı ve kayan nokta yazmaçlarının ayrı olması.
  3. Ian Wienand, Computer Science from the Bottom Up, Bölüm 2: Types and Number Representation, Two's Complement: tasarımcının yalnızca toplama devresine ihtiyaç duyması; çıkarmanın negatifi alıp toplamayla yapılması; tüm modern bilgisayarların ikiye tümleyen kullanması.
  4. CS50x 2026, Lecture 1 ders notları, "Overflow" ve "Types" bölümleri: tamsayı taşması, işaretli int'in en büyük değeri 2147483647.
  5. SIAM News, Inquiry Board Traces Ariane 5 Failure to Overflow Error (soruşturma kurulu raporunun özeti): 4 Haziran 1996 uçuşu, yaklaşık 37 saniye sonra ataletsel referans sistemlerinin durması, 64 bitlik kayan noktanın 16 bitlik işaretli tamsayıya dönüştürülmesi, korumasız dönüşüm.
  6. The Go Programming Language Specification, Arithmetic operators → Integer overflow: işaretli tamsayıda taşmanın çalışma zamanı hatası doğurmaması.
  7. PostgreSQL belgeleri, Numeric Types: smallint, integer (4 bayt, −2147483648…+2147483647), bigint (8 bayt).
  8. Python belgeleri, 15. Floating-Point Arithmetic: Issues and Limitations: ikilik kesirler, 0,1'in sonsuz ikilik açılımı, saklanan değerin 3602879701896397 / 2**55 olması ve tam ondalık açılımı. 0,2 ve toplamın uzun ondalık açılımı bu bölümün yazarı tarafından decimal ile hesaplanmıştır.
  9. MDN, Number: Number türünün 64 bitlik IEEE 754 çift duyarlıklı değer olması (1 bit işaret, 11 bit üs, 52 bit mantis); güvenli tamsayı aralığı ±(253 − 1). 19.99 * 100 ve 0.1 + 0.2 sonuçları Node.js'te çalıştırılarak doğrulanmıştır.
  10. Nand2Tetris, Project 2: Boolean Arithmetic: ALU'nun işlemcinin merkezi olması; 16 bitlik ana çıkış ve zr/ng durum çıkışları.
  11. Nisan & Schocken, The Elements of Computing Systems, Bölüm 3, 3.1 Background, "Time Matters": kombinasyonel devrede çıktının girdilerin zamandan bağımsız bir işlevi olması.