Ana içeriğe geç

Bölüm 1:Sayılar: Bit'ten Kayan Noktaya

13 dakikalık okumaGüncelleme:

Bilgisayarlar hakkında konuşurken sürekli karşımıza çıkan birkaç kelime var: bit, byte, binary, hex… Eğer bunlar sana yabancı geliyorsa endişelenme, bu bölüm ve bir sonraki tam olarak bunun için var. Atlama; buradaki her kavram sonrakinin temeli.

Hepsi tek bir soruya bağlanıyor: bilgisayar neden sadece iki şey sayabiliyor ve bu iki şeyle nasıl her şeyi anlatıyor?

Bu bölümde neyi çözüyoruz?

  • Bilgisayarların neden sadece 0 ve 1 anladığını göreceğiz.
  • Bit, byte ve ikilik sistemi pratikleştireceğiz.
  • Negatif sayıların nasıl tutulduğunu ve aralığın neden simetrik olmadığını çözeceğiz.
  • 0.1 + 0.2 işleminin neden tam olarak 0.3 etmediğini çözeceğiz.
  • Onaltılık sistemin neden var olduğunu ve nerede işe yaradığını göreceğiz.

Neden Sadece 0 ve 1?

Bilgisayarların içinde milyarlarca minik anahtar var: transistörler. Bir transistörün yaptığı iş şaşırtıcı derecede basittir — bir devrede akımın geçmesine izin verir ya da vermez. Açık ve kapalı, o kadar.

Bir ucundan bakınca sıradan bir elektrik anahtarı. Ama bu anahtarları belirli kalıplarda birbirine bağladığında ortaya iki tür devre çıkar: karar verebilenler (lojik kapılar) ve durumunu hatırlayabilenler (bellek hücreleri). Bilgisayar dediğimiz şey, bu iki tür devrenin milyarlarcasının üst üste bindirilmesinden ibaret.

Biz de o iki kararlı duruma bir isim veririz: 0 ve 1. Tüm hesaplamalar, tüm videolar, tüm oyunlar, hatta şu an okuduğun bu cümle bile — hepsi bu iki durumun kontrollü kombinasyonlarından oluşur.

Buradaki en yaygın yanılgıyı baştan kapatalım: “bir transistör = bir bit” değildir. Bir transistör temel tuğladır; bit ise o tuğlalarla kurulan devrenin tuttuğu anlamdır. Hızlı ama pahalı olan SRAM’de tek bir bit, birbirini besleyen birkaç transistörden kurulu küçük bir kilitle (latch) tutulur. Ucuz ve yoğun olan DRAM’de ise tek transistör ve minik bir şarj deposu (kondansatör) yeter — bu yüzden DRAM’in içeriğinin sürekli tazelenmesi gerekir. İkisinin farkına 7. bölümde cache’i konuşurken döneceğiz.

“Milyarlarca” derken abartmıyorum: Apple M3 çipinde yaklaşık 25 milyar transistör var. Hepsi de yukarıda anlattığım tek işi yapıyor — geçir ya da geçirme.

Şimdi bu anahtarlardan mantığın nasıl çıktığına bakalım. Birkaç transistörü öyle bağlarsın ki çıkış ancak her iki giriş de 1 iken 1 olur — buna AND dersin. Öyle bağlarsın ki herhangi biri 1 iken çıkış 1 olur; bu OR. Girişi tersine çeviren tek düzenek NOT, iki giriş birbirinden farklıysa 1 veren düzenek de XOR’dur.

Aşağıdaki tabloda her kapının hangi girişe ne cevap verdiğini göreceksin. Aklında tutmanı istediğim şey şu: bilgisayarın yaptığı her şey — toplama, video çözme, bu sayfayı ekrana çizme — bu birkaç kapının milyarlarca kez üst üste bindirilmesinden ibaret.

Boolean operatörlerin (AND, OR, NOT, NAND, NOR, XOR) doğruluk tabloları ve lojik kapı sembolleri.

Kaynak: William Stallings, Computer Organization and Architecture, Ch. 11 — Digital Logic, Slide 5.

İki kapının ayrıca not düşmeye değer bir özelliği var.

XOR kendi kendini geri alır. Bir değeri aynı anahtarla iki kez XOR’larsan orijinali geri elde edersin: A ⊕ K ⊕ K = A. Bu küçük özellik, en basit şifreleme yöntemlerinin, grafik programlarındaki “ters çevir” işlemlerinin ve hata tespit algoritmalarının temelidir.

NAND tek başına yeter. NAND (AND’in tersi) evrensel bir kapıdır: yalnızca NAND kapıları kullanarak AND’i de, OR’u da, NOT’u da, dolayısıyla herhangi bir devreyi kurabilirsin. Bunun donanımda da bir karşılığı var. Bugünkü işlemcilerin üretildiği teknolojide (CMOS) en az transistörle kurulan kapılar NAND ve NOR’dur; AND dediğimiz şey aslında bir NAND ile arkasına eklenmiş bir NOT’tur. Fikri sonuna kadar götüren örnek de meşhurdur: Ay’a inen Apollo uçuşlarının yol bilgisayarı, bütün mantık devrelerini tek tip bir kapıyla, üç girişli NOR ile kurmuştu.

Bit ve Byte

Neden 8? Fizikten gelen bir zorunluluk yok; tarihte 6 ve 9 bitlik byte kullanan makineler de oldu. 8’i kalıcı yapan şey bir tasarım kararı ve o kararın arkasından gelen alışkanlık.

Hikâye şöyle gelişti. ASCII 7 bit ile 128 karakteri temsil edebiliyordu, yani metin için yedi bit yetiyordu. Birçok sistem kalan sekizinci biti parity için ayırdı. Parity fikri çocukça basittir: diğer yedi bitteki 1’lerin sayısı tek mi çift mi, onu söyleyen bir bit eklersin. Hat üzerinde bir bit bozulursa sayı tutmaz ve hatayı yakalarsın. Tek bitlik hatayı fark eder, düzeltemez — ama 1960’ların gürültülü hatlarında bu bile büyük kazançtı.

Sonra IBM 1964’te System/360’ı çıkardı ve 8 bitlik byte’ı kendi mimarisinin merkezine koydu. Seçimin arkasında sağlam bir hesap vardı. O dönemin iş bilgisayarları parayı onluk basamaklarla hesaplıyordu ve bir onluk basamak (0-9) 4 bite sığar; sekiz bit, iki basamağı tek bir byte’a yan yana koymaya izin verdi. Üstelik 8, 2’nin kuvveti olduğu için adres hesaplarını da sadeleştiriyordu. Bu makine dönemin sektör standardı oldu; ardından gelen donanım da yazılım da bu ölçüye göre büyüyünce, 1 byte = 8 bit artık tartışılmayan bir alışkanlığa dönüştü.

TerimDeğer
1 byte8 bit
1 kilobyte (kB)1.000 byte
1 kibibyte (KiB)1.024 byte
1 megabyte (MB)1.000.000 byte
1 mebibyte (MiB)1.024 × 1.024 byte

Bu kitapta bellek boyutları bağlamında genelde kibibyte (KiB), mebibyte (MiB) gibi ikili tabanlı birimleri kullanacağız. Çünkü bilgisayarlar 2’nin kuvvetleriyle düşünür.

İkilik (Binary) Sistem

Sen on parmağınla saydığın için onluk sistemi doğal buluyorsun: 0’dan 9’a on rakam, sonra basamak atlarsın. Donanımın parmağı yok; elinde güvenilir biçimde ayırt edebildiği yalnızca iki durum var. O yüzden bilgisayar ikilik sistemle sayar: sadece 0 ve 1. Bu iki değere yazılım tarafında false ve true, devre tarafında düşük ve yüksek voltaj dersin — isim değişir, iş değişmez. Önemli olan voltajın kaç volt olduğu değil, iki durumun birbirine karışmamasıdır.

DecimalBinary
00
11
210
311
4100
5101
6110
7111
81000

Bu sistemi zaten biliyorsun, sadece farkında değilsin. Onluk sistemde 523 yazdığında aslında şunu kastediyorsun: 5×10² + 2×10¹ + 3×10⁰. Her basamak, tabanın bir kuvveti. İkilik sistemde değişen tek şey taban: 10 yerine 2.

Yani binary’deki her basamak 2’nin bir kuvvetini temsil eder. Sağdan sola: 2⁰, 2¹, 2², 2³…

Örneğin 1011 binary:

Peki Eksi Sayılar?

Buraya kadar her şey pozitif. Ama -5 nasıl saklanıyor? Elimizde eksi işareti diye bir şey yok; yalnızca 0’lar ve 1’ler var.

Akla gelen ilk çözüm en soldaki biti işaret olarak kullanmaktır: 0 ise artı, 1 ise eksi. Mantıklı görünür ve çalışmaz. İki sorunu vardır:

  1. İki tane sıfır olur. 0000 0000 artı sıfır, 1000 0000 eksi sıfır. Birbirine eşit olması gereken iki farklı bit deseni. Her karşılaştırmada bunu ayrıca düşünmek gerekir.
  2. Toplama bozulur. 5 + (-5) yapmak istersen devrenin önce işaret bitlerine bakıp “aslında bu bir çıkarma” diye karar vermesi gerekir. Yani ayrı bir çıkarma devresi kurman gerekir.

Gerçekte kullanılan çözüm çok daha zarif: ikiye tümleyen (two’s complement). Bir sayının negatifini bulmak için iki adım yeter — tüm bitleri ters çevir, sonra 1 ekle.

İşlem8 bit
50000 0101
bitleri ters çevir1111 1010
1 ekle → −51111 1011

Şimdi sihri gör. 5 + (-5) toplamasını sıradan bir toplayıcıya verelim:

  0000 0101   (5)
+ 1111 1011   (-5)
-------------
1 0000 0000   → taşan bit atılır, geriye 0000 0000 kalır

Sonuç sıfır. Üstelik devre “bunlardan biri negatifmiş” diye bir şey bilmiyor; sadece topladı. İkiye tümleyenin bütün amacı budur: çıkarma diye ayrı bir işleme gerek kalmaz, negatifini alıp toplarsın. Donanımda tek bir toplayıcı hem toplamayı hem çıkarmayı yapar.

Bir de sıfır artık tek: 0000 0000. Karşılığında küçük bir tuhaflık ortaya çıkar — aralık simetrik değildir. 8 bit ile temsil edilebilen sayılar −128 ile +127 arasındadır. Negatif tarafta bir sayı fazladır, çünkü artık ikinci bir sıfıra yer harcanmıyor.

DerinleşmeO eksik bir sayının gerçek hayattaki bedeli

Aralığın simetrik olmaması akademik bir ayrıntı gibi durur. Değildir.

-128’in mutlak değeri +128’dir ve +128, 8 bitlik işaretli bir sayıya sığmaz. Yani abs(-128) sorusu 8 bit dünyasında cevapsızdır. Aynı şey her genişlikte tekrarlanır: 32 bitlik bir tam sayıda abs(INT_MIN) tanımsızdır.

Bu, teorik bir merak değil, gerçek bir açık sınıfıdır. Bir programın “uzunluk negatif olamaz, mutlak değerini alayım” diye yazılmış tek satırı, saldırganın INT_MIN göndermesiyle negatif kalır ve ardından gelen sınır kontrolünü sessizce geçer.

C ve C++ standartları işaretli tam sayı taşmasını tanımsız davranış ilan eder; taşmayı “sarma” olarak varsayan kod, derleyici optimizasyonu açıldığında bambaşka davranabilir. Derleyici “taşma olamaz” varsayımıyla kontrolü tamamen silebilir — ki bu, gerçekte yazılmış güvenlik kontrollerinin optimizasyonla yok olduğu meşhur hataların kaynağıdır.

Modern diller bu sınıfı kapatmayı seçer. Rust’ın yolu şu: hata ayıklama derlemesinde taşma anında panik eder, yani hatayı üretildiği satırda gösterir. Üretim derlemesinde bu kontrol varsayılan olarak kapalıdır — bedava değil çünkü — ve taşma sessizce sarmalanır; istersen overflow-checks ayarıyla orada da açabilirsin. Asıl çözüm ise başka yerde: niyetini işlemin adına yazarsın. wrapping_add sarmalanmayı, checked_add başarısızlığı döndürmeyi, saturating_add sınırda durmayı seçer. Yani dil “hangi davranışı istediğini söyle” der; varsayım bırakmaz.

Peki Ondalık Sayılar?

Tam sayıları çözdük, eksileri de. Sırada programcıların en çok tökezlediği yer var.

Bir hesap makinesi aç ve 0.1 + 0.2 yaz. Sonuç 0.3 çıkar. Aynı işlemi neredeyse her programlama dilinde yap; büyük ihtimalle 0.30000000000000004 görürsün. Bu bir dil hatası değil, kütüphane hatası da değil. Donanımın sayıyı saklama biçiminin doğrudan sonucu.

Sebebi aslında tanıdık. Ondalık sistemde 1/3’ü yazamazsın: 0,3333… diye sonsuza gider, bir yerde kesersin. Aynı şey ikilik sistemde 1/10 için geçerli. 0.1, ikilik tabanda devirli bir kesirdir; makine onu tam olarak saklayamaz, en yakın komşusuna yuvarlar. Yuvarlama hatası daha toplama işlemi başlamadan, sayı yazıldığı anda oluşur.

Kayan Nokta: Bilimsel Gösterimin İkilik Hâli

Peki nasıl saklıyor? Sabit sayıda bit ile hem çok büyük hem çok küçük sayıları tutmak gerekiyor. Çözüm, bilimsel gösterimin ikilik karşılığı: her sayı bir işaret, bir üs ve bir mantis (anlamlı basamaklar) olarak üçe bölünür. Tıpkı 6,022 × 10²³ yazarken yaptığın gibi — nokta sabit değil, üsse göre kayar. Adı buradan geliyor: kayan nokta (floating point).

Bu düzenin standardı IEEE 754’tür ve pratikte her işlemci onu uygular. En yaygın iki boyu şöyle:

TürToplamİşaretÜsMantisYaklaşık ondalık basamak
float (tek duyarlık)32 bit1823~7
double (çift duyarlık)64 bit11152~16

Buradan iki sonuç çıkıyor. Birincisi, kayan nokta sayıları sayı doğrusunda eşit aralıklı değildir: sıfırın yakınında komşu iki sayı birbirine çok yakın, milyarların yanında ise aralarında koca boşluklar vardır. İkincisi, hassasiyet mutlak değil görelidir — bu yüzden büyük bir sayıya çok küçük bir sayı eklemek hiçbir şey değiştirmeyebilir.

Bunun Pratikte Anlamı

Üç kural yeter.

Kayan nokta sayılarını eşitlikle karşılaştırma. a == b yerine aradaki farkın kabul edilebilir bir eşiğin altında olup olmadığına bak.

Parayla kayan nokta kullanma. Bir kuruşluk yuvarlama hatası tek işlemde görünmez ama milyonlarca işlemde birikir. Para için ya tam sayı kuruş tut ya da dilinin ondalık türünü kullan.

Toplama sırası sonucu değiştirir. Kayan noktada (a + b) + c ile a + (b + c) aynı sonucu vermeyebilir; okuldaki birleşme özelliği burada geçerli değildir. Çok sayıda değeri toplarken küçükten büyüğe gitmek hatayı azaltır.

DerinleşmeSonsuz, tanımsız ve eksi sıfır: standardın tuhaf köşeleri

IEEE 754 yalnızca sayı saklamaz; sayı olmayan durumlar için de yer ayırır. Üs alanının en büyük değeri bu iş için rezerve edilmiştir.

Sonsuz. Bir ondalık sayıyı sıfıra böldüğünde çoğu dilde program çökmez, sonuç Infinity olur. Bu, hesabın taşması durumunda da ortaya çıkar: sonucu doublea sığmayacak kadar büyük bir çarpım sessizce sonsuza yuvarlanır. Buradaki “ondalık” kelimesi önemli — kural yalnızca kayan noktalı sayılar için geçerlidir. Aynı işlemi tam sayılarla yaparsan bambaşka bir şey olur: x86’da C programı SIGFPE sinyaliyle çöker, Python ZeroDivisionError fırlatır, Java ArithmeticException verir. 1.0 / 0 ile 1 / 0 aynı dilde bile aynı şey değildir.

C’deki çöküş aslında dilin değil işlemcinin tercihidir. x86’nın bölme talimatı sıfıra bölmede bir donanım hatası üretir; ARM64’ünkü ise hiçbir şey demeden 0 döndürür. C standardı tam sayıyı sıfıra bölmeyi tanımsız bıraktığı için ikisi de kurala uygundur — aynı C programı bir makinede çöker, diğerinde sessizce yanlış sonuçla devam eder.

NaN (Not a Number). Sonsuzu sonsuzdan çıkarmak ya da eksi bir sayının karekökünü almak gibi cevabı tanımsız işlemler bunu üretir. En sinsi özelliği bulaşıcı olmasıdır: NaN içeren her hesap NaN döndürür, yani bir yerde oluşan hata sessizce bütün tabloyu boşaltabilir. Bir tuhaflığı daha var — NaN kendisine bile eşit değildir. x != x ifadesinin doğru çıkması, bir değerin NaN olup olmadığını anlamanın klasik yoludur.

Eksi sıfır. İşaret ayrı bir bit olduğu için +0 ve -0 diye iki ayrı gösterim vardır. Karşılaştırmada eşit sayılırlar, ama bölmede ayrışırlar: biri artı sonsuza, diğeri eksi sonsuza gider.

Bunların hepsi kasıtlı tasarım. Alternatif, her taşmada ya da tanımsız işlemde programın durmasıydı; standart bunun yerine hesabın devam etmesini ve hatanın taşınmasını seçti.

Hexadecimal (Onaltılık) Sistem

İkilik sistemin bir bedeli var: yazması ve okuması dayanılmaz derecede uzun. 255 sayısını 11111111 diye yazmak zorunda kalırsın; birkaç basamak sonra gözün kayar ve fazladan bir 1 saydığını fark bile etmezsin.

Bunu çözmek için onaltılık (hexadecimal) sisteme geçeriz: 0-9’un devamına A’dan F’ye altı harf ekleriz, böylece tek karakterle 0 ile 15 arasını anlatabiliriz.

Hex’i asıl kullanışlı yapan şey ise şu eşitlik: 16 = 2⁴. Yani her hex karakteri tam olarak 4 bit’e karşılık gelir. Bunun pratik sonucu çok büyük — binary ile hex arasında çevirmek bir hesap işi değil, sadece gruplama işidir:

1101 0110 → sağdan dörderli böl → 1101 = D, 0110 = 6 → D6

Hiç toplama çıkarma yapmadın; sadece grupları okudun. 4 bitlik bu gruba nibble denir (byte’ın yarısı olduğu için “kemirik”). Sekizlik sistem (octal) bu yarışı neden kaybetti diye sorarsan: 8 = 2³’tür ve 3, 8’i tam bölmez — bir byte octal’de iki buçuk basamak eder, ki bu kimsenin işine yaramaz.

Octal yine de ortadan kalkmadı; büyük ihtimalle farkında olmadan kullandın bile. Linux’ta chmod 755 yazdığında o sayı octal’dir. Dosya izinleri sahip, grup ve diğerleri için üçer bitten oluşur (okuma, yazma, çalıştırma) ve üç bit tam olarak bir octal basamağıdır: 7 = 111 üçüne de izin verir, 5 = 101 yalnızca yazmayı kapatır.

DecimalBinaryHex
000000
101010A
151111F
160001 000010
2551111 1111FF

Bir byte (8 bit) her zaman iki hex karakteriyle gösterilebilir. Bu yüzden bellek dökümleri ve makine kodları genelde hex formatında yazılır.

Dış Kaynaklar

Özet

Peki, ne öğrendik?

  • Bilgisayarlar iki durumu güvenilir biçimde ayırt edebildiği için ikilik sistemi kullanır; mesele matematik değil, elektrik.
  • Bit en küçük bilgi birimidir, byte sekiz bittir ve belleğin adreslenebilir en küçük parçasıdır.
  • Negatif sayılar ikiye tümleyen ile tutulur; bu yüzden aralık simetrik değildir ve eksi tarafta bir sayı fazladır.
  • Kayan nokta sayıyı işaret, üs ve mantis olarak saklar. 0.1 ikilik tabanda devirli olduğu için tam tutulamaz; eşitlikle karşılaştırma ve parayla kullanım bu yüzden hatalıdır.
  • Onaltılık sistem bir byte’ı tam olarak iki karakterle gösterdiği için bellek dökümlerinin ve makine kodunun ortak dilidir.

Sayıları hallettik. Ama bilgisayarın belleğinde sadece sayı yok: metin de var, adresler de var, programın kendisi de var. Şimdi sıra bunların hepsinin aslında yine sayı olduğunu görmekte — aradaki tek fark, o sayıyı nasıl yorumladığın.

Bir byte yalnızca 0 ile 255 arası bir sayıysa, aynı byte nasıl oluyor da bazen bir harf, bazen bir adres, bazen de bir talimat oluyor?

2. bölüme devam et: Metin, Bellek ve Kod