Bölüm 8:İşlemciyi Hızlandıran Hileler
CPU'ya “Sen”i Katmak yazısının parçası: bilgisayarının programları nasıl çalıştırdığına doğru inen uzun bir teknik tavşan deliği.
Tüm bölümler
- Giriş
- Başlamadan Önce
- Temeller
- Kernel, User Mode ve Syscall
- Mimariler: x86, ARM ve Diğerleri
- Bellek Hiyerarşisi
- Cache Nasıl Çalışır
- Zamanı Dilimle
- İşlemciyi Hızlandıran Hileler
- Tahmin, Spekülasyon ve Spectre
- Bir Program Nasıl Çalıştırılır?
- Shell'den Kernel'e
- Bir ELF Ustasına Dönüşmek
- Bellek Aslında Sanal
- Adres Çevirisi ve TLB
- Bellek Güvenliği ve Sertleştirme
- Fork'lar ve COW'lar Hakkında Konuşalım
- Dosya Sistemi ve I/O
- Sanal Makineler ve Container'lar
- Son Söz
Buraya kadar CPU’yu basit bir “talimat oku, yürüt, tekrar et” makinesi olarak anlattım. O resim yanlış değil, sadece eksik.
Sen bu cümleyi okurken önündeki işlemci muhtemelen yüzden fazla talimatı aynı anda havada tutuyor, bir sonraki if’in hangi tarafa sapacağını daha koşulu hesaplamadan tahmin ediyor ve o tahmine güvenip çoktan işe koyulmuş durumda. Bu bölümde o hilelerin her birini tek tek açacağız.
Bu bölümde işlemcinin “aynı anda daha fazla iş yapma” yollarını göreceğiz. Tahmin etmeye dayananları ise bir sonraki bölüme bırakıyorum, çünkü onların hikâyesi güvenlikle iç içe.
Bu bölümde neyi çözüyoruz?
- CPU’nun neden sadece saat hızı artırılarak hızlanamadığını göreceğiz.
- Pipeline ve superscalar yürütmenin nasıl çalıştığını anlayacağız.
- Talimatların neden sırasız yürütülüp sıralı emekli edildiğini çözeceğiz.
- Çok çekirdek, SMT ve SIMD’in birbirinden farkını netleştireceğiz.
Önce Şunu Soralım: Bu Hilelere Neden İhtiyaç Var?
2004’te bir masaüstü işlemcisi 3 GHz’de çalışıyordu. Bugün, yirmi yıl sonra, hâlâ 3-5 GHz civarındayız. Oysa aynı sürede transistör sayısı yüzlerce kat arttı. Neden saat hızı artmadı?
Cevap, adını Robert Dennard’dan alan bir kuralın çökmesi. Dennard ölçekleme, transistörler küçüldükçe güç yoğunluğunun sabit kaldığını söylüyordu: daha küçük transistör daha az voltaj ister, daha az voltaj daha az ısı demektir, yani hızı artırmak bedavaya yakındır. Yaklaşık 2005’e kadar sektör bu bedavaya yakın hızlanmanın üzerinde yaşadı — yazılımcılar hiçbir şey yapmadan, sadece bekleyerek programlarının hızlandığını gördü.
Sonra fizik araya girdi. Transistörler o kadar küçüldü ki sızıntı akımı baskın hâle geldi; kapalı transistör bile akım harcamaya başladı. Voltajı daha fazla düşürmek mümkün olmayınca, saat hızını artırmanın bedeli doğrudan ısıya dönüştü. Duvara çarpıldı.
Sektörün cevabı yön değiştirmek oldu ve iki koldan ilerledi:
- Daha çok çekirdek. Tek çekirdeği hızlandıramıyorsan yan yana koy.
- Tek çekirdekten daha çok iş çıkar. Aynı saat hızında saat çevrimi başına daha fazla talimat bitir.
Bu bölümün ağırlığı ikinci kolda: pipeline, superscalar, out-of-order ve branch prediction, hepsi tek bir sorunun cevabı — saat hızı artmıyorsa aynı saatte nasıl daha fazla iş yaparım? Birinci kola, yani çekirdek çoğaltmaya, bölümün sonunda döneceğiz.
DerinleşmeAmdahl mı Gustafson mu: daha çok çekirdek ne kadar işe yarar?
Birinci kol, yani “daha çok çekirdek”, ilk bakışta sınırsız görünür. Değildir ve sınırı 1967’de Gene Amdahl koydu.
Amdahl yasası şunu söyler: bir programın ancak paralelleşebilen kısmı hızlanır; seri kalan kısım hızlanmaz ve toplam hızlanmayı o belirler. Formülü basit — programın %5’i seri ise, sonsuz çekirdekle bile en fazla 20 kat hızlanabilirsin. %1 seri ise tavan 100 kattır. Çekirdek eklemek bu tavanı yükseltmez, yalnızca ona yaklaştırır.
Sayılarla: %95 paralel bir programda 16 çekirdek yaklaşık 9,1 kat hızlanma verir. 64 çekirdek 15,4 kat. Yani çekirdeği dört katına çıkarmak hızlanmayı %70 artırır. 1024 çekirdek ise 19,6 katta durur; artık çekirdek eklemek neredeyse hiçbir şey getirmez.
Bu, 1980’lerde paralel hesaplamaya karamsar bakılmasının sebebiydi. Sonra 1988’de John Gustafson karşı bir argüman getirdi: Amdahl yanlış soruyu soruyor.
Gustafson’ın gözlemi şu: pratikte insanlar aynı problemi daha hızlı çözmek için makine büyütmez; daha büyük problem çözmek için büyütür. Hava tahmini yapan biri, aynı çözünürlüklü tahmini daha hızlı almak istemez; aynı sürede daha yüksek çözünürlüklü tahmin ister. Problem büyüdükçe seri kısım (kurulum, toplama, çıktı) sabit kalırken paralel kısım büyür, yani seri oran düşer.
İkisi çelişmez; farklı soruları cevaplar. Amdahl: problem sabitken çekirdek eklemenin getirisi nedir? Gustafson: çekirdek sayısı arttıkça ne kadar büyük problem çözebilirim?
Pratik sonuç ikisinin ortasındadır ve bugün hâlâ geçerlidir: sabit boyutlu bir işi paralelleştirerek çok az kazanırsın, o yüzden ölçeklenme hikâyesinin gerçekten ölçeklenen kısmı neredeyse her zaman veri tarafındadır. Sunucu dünyasında çekirdeklerin işe yaramasının sebebi tek bir isteği hızlandırmaları değil, aynı anda binlerce bağımsız istek işlemeleridir.
Pipeline: Fabrika Bandı Gibi Çalışmak
İlk CPU’lar bir talimatı tamamen bitirmeden diğerine geçmezdi. Fetch, decode, execute, write-back adımları tek tek yapılırdı. Modern CPU’lar ise pipeline (boru hattı) kullanır. Fabrika bandı gibi, her aşamada farklı bir talimat bulunur.
| Aşama | Ne Yapar? |
|---|---|
| Fetch | Bellekten bir sonraki talimatı getir |
| Decode | Talimatı çözümle (ne yapacağını anla) |
| Execute | İşlemi gerçekleştir (toplama, çarpma vb.) |
| Memory | Gerekirse belleğe oku/yaz |
| Write-back | Sonucu register’a geri yaz |
Bunu bir tablo olarak değil, zaman içinde görmek çok daha ikna edici. Aşağıdaki şemayı adım adım ilerlet: her adım bir saat çevrimi, her satır bir talimat.
- 1. çevrim. Yalnızca ilk talimat işlenmeye başladı: bellekten getiriliyor (IF). Diğer dört aşama boş duruyor.
- 2. çevrim. Birinci talimat çözülürken (ID) ikinci talimat getiriliyor. Getirme birimi boş kalmadı — pipeline'ın bütün fikri bu.
- 3. çevrim. Üç aşama aynı anda çalışıyor. Hiçbir talimat daha bitmedi ama üçü birden yolda.
- 4. çevrim. Dört aşama dolu. Buraya kadar tek bir talimat bile tamamlanmadı; pipeline hâlâ doluyor.
- 5. çevrim: ilk sonuç. Birinci talimat WB aşamasında bitiyor ve beş aşama da dolu. Pipeline tam kapasiteye ulaştı.
- 6. çevrim: kararlı hâl. Artık her çevrimde bir talimat bitiyor. Tek bir talimat hâlâ beş çevrim sürüyor — değişen tek şey, aynı anda beş talimatın havada olması.
Beş çevrim boyunca hiçbir talimat bitmiyor, sonra birden her çevrimde bir tane bitmeye başlıyor. Pipeline’ın vaadi tam olarak bu: tek bir talimatı hızlandırmıyor, aynı anda kaç talimatın havada olduğunu artırıyor.
Aynı deseni daha uzun bir örnekte, dokuz talimat ve on dört çevrim üzerinde de görebilirsin:

Kaynak: William Stallings, Computer Organization and Architecture, 10. baskı, Şekil 14.10.
Şemanın iki şeyi birden anlattığına dikkat et. Merdiven deseni pipeline’ın dolmasıdır: ilk talimat altı çevrim sürer, ama altıncı çevrimde artık altı talimat aynı anda ilerlemektedir. Bu noktadan sonra her çevrimde bir talimat tamamlanır — tek tek altı çevrim süren işler, ardışık geldiklerinde çevrim başına bire iner.
Bir de sol üstteki boşluğa bak: pipeline dolana kadar geçen o birkaç çevrim bedavaya gelmez. Kısa süren bir kod parçasında pipeline dolmadan iş biter ve vaat edilen hızlanma hiç gerçekleşmez.
Buradaki aşama isimleri (FI, DI, CO, FO, EI, WO) Stallings’in altı aşamalı modelinden geliyor ve yukarıdaki beş aşamalı tablodan biraz farklı bölünmüş. Aşama sayısı mimariye göre değişen bir tasarım tercihidir: klasik RISC beşte kalır, Pentium 4 yirmiyi aşmıştı. Derin pipeline saat hızını yükseltmeyi kolaylaştırır ama birazdan göreceğimiz yanlış tahmin cezasını da o oranda büyütür.
Pipeline Hazardları
Pipeline’ın verimli çalışması için talimatlar sürekli akmalıdır. Ama bant her zaman akmaz. Bazen bir talimat sırası geldiği hâlde kendi aşamasına giremez ve arkasındaki herkes onu bekler. Bu duruma hazard deniyor. Üç ayrı sebepten çıkar ve üçünü de tanıman gerekiyor, çünkü bu bölümün geri kalanındaki bütün hilelerin varlık sebebi bunlar.
- Structural hazard (yapısal): İki talimat aynı anda aynı donanım birimini kullanmaya çalışır. Örneğin tek bir bellek portu hem fetch hem memory aşamasında kullanılmak istenirse biri beklemek zorundadır.
- Veri Hazardı (Data): Bir talimatın çalışması için önceki talimatın sonucuna ihtiyaç duyulur ama o sonuç henüz yazılmamıştır.
- Kontrol Hazardı (Control): Koşullu dallanma (
if,jmp) sonrası hangi talimatın geleceği bilinmez; pipeline’ın doğru yoldan devam etmesi için beklenir veya tahmin edilir.
Veri hazardlarından en yaygın olanı Read-After-Write (RAW) bağımlılığıdır:
add eax, ebx
mov ecx, eax ; önceki talimatın sonucunu bekliyor — stall'e sebep olurmov ecx, eax talimatı eax’in güncellenmiş değerine ihtiyaç duyar. Eğer add talimatının write-back aşaması henüz tamamlanmadıysa, CPU bir veya daha fazla saat çevrimi stall (duraklama) ekler veya bypassing/forwarding ile sonucu doğrudan execute birimine yönlendirir.
Superscalar: Birden Fazla Fabrika Bandı
Pipeline tek bir fabrika bandıdır. Superscalar işlemciler ise aynı anda birden fazla talimatı farklı birimlerde yürütebilir. Örneğin bir toplama birimi ve bir çarpma birimi aynı anda çalışabilir.
- IPC (Instructions Per Cycle): Bir saat çevriminde kaç talimat yürütülür? Basit bir pipeline’da bu sayı en iyi ihtimalle 1’e yaklaşır; superscalar bir çekirdekte 1’in üstüne çıkabilir.
- Out-of-Order Execution: İşlemci, talimatların bağımsız olanlarını önce yürütüp bağımlı olanları bekletebilir. Bu, pipeline’ın boş kalmamasını sağlar.
Sıra Neden Bozulur?
Out-of-order execution modern işlemcinin en büyük hilesi ve — göreceğimiz gibi — Spectre’ın da altyapısı. O yüzden üç basamakta açalım.
Birinci basamak: beklemenin bulaşıcılığı. Bellekten veri bekleyen bir talimat düşün; cache miss olmuş, veri yüzlerce çevrim sonra gelecek. Sıralı bir işlemcide bu talimatın arkasındaki talimatlar da bekler. Oysa onların çoğunun bekleyen veriyle hiçbir alakası yoktur. Boş oturmalarının tek sebebi, kodda daha sonra yazılmış olmaları.
İkinci basamak: sahte bağımlılıklar. Sıra bozacaksan gerçek bağımlılıkları bilmen gerekir, ama register isimleri seni yanıltır:
mov eax, [mem1] ; birinci is
add eax, 5
mov eax, [mem2] ; ikinci is -- birinciyle hicbir ilgisi yok
add eax, 9Üçüncü satır ikinci satırı beklemek zorunda mı? Mantıken hayır; bunlar bambaşka iki hesap. Ama ikisi de eax adını kullanıyor. Bu bir veri bağımlılığı değil, isim bağımlılığıdır. Donanım bunu register renaming ile çözer: eax aslında bir isimdir ve çekirdeğin içinde onlarca fiziksel register vardır. İşlemci ikinci eax’e farklı bir fiziksel register verir, iki hesap birbirinden tamamen kopar ve aynı anda yürüyebilir.
Üçüncü basamak: sıra geri nasıl kuruluyor? Talimatlar sırasız yürütülür ama sıralı emekli edilir (retirement). Sonuçlar önce reorder buffer denen bir kuyrukta bekler; bir talimatın sonucu ancak kendinden önceki bütün talimatlar tamamlandığında dünyaya görünür olur. Programın gördüğü sonuç bu yüzden her zaman sıralıdır.
Bu üçüncü basamağı aklında tut: birazdan “mimari sonuçları geçici tutmak” dediğimde kastettiğim mekanizma tam olarak budur.
Çok Çekirdekli İşlemciler
Pipeline ve superscalar tek bir çekirdek (core) içinde paralellik sağlar. Ama fiziksel sınırlar — ısı ve güç tüketimi — tek bir çekirdeği sınırsız hızlandırmana izin vermez. Bu noktada sektörün bulduğu çözüm yön değiştirmek oldu: tek çekirdeği daha da hızlandırmak yerine, aynı çipin üzerine birden fazla bağımsız çekirdek koymak.
Modern bir masaüstü işlemcisinde sekiz, sunucu tarafında yüzü aşkın çekirdek görmen sıra dışı değil. Bu çekirdeklerin her biri kendi register setine, kendi L1 ve L2 cache’ine sahiptir; yani birbirlerinin defterine bakmadan, kendi programlarını kendi hızlarında yürütürler. Ortak kaldıkları yer L3’tür: 5. bölümde tanıştığın o büyük ve görece yavaş katman, artık aynı zamanda çekirdeklerin buluşma noktası.
Buradan doğal olarak bir soru çıkıyor ve cevabı bu kitabın en sinsi performans tuzaklarından birine bağlanıyor: iki çekirdek aynı veriye dokunursa ne olur? İkisi de o cache line’ı kendi L1’ine kopyalar; biri değeri değiştirdiğinde diğerinin elinde bayat bir kopya kalır. Donanımın bunu engellemek için yürüttüğü sessiz pazarlığa cache coherence denir ve 5. bölümde MESI protokolüyle birlikte ayrıntısını konuşmuştuk. Oradaki false sharing bölümünü atladıysan, paralel kod yazmadan önce dönüp okumanı öneririm: paralelleştirdiğin bir programın tek çekirdekliden yavaş çıkmasının en yaygın sebebi orada anlatılıyor.
Programlama dillerinde
std::thread(C++),Thread(Java),threading(Python) gibi yapılar bu çok çekirdekli yapıyı kullanmak içindir. Ama birden fazla çekirdek kullanmak, programın paralel çalışacak şekilde yazılmasını gerektirir.
Simultaneous Multithreading (SMT / Hyper-Threading)
Intel’in Hyper-Threading teknolojisi (genel adıyla SMT), tek bir fiziksel çekirdeğin içinde iki (veya daha fazla) mantıksal yürütme akışı çalıştırmasına olanak tanır.
Fikri anlamak için çekirdeği ikiye ayırmak gerekiyor. Hesabı yapan birimler — ALU, FPU, cache — tek takımdır ve paylaşılır. Buna karşılık durum bilgisi, yani register seti ve instruction pointer, çekirdeğin içinde ikişer kopya hâlinde tutulur; iki akış birbirinin defterine bakmaz.
Kazanç şuradan geliyor: bir akış bellekten veri beklerken (cache miss) o pahalı hesap birimleri boş oturacaktı. SMT, o boşluğu ikinci akışın işiyle doldurur. Yani SMT sana ikinci bir çekirdek vermez, birinci çekirdeğin boş anlarını satar.
SMT, tek thread’in performansını ikiye katlamaz ama genel verimliliği %10-30 artırabilir. Güvenlik açısından bazı bulut sağlayıcıları SMT’yi devre dışı bırakır (yan yana thread’ler arası bilgi sızıntısı riski).
SIMD: Tek Talimat, Çok Veri
SIMD (Single Instruction, Multiple Data), tek bir talimatın aynı anda birden fazla veri parçası üzerinde çalışmasını sağlar. Örneğin iki dizinin karşılıklı elemanlarını dörder dörder tek talimatta toplarsın: a[0]+b[0], a[1]+b[1], a[2]+b[2], a[3]+b[3] — dört ayrı toplama değil, bir tane toplama talimatı.
Bunu mümkün kılan şey, normal register’lardan çok daha geniş özel register’lar. Genişliği anlamlı kılan da içine kaç sayı sığdığı:
| Kuşak | Register genişliği | 32-bit float | 64-bit double | 8-bit tam sayı |
|---|---|---|---|---|
| SSE | 128 bit | 4 | 2 | 16 |
| AVX / AVX2 | 256 bit | 8 | 4 | 32 |
| AVX-512 | 512 bit | 16 | 8 | 64 |
Register’ın bu dilimlerinin her birine lane (şerit) denir. Sekiz lane’li bir toplama talimatı, sekiz ayrı toplamanın işini tek çevrimde bitirir.
SIMD’in sınırını da hemen söyleyeyim, çünkü çoğu hayal kırıklığı buradan çıkar: bütün lane’ler aynı talimatı yürütmek zorundadır. Lane’ler arasında if yazamazsın. “Şu elemanlar için topla, şunlar için çıkar” demek istiyorsan, ikisini de hesaplayıp sonucu bir maske ile seçmen gerekir — yani işin bir kısmını baştan çöpe atarsın. Verinin düzenli olmadığı yerde SIMD’in kazancı hızla erir.
Video işleme, yapay zeka, bilimsel hesaplama gibi alanlarda SIMD kritik öneme sahiptir. Derleyiciler kodu kendiliğinden vektörize edebilir (auto-vectorization), ama işi sonuna kadar sıkmak istiyorsan intrinsic fonksiyonlarla vektör talimatlarını elle yazarsın.
DerinleşmeDaha geniş SIMD neden her zaman daha hızlı değil?
AVX-512 daha geniş register’lar ve daha fazla paralellik vaat eder, ama bu verimlilik bedelsiz değildir. 512-bit işlemler çekirdek içindeki vektör birimlerini çok daha yoğun kullanır ve bu doğrudan ısı artışına yol açar.
Bunun iki ayrı sonucu var. Birincisi klasik termal throttling: çip ısınır, saat hızı düşer. İkincisi daha sinsi — bazı Intel nesilleri AVX-512 talimatını görür görmez, daha ısınmamışken bile, o çekirdeği (bazen tüm çekirdekleri) önceden tanımlı daha düşük bir frekans kademesine indirir. Yani cezayı ısındığın için değil, ısınma ihtimalin olduğu için ödersin. Bu yüzden bulut tarafında AVX-512’nin varlığı instance tipine göre değişir; aynı sağlayıcının iki farklı nesil makinesinde aynı kod farklı hızlarda koşabilir. “Daha fazla SIMD” her zaman “daha hızlı” anlamına gelmez; iş yüküne, soğutmaya ve güç bütçesine bağlıdır.
Özet
Peki, ne öğrendik?
- Saat hızı tek başına artık artmıyor; kazanç aynı anda daha fazla iş yapmaktan geliyor.
- Pipeline, talimatları aşamalara böler ve her aşamada farklı bir talimat ilerletir.
- Superscalar yürütme aynı çevrimde birden fazla talimat başlatır; out-of-order yürütme ise bekleyen bir talimatın arkasındaki bağımsız işi öne alır.
- Talimatlar sırasız yürütülür ama sıralı emekli edilir. Programın gördüğü sonuç bu yüzden her zaman sıralıdır.
- Çok çekirdek ayrı işlemciler, SMT tek çekirdeğin boş anlarının satılması, SIMD ise tek talimatla çok veri demektir. Üçü birbirinden bağımsız üç paralellik türüdür.
Bu hilelerin hepsi sessiz bir varsayıma dayanıyor: CPU, sıradaki talimatın hangisi olduğunu biliyor. Peki kodda bir if varsa? Sıradaki bölümün konusu, işlemcinin bu soruya verdiği cevap — ve o cevabın kırk yıl sonra açtığı devasa güvenlik deliği.