C düşük seviyeli bir dil değildir (2018)
(queue.acm.org)- C, PDP-11 döneminde donanım soyutlamasıyla iyi eşleşiyordu, ancak modern CPU'larda C'nin soyut makinesi olan sıralı yürütme ve düz bellek gerçek donanımdan büyük ölçüde sapıyor
- Spectre ve Meltdown, işlemcilerin C'nin sıralı modelini hızlı çalıştırmak için dal tahmini, spekülatif yürütme ve komut düzeyinde paralellik mekanizmalarına yoğun biçimde dayanmasının sonucu ile bağlantılı
- C kodunu hızlı hale getirmek için basit bir makine kodu çevirisi yetmez; LLVM/Clang ölçeğinde karmaşık optimizasyonlar gerekir ve bazı optimizasyonlar C anlambilimiyle çatışabilir
- Pointer kökeni, struct padding, başlatılmamış değerler ve signed integer overflow gibi kurallar yürütme sonuçlarını öngörmeyi zorlaştırır ve güvenlik açıklarına da yol açabilir
- Modern donanıma daha iyi uyan model; çok sayıda thread, geniş vektör birimleri ve basit bir bellek modeli kullanır, ancak mevcut C kodu uyumluluğu en büyük kısıt olarak kalır
C'nin “düşük seviyeli” gibi görünmesinin nedeni
- Bir dil düşük seviyeliyse, donanımın sunduğu soyutlamalar ile dilin soyut makinesi kolayca eşleşebilmelidir
- C, PDP-11 üzerinde düşük seviyeli bir dil sayılabilirdi
- Programlar sıralı olarak yürütülürdü
- Bellek düz bir alan gibi ele alınırdı
- pre-increment ve post-increment işleçleri PDP-11'in adresleme biçimiyle iyi örtüşüyordu
- Alan Perlis, “Bir programın ilgisiz şeylere dikkat etmek zorunda kaldığı dil düşük seviyelidir” diye tanımlamıştı; ancak bu tanım tek başına insanların düşük seviyeli dilden beklediği “donanıma yakınlık” fikrini tam açıklamaya yetmiyor
Modern CPU'lar hızlı PDP-11 emülatörleri gibi çalışıyor
- Spectre ve Meltdown'ın temel nedeni yalnızca hızlı işlemciler üretmek değil, PDP-11 benzeri bir soyut makineyi hızlı biçimde görünür kılmaya çalışan işlemci tasarımıyla da ilgili
- C kodu, C11 öncesinde standart dışı üretici uzantıları dışında fiilen tamamen sıralı bir makine sunuyordu; C11 sonrasında da büyük ölçüde sıralı soyut makine korunuyor
- Modern CPU'lar yürütme birimlerini sürekli meşgul tutmak için komut düzeyinde paralellik (ILP) çıkarır
- Bitişik işlemleri inceler ve bağımsız işlemleri paralel olarak yayınlar
- Programcının çoğunlukla sıralı kod yazmasına izin verirken karmaşıklığı ve güç tüketimini artırır
- GPU'lar bu tür mantık olmadan da yüksek performans verebilir, ancak açık paralel programlar gerektirir
Spectre, Meltdown ve spekülatif yürütmenin maliyeti
- Modern Intel işlemcileri aynı anda en fazla 180 komutu uçuş hâlinde tutabilir
- C kodunda ortalama olarak yaklaşık her 7 komutta bir dal bulunduğu söylenebilir
- Tek bir thread'de pipeline'ı doldurmak için sonraki 25 dalın hedefini tahmin etmek gerekir
- Yanlış tahminler, yapıldıktan sonra atılan işler üretir ve gücü de boşa harcar
- Spectre ve Meltdown, böyle atılmış işlerin gözlemlenebilir yan etkilerini side channel olarak kötüye kullanabildi
- Modern yüksek performanslı çekirdeklerde register rename engine, kalıp alanı ve gücün büyük tüketicilerinden biridir
- Komutlar yürütülürken bu birimleri kapatmak veya güçlerini kesmek zordur
- GPU'larda böyle birim yoktur; paralellik çok sayıda thread'den gelir
C'nin düz bellek modeli, cache gerçeğiyle uyuşmuyor
- C soyut makinesinin çekirdeğindeki düz bellek, 20 yılı aşkın süredir gerçek donanımla uyumlu değil
- Modern işlemcilerde register'lar ile ana bellek arasında genellikle 3 seviyeli cache bulunur
- Cache, adı üstünde, programcıdan gizlenir ve C'de görünmez
- Modern işlemcilerde hızlı kod üretmek için cache'in verimli kullanılması gerekir
- C programcısı, performans için yalnızca soyut makineyi değil, gerçekleştirim ayrıntılarını da bilmek zorundadır
- Örneğin 64 bayta hizalanmış iki değer aynı cache line içinde yer alabilir
C kodunu hızlandırmak için gereken derleyici karmaşıklığı
- Düşük seviyeli bir dil, karmaşık derleyiciler olmadan da hızlı koda kolay çevrilebilmelidir; ancak C böyle değildir
- Clang ve ilişkili LLVM bölümleri yaklaşık 2 milyon satır büyüklüğündedir
- C'yi hızlı çalıştırmak için gerekli analiz ve dönüşüm pass'leri sayıldığında bile yorumlar ve boş satırlar hariç neredeyse 200 bin satıra ulaşır
- C'de büyük miktarda veriyi işlerken genellikle her öğeyi sıralı işleyen döngüler yazılır
- Modern CPU'da bunu en iyi şekilde çalıştırmak için derleyicinin önce döngü yinelemeleri arasındaki bağımsızlığı saptaması gerekir
restrictanahtar sözcüğü, bir pointer üzerinden yapılan yazmanın başka bir pointer üzerinden yapılan okumayla çakışmayacağını garanti edebilir
- Fortran, bu tür bilgi sağlama açısından C'ye göre avantajlıdır; C'nin yüksek başarımlı hesaplamada Fortran'ın yerini alamamasının başlıca nedenlerinden biri de budur
Vektörleştirme ile C'nin bellek yerleşimi garantilerinin çatışması
- Döngü yinelemeleri bağımsızsa, derleyici sonucu vektörleştirmeye çalışır
- Modern işlemciler, skaler koda kıyasla vektör kodunda 4 ila 8 kat throughput elde edebilir
- Bu tür işlemciler için düşük seviyeli bir dilde, keyfi uzunlukta yerel vektör tiplerinin bulunması doğal olurdu
- LLVM IR böyle bir model sunar; çünkü büyük vektör işlemlerini küçük işlemlere bölmek, tersinden daha kolaydır
- C'nin bellek yerleşimi garantileri optimizasyonla çatışır
- Aynı prefix'e sahip struct'lar birbirinin yerine kullanılabilir
- Struct alanı ofsetleri dilde görünür durumdadır
- Derleyici, vektörleştirmeyi iyileştirmek için alan sırasını değiştirmekte veya padding eklemekte zorlanır
- Veri yapısı yerleşimini ayrıntılı denetleyebilme özelliği düşük seviyeli dilin avantajı olabilir, ancak aynı zamanda C'yi hızlı hale getirmeyi zorlaştırır
Padding, SROA ve loop unswitching sorunları
- C, diziler içinde padding olmayacağı garantisi için struct sonuna padding eklenmesini gerektirir
- Struct'lar
memcmpgibi türden bağımsız karşılaştırmalara uygun olmalıdır; bu yüzden struct kopyası padding'i de korumalıdır- Bazı deneylerde belirli iş yüklerinde toplam çalışma süresinin kayda değer bir bölümü padding kopyalamaya harcanmıştır
- SROA, struct ve sabit uzunluklu dizileri ayrı değişkenlerle değiştirmeye çalışan bir optimizasyondur
- Erişimleri bağımsız ele almayı sağlar ve sonucu gözlemlenmeyen işlemleri kaldırabilir
- Bazı durumlarda padding'i ortadan kaldırır, ama her zaman değil
- loop unswitching, koşullu bir döngüdeki koşulu döngünün dışına çıkarıp iki yol için ayrı döngüler oluşturan optimizasyondur
- Düşük seviyeli bir dilde yazılan kod çalışırken programcının hangi kodun ne zaman yürütüleceğini bildiği varsayımıyla çatışır
- C'nin unspecified value ve undefined behavior kavramlarıyla da sorun çıkarabilir
Başlatılmamış değerler ve tanımsız davranış
- C'de başlatılmamış bir değişken okunursa unspecified value elde edilir ve her okumada farklı bir değer alınabilir
- Bu kural, sayfaların gecikmeli geri kazanımı gibi davranışlara izin verir
- FreeBSD'nin
mallocgerçekleştiriminde şu anda kullanılmayan sayfalar işletim sistemine bildirilir - İşletim sistemi, bir sayfaya yapılan ilk yazmayı o sayfanın yeniden gerektiğine dair ipucu olarak kullanabilir
- FreeBSD'nin
- unspecified value akış denetiminde kullanılırsa undefined behavior oluşur
- Örneğin
ifkoşulunda başlatılmamış bir değerin kullanılması buna girer
- Örneğin
- loop unswitching'de döngünün 0 kez çalıştığı durumda özgün kodda tüm döngü gövdesi ölü koddur
- unswitching sonrasında, başlatılmamış olabilecek bir değişkenle dallanma yapılabilir
- Böylece ölü kod tanımsız davranışa dönüşmüş olur
- C kodu hızlı hale getirilebilir, ancak yeterince akıllı bir derleyici oluşturmak binlerce insan-yılı gerektirir ve bazen dil kurallarının bir kısmını ihlal etmek zorunda kalır
C'yi anlamanın zorlaşmasının nedeni
- Düşük seviyeli bir dilde programcı, soyut makine ile gerçek fiziksel makine arasındaki eşleşmeyi kolayca anlayabilmelidir
- PDP-11'de C ifadeleri bir veya iki komuta kolayca eşleniyordu; yerel değişkenler ve ilkel türler de donanımla basit biçimde örtüşüyordu
- Sonrasında C gerçekleştirimleri, hızlı kod ve donanımla eşleşme yanılsamasını korumak için giderek daha karmaşık hâle geldi
- 2015'te C programcıları, derleyici yazarları ve standart komitesi üyeleriyle yapılan bir ankette C'nin anlaşılabilirlik sorunu ortaya çıktı
- Bir struct sıfırla başlatıldıktan sonra bazı alanlar ayarlanırsa, padding bitlerinin tümünün sıfır olup olmadığı konusunda %36'sı emin olduğunu, %29'u ise bilmediğini söyledi
- Gerçek sonuç, derleyiciye ve optimizasyon düzeyine göre değişebilir
Pointer kökeni ve güvenlik açıkları
- BCPL modeli, değerlerin sözcüklerden oluştuğu ve her sözcüğün ya veri ya da verinin adresi olduğu görece basit bir modeldi
- C modeli, segment mimarileri veya garbage collection sanal makineleri dahil çok çeşitli hedeflerde uygulanmak üzere tasarlandı
- C standardı, bu sistemlerde sorunları önlemek için pointer üzerindeki geçerli işlemleri kısıtlar
- C Defect Report 260, pointer tanımına pointer provenance kavramını ekledi
- Gerçekleştirim, bit deseninin kökenini izleyebilir
- Bit düzeyinde aynı olsalar bile farklı kökene sahip pointer'ları ayırt edebilir
provenancesözcüğü C11 belirtiminde geçmez; bu nedenle anlamını derleyici yazarlarının belirlemesi gerekir- GCC ve Clang, bir pointer'ın tamsayıya dönüştürülüp yeniden pointer'a çevrilmesi durumunda provenance'ın korunup korunmadığı konusunda farklı davranır
- signed integer overflow ve null check öncesi pointer dereference içeren kodlarda güvenlik açıklarının gözlendiği örnekler vardır
- null pointer dereference, C'de undefined behavior olduğundan derleyici zaten dereference edilmiş bir pointer'ın null olamayacağını varsayabilir
- Örnek olarak CVE-2009-1897 verilebilir
C olmayan işlemcileri hayal etmek
- Spectre ve Meltdown için önerilen düzeltmeler ciddi performans cezaları getiriyor ve son 10 yıldaki mikro mimari ilerlemenin önemli bir kısmını geri alıyor
- C kodunu hızlandırmak yerine, hızlı işlemcilere uygun programlama modelini yeniden düşünmenin zamanı gelmiş olabilir
- Sun/Oracle UltraSPARC Tx gibi yüksek derecede çok iş parçacıklı çipler, yürütme birimlerini doldurmak için çok fazla cache'e ihtiyaç duymaz
- Yeterli yüksek seviyeli paralellik varsa, bellek bekleyen thread durdurulup yürütme birimleri başka bir thread'in komutlarıyla doldurulabilir
- Sorun, C programlarının genellikle az sayıda meşgul thread'e sahip olma eğilimidir
- ARM SVE (Scalar Vector Extensions), program ile donanım arasında daha iyi bir arayüz örneğidir
- Mevcut vektör birimleri sabit boyutlu vektör işlemleri sunar ve derleyicinin algoritmaları bu boyuta uyarlaması beklenir
- SVE'de programcı kullanılabilir paralellik derecesini ifade eder, donanım da bunu yürütme birimi sayısına göre eşler
- C'de autovectorizer, döngü yapısından paralelliği çıkarsamak zorunda olduğu için karmaşıktır; ancak işlevsel tarzda
mapişleminde eşlenecek dizinin uzunluğu doğrudan kullanılabilir paralelliği verdiğinden kod üretimi basitleşir
Daha basit bellek modeli ve paralel programlama
- Modern CPU'larda cache coherency protocol, hızlı ve doğru biçimde yapılması en zor bölümlerden biridir
- Karmaşıklığın önemli bir kısmı, verinin paylaşılıp değiştirilebilir olmasını bekleyen dilleri desteklemekten gelir
- Erlang tarzı soyut makinede tüm nesneler ya thread-local ya da immutable'dır
- Erlang, thread başına yalnızca bir değiştirilebilir nesne bulunan daha basit bir modele sahiptir
- Bu tür sistemlerde cache tutarlılık protokolü mutable veya shared olmak üzere iki duruma ayrılabilir
- Immutable nesneler cache'i basitleştirir ve birçok işlemi daha ucuz hâle getirebilir
- Sun Labs'ın Project Maxwell'i, cache içindeki nesneler ile young generation'a ayrılan nesnelerin neredeyse aynı küme olduğuna dikkat çekti
- Bir nesne cache'ten atılmadan önce ölüyorsa, ana belleğe geri yazılmaması güç tasarrufu sağlayabilir
- Heap'te immutable nesneler ve değiştirilebilir stack kullanılırsa garbage collector, donanımda uygulanması kolay basit bir durum makinesine dönüşebilir
- Salt hız için tasarlanmış işlemciler büyük olasılıkla çok sayıda thread, geniş vektör birimleri ve daha basit bellek modellerini destekleyecektir
- Böyle sistemlerde C kodunu çalıştırmak sorun olabilir
- Dünyada çok fazla legacy C kodu bulunduğu için ticari olarak başarılı olmak zor olacaktır
- Paralel programlamanın zor olduğu yönündeki yaygın kanı, daha çok C benzeri soyut makinelere sahip dillerdeki paralel programlama için geçerlidir
- Alan Kay, actor-model dillerini çocuklara öğretti ve onlar 200'den fazla thread içeren çalışan programlar yazdı
- Erlang programcıları binlerce paralel bileşen içeren programları yaygın olarak yazar
- Çok çekirdekli CPU'lar ve many-core GPU'lar yaygın kullanılırken, C modern donanıma iyi eşlenmiyor
1 yorum
Hacker News yorumları
C’nin düşük seviyeli olmasının nedeni en azından elle bellek yönetimidir
Özellikle modern donanımda bellek yönetimi programlamanın merkezindedir. Rust’ın çöp toplayıcı olmadan bellek güvenliğini öne çıkarması da nihayetinde Rust’ın varlık nedeninin büyük ölçüde bellek yönetimine yakın olmasından kaynaklanır. C’nin hızlı olmasının nedeni de bellektir; C’nin güvensiz olmasının nedeni de çoğunlukla bellektir. Paralel hesaplamanın zor olmasının büyük nedenlerinden biri de eşzamanlı bellek erişimidir. Fonksiyonel programlama çoğu zaman matematiksel kavramlarla çevrelenmiş görünür, ama önemli bir kısmı nesneleri değişmezmiş gibi göstermeye ve içeride derleyicinin değişebilir belleği yönetmesine dayanır
C’de bir ayırıcı kullanıyorsanız bu çağrıların hepsi açıktır. Eski C++’taki
new/deleteve ham işaretçiler ayırıcıyı açıkça çağırır, ama yıkıcılarda otomatik gerçekleşen çok şey de vardır. Modern C++’ın akıllı işaretçileri, ayırma ve serbest bırakmanın ikisinin de otomatik gerçekleşmesi bakımından özünde çöp toplamalı dillere benzerHangi verinin hangi önbellek katmanına konacağını, neyin sanal belleğe gönderileceğini vb. işlemciye söyleyemezsiniz. Python’a göre daha düşük seviyelidir, ama PDP-11 dönemi C gibi düşük seviyeli bellek yönetimi saymak zordur
Mikrodenetleyici sınıfı sistemlerde veya MMU’suz sistemlerde durum değişir, ama bu ayrı bir meseledir
Bir Rust geliştiricisi olarak ben bile işaretçileri bellek adresleri gibi gerçek fiziksel nesneler sanma yanılsaması içinde çalışıyorum. Rust ve bir ölçüde C++ referanslar ve ödünç alma gibi yönetim soyutlamalarını öne çıkarır, ama temel kavram aynı kalır
Gerçekte işletim sistemi çekirdeği fiziksel bellek ile program arasına devasa bir katman koyar; “adres” ve “işaretçi” OS ile MMU’nun türlü işlemler yaptığı tutamaçlara daha yakındır
“Ham işaretçi” de aslında ham değildir. Bir sayfa içindeki ofsete yönelik bir tutamaktır; gerçek sayfalar oraya buraya dağılmış olabilir. libc ve C modelinden tamamen ayrılıp VM alt sisteminin sayfalarıyla doğrudan etkileşen saf referanslar, bir tür “nesne tutamaçları” dünyasına gidilirse, bu gerçek alt sistem davranışına daha da yakın olabilir
mallocvefreekütüphane fonksiyonlarıdırDonanımda bu tür bayt düzeyinde ayırma yoktur; dolayısıyla donanım üzerindeki bir soyutlama olmanın yanı sıra işletim sisteminin belleği ayırma biçimini de soyutlar
C’de yığına da doğrudan erişemezsiniz. Yığın çerçevesi soyutlanmıştır; kullanabileceğiniz şey en fazla
longjmpgibi bir şeydirTanımsız davranış ve katı aliasing kurallarını da hesaba katarsanız, belleği kafanıza göre kurcalama erişiminiz de pek fazla değildir
Bir C programcısı ve derleyici yazarı olarak, C’yi anlayıp profesyonelce kullanan biri için C kesinlikle düşük seviyeli bir dildir.
Düşük seviyeli bir dil arıyorsanız C ve akrabaları en iyi seçeneklerdir.
C’yi yeni öğreniyor ve onu uzman gibi nasıl kullanacağınızı bilmek istiyorsanız bu yazıyı görmezden gelmeniz daha iyi olur. Yalnızca kafa karıştırabilir ve C’yi etkili kullanma becerinizi azaltabilir.
Gerçek makinelerin epey zahmetle emüle etmek zorunda kaldığı bir makineye düşük seviyeli erişim sunar. Gerçek makineye erişmek için yıllar içinde eklenen sallantılı düzenekler ve yamalar, C içinde nispeten yabancı unsurlardır.
Yine de başlığın retorik olarak sert olduğu konusunda hemfikirim. Yanlış düşük seviyeli bir dil olması onu yüksek seviyeli bir dil yapmaz. WASM de modern donanıma doğrudan karşılık geldiğini iddia ederse “yanlış” olur, ama bu onu yüksek seviyeli yapmaz.
C’nin kötü bir eşleme olması kendi başına sinir bozucu değil. 1970’lerin dili olduğu için bu anlaşılır ve bugün de pek çok durumda kesinlikle yararlıdır. Daha sinir bozucu olan, C’nin hâlâ dil tasarımını büyük ölçüde yönlendirmesi ve dil tasarımcılarının donanıma bakışını güçlü biçimde renklendirmesi. Bu yüzden modern dil tasarımı, donanımla iyi eşleşen diller yapmak yerine çoğu zaman C parçalarını yeniden karıştırmakla sınırlı kalıyor.
Yazdığınız kodun assembly ile bire bir ilişkiye sahip olacağını düşünürseniz sorun yaşarsınız. Bunun insanı nasıl tökezletebildiğini daha derin görmek isterseniz https://youtu.be/w3_e9vZj7D8 izlenebilir.
Yazarın ana derdi muhtemelen “C sistem programlama için iyi bir dil değildir” değildir. Haskell’de
volatile int *dma_register = SCATTER_GATHER_BASE;gibi bir şeyi eşdeğer biçimde yazmak zordur.Yazarın söylemek istediği, C ve diğer “von Neumann makinesini modelleyen” dilleri hızlı çalıştırma motivasyonunun derleyicileri çok karmaşık hâle getirdiği; yazarın da “düşük seviyeliyse basit bir derleyici gerekir” imasında bulunduğudur. Böyle kodları hızlı çalıştırmak için yapılan işlemciler de çok karmaşıktır ve bu karmaşıklığın bir maliyeti vardır.
Birçok açıdan bu, programlama modeli değişimi çağrısı yapan bir yazı; GPU’yu da “yeni programlama modeli” ile “onu destekleyen silikon” birlikte yaratıldığında ortaya çıkabilecek potansiyelin örneği olarak veriyor.
Asıl anlamı, yazıda kullanılan anlama daha yakın. Düşük seviyeli dil taşınabilir değildir, üzerinde çalıştığı donanıma bağlıdır; yüksek seviyeli dil ise birden fazla platformu hedefleyebilir. Bu tanıma göre C açıkça yüksek seviyeli bir dildir.
Yazarın kelime oyunu yaptığından çok, eski bir terime tutunmanın tam tersine anlayışı bulanıklaştırmasından şikâyetçiyim. “Nesil” sınıflandırması genelde daha açıklayıcıdır.
Bu yaklaşımın iyi yanı, dilleri ne zaman kullandığınıza dair nispeten net farklara göre ayırmasıdır. Sonra “yüksek seviye/düşük seviye” göreli terimler olarak kullanılabilir. Daha yüksek seviyeli diller, bilgisayarın gerçekte ne yaptığının ayrıntılarını daha fazla soyutlama eğilimindedir. Böyle yapınca da daha yüksek nesil dillerin genel olarak daha yüksek seviyeli olduğu gerçeği korunur; kaybolan tek şey ise tamamen keyfî ve açıkçası işe yaramaz bir sınır çizgisi etrafında dönen aptalca tartışmadır.
Bu yaklaşımla .NET IL, WebAssembly ve Java bytecode’u çok yüksek seviyeli 2. nesil diller olarak görmek de mümkün; bu da ilginç. Forth ise 3. nesil bir dildir. Chuck istersen hodri meydan.
Çekicin nasıl kullanılacağından ziyade, çekici her yerde kullanma biçiminin, yani C tasarımının bizi sınırlayıp sınırlamadığını sorgulayan bir yazıya daha yakın.
CPU komut kümesinin CPU implementasyonunu daha fazla açığa çıkarması gerektiği yönündeki yazarın iddiasına katılmıyorum.
Geçmişte de denendi ve uzun vadede başarısız oldu. Örnek olarak 80’lerin sonu ve 90’ların başında tasarlanan bazı RISC işlemcilerde, örneğin MIPS ve SuperH’deki branch delay slot verilebilir. Kavramı bilmeyenlere açıklarsak: dallanma komutundan sonraki komut, dallanmanın alınıp alınmadığına bakılmaksızın çalıştırılır.
Kısa vadede, dallanmadan sonra pipeline’ın durmasını önleme işini programcıya bırakarak işlemciyi daha basit ve ucuz yapmak mümkün oldu. Ama zamanla işlemci tasarımı ve pipeline’lar daha karmaşık hâle geldi; tek bir komut artık dallanma gecikmesini örtmeye yetmedi. Sonuçta bu, uyumluluk nedeniyle gelecekteki işlemcilerin uğraşmak zorunda kaldığı bir mirasa dönüştü ve branch prediction ile pipeline mantığını daha karmaşık hâle getirdi.
Yanlış ayrıntıları açığa çıkarmak elbette kötüdür. Söylediği şey, modern CPU dünyasında C modelinin ciddi sınırlamaları olduğudur.
Bir işlemcideki böyle bir alt sistemi geliştiricilerin kullandığı bir sunum dinlemiştim. Kullanmazlarsa zaman penceresinin %95’ini yalnızca veri kopyalamaya harcıyorlarmış; ama o motorla veriyi önceden istediklerinde veri edinmeye zaman penceresinin yalnızca %10’unu harcayıp, istenen işi toplam zaman penceresinin yaklaşık %50’si içinde bitiriyor ve ek özellikler ile iyileştirmeler için bolca zaman bırakıyorlarmış.
x86’da böyle bir özellik olsaydı doktora dönemimde erişeceğim matris verilerini önceden istemek için kullanırdım. Kullandığım desen doğrusal değil ama iyi tanımlı. Şu anda o kodu daha da hızlandırmak için matrisi prefetcher’ın seveceği şekilde yeniden düzenlemem ve tüm kod tabanını baştan sona refactor etmem gerekiyor.
İsterseniz berbat tasarlayabilirsiniz elbette, ama genelleme yapmaya yetecek kadar tarihsel örnek olup olmadığını merak ediyorum.
Düşük seviyeden yüksek seviyeye geçişin bir ikilik değil, bir spektrum olduğunu düşünüyorum
C’nin diller arasında alt üçte birlik dilime girdiği söylenebilir; bellek ve iş parçacığı yönetimi gibi birçok makine ilkel unsurunu kullanıcıya açar. Assembly kadar düşük seviyede olmasa da Java veya Go’dan daha düşük seviyededir; Python ya da JavaScript tarafıyla arasında ise kesinlikle mesafe vardır
Üstelik C, segmentli bellek veya düz olmayan adresler kullanan platformlara pek uygun değildir. Bunların yeniden moda olabileceğine dair işaretler var; C’nin yaygın kullanımı ise buna gerçekten büyük bir engel oluşturuyor
Bu yüzden zihnimdeki model hep “C, işlemciye doğrudan komut vermeden önce inilebilecek en düşük seviye” oldu
“C, tipik bir ‘yüksek seviyeli’ dil gibi davranmaz. Çünkü daha çok assembly dili gibi ‘düşük seviyeli’ dillerle ilişkilendirilen çeşitli özellikler sunar. Belirli bellek adreslerine veri yazıp okuyabilme, bellek konumlarının içeriği üzerinde işlem yapabilme, tamsayı değişkenlerini artırıp azaltan komutlar buna dahildir … Dolayısıyla C, programcıya düşük seviyede çalışmanın esnekliğini ve verimliliğini sağlarken, günümüz bilgisayar dillerinde tipik olan daha gelişmiş veri yapıları ve program akışı denetimi gibi yüksek seviyeli işlemlerin avantajlarını da sunar. Bu nedenle C bazen ‘yüksek seviyeli düşük seviyeli dil’ veya ‘düşük seviyeli yüksek seviyeli dil’ olarak tanımlanır.” - https://archive.org/details/computerprogramm0000ford/page/13...
Yazının sonundaki “yazılım geliştirmede paralel programlamanın zor olduğuna dair yaygın bir mit vardır” cümlesi yanıltıcı
Yazar, zor olmadığı belirli bir durum sunsa da genel olarak uygulanabilecek bir soruysa paralel programlama zordur ve bu yaygın bir mit değildir
Paralel programlama zor mu? Daha ayrıntılı koşullar olmadan sorulursa evet. Kod komutlarının sırayla tek tek çalıştırılmasındansa aynı anda çalıştırılmasını kavramsallaştırmak çok daha zordur
(map inc [0 1 2 3])programlanırken,incfonksiyonunun her öğe için sırayla çalışmasıyla paralel çalışmasını kavramsallaştırmanın zorluğu gerçekten farklı mı?Paralel programlamanın zorluğunun doğuştan gelen bir şeyden çok iki şeye benzediğini düşünüyorum
Birincisi, diller genellikle varsayılan olarak sıralı yürütmeyi temel alır; bu yüzden asenkron yapmak için programcıya ek ilkel unsurlar sunmak gerekir
İkincisi, paralel programlamanın ne zaman etkili biçimde kullanılacağını bilmek gerekir
Yalnızca bağımsız hesaplamalar gerektiren bağımsız öğelerden oluşan bir liste veya akış varsa paralel programlama sezgiseldir
İnsanların takıldığı nokta, asenkronluğun gereksiz olduğu yerlere, yani sıralı yürütmeyle aynı performansı verdiği ya da daha kötü olduğu yerlere bunu zorla sokmaları veya hesaplamalar gerçekte birbirine bağımlıyken asenkronluk ekleyip davranışı bozmalarıdır
“Ek ayrıntı ya da somutluk yoksa” dediğinizde aslında temel çerçeve olarak C/C ailesi dünya görüşünü kullanıyorsunuz
Yazarın derdi, sıralı programlamanın basit programlamanın yalnızca bir türü olduğu, tek tür olmadığı ve modern donanıma kolayca uymadığı
Erlang’ın var olması ve insanların onu başarıyla kullanması, daha zor olan şeyin zor olmadığı anlamına gelmez
Süreçler veya iş parçacıkları gibi eşzamanlılık programlama altyapısıyla paralel algoritmalar uygulamak da zordur. Ama paralel programlama, yani çok sayıda işlem öğesinin aynı işi birlikte yapmasını sağlamak, doğru soyutlama varsa çok daha kolaydır
Ancak matris çarpımı gibi bazı kullanım senaryoları bunun istisnasıdır
Bilgisayarın hızlı bir PDP-11 olmadığı konusunda bu yazı haklı; ama bunun C ile ilgili olduğu konusunda haksız
Örneğin şöyle bir cümle var: “C soyut makine bellek modelinin bir başka temeli olan düz bellek. Bu, 20 yıldan uzun süredir doğru değil”
Bunun C ile ilgisi yok. Donanım bu soyutlamayı zorunlu kılıyor. Ve iyi ki de öyle. Aksi halde programlar, farklı önbelleklere sahip bir makineye taşındığında takılıp kalırdı
Düz RAM’miş gibi davranan hiyerarşik bellek yapısı, komut kümesinin ima ettiğinden çok daha büyük CPU’lar ve sıra dışı/spekülatif yürütme, yazılan program ile gerçek yürütmeyi daha da ayıran optimize edici derleyiciler buna örnektir
IBM, C yükselişe geçmeden çok önce, 1970’lerde bunlar üzerinde çalışıyordu. Bu modeli eleştirmek ve alternatifler aramak makul; ama C’yi suçlamak adil değil
Bu yazı artık 5 yıl öncesine ait; bilgisayarların yapısal olarak PDP-11'e pek benzemediği varsayımı ise aksine daha da doğru hâle geldi. Ancak “C olmayan bir işlemci hayal edelim” sonucu artık daha zayıf görünüyor.
Doğrusal kod ile son derece paralel kod arasında güçlü bir ayrım görüyoruz; 2018'de de bu zaten böyleydi. Bunun en belirgin örneği, makine öğrenimi ve bilimsel hesaplamada Python'ın yükselişi. Performans birinci öncelik olmadığında tek iş parçacıklı tarzda ve düz bellek modeliyle yazmak hâlâ çok kullanışlı.
Performans önem kazandığında, paralel programlamaya daha uygun bir dile geçmek mantıklı. Pytorch gibi şeylerin hesaplama grafiği dili, CUDA üzerindeki farklı ilkel öğe kümeleri ya da Futhark gibi daha deneysel diller buna giriyor. Performans açısından kritik kodlar her zaman alana özgü diller getirdi; bunlar azalmak yerine daha yaygınlaşıyor gibi. Donanım da buna uygun şekilde yapılıyor. Masaüstü PC'lerde yaygın CPU+GPU birleşimi, fiilen kendi DSL'ini oluşturan ilkel öğelere sahip x86 vektör uzantıları, GPU'yu CPU'ya bağlayıp ikisinin de aynı sistem belleğine yüksek hızda erişmesini sağlayan M1 gibi şeyler buna örnek.
Başka bir deyişle, gerçekten eskimiş olan C değil, her tür işe aynı derecede iyi uyan genel amaçlı dil kavramı olabilir.
Modern CPU'ların karmaşıklığı yüzünden C artık “düşük seviyeli” bir dil değilse, aynı mantık assembly dili için de geçerli.
Çünkü sıra dışı yürütme ve register yeniden adlandırma gibi şeyler assembly için de geçerli.
Son birkaç on yılda derleyicilerin karmaşıklaşması da bu iddiayı güçlendiriyor. Bir C derleyicisinin ürettiği assembly, yani nesne kodu bile döngü dışına taşıma, ortak alt ifade eliminasyonu vb. nedeniyle beklenenden farklı çıkabilir.
Yine de C'ye “düşük seviyeli” dil demenin hâlâ kullanışlı bir etiket olduğunu düşünüyorum. Değilse, bu adlandırmanın kendisini emekliye ayırmak gerekir.
Gerçek bilgisayar üzerindeki bir soyutlama olduğu doğru, ama C'nin sanal bilgisayar modeli üzerine inşa ettiğinden çok daha az. Günümüz assembly'si, C'nin yaratıldığı zamandaki C ile benzer seviyede. Günümüz C'si ise fazla yüksek seviyeli; daha iyi ve modern dillerle elde edilemeyecek bir özellik sunmuyor.
Ancak bugün “düşük seviyeli” ve “yüksek seviyeli” adlarının pek kullanışlı olmadığı konusunda hemfikirim.
Yazı, birbirleriyle bağdaştırılması zor iki ayrı argüman hattı geliştiriyor gibi.
İlki, C'nin düşük seviyeli bir dil olmadığı iddiası; buna örnek olarak struct padding ve işaretli tamsayı taşmasının tanımsız davranış olmasını veriyor. Bu kısmı anlıyorum ve varsayımsal “gerçekten düşük seviyeli” bir dil için dil özellikleri önermesi bakımından yapıcı görünüyor.
İkincisi, C'nin hâkimiyeti nedeniyle CPU tasarımcılarının C'yi doğal biçimde çalıştıran bir şeyler yapmak için kendilerini zorlamak zorunda kaldığı iddiası. Burada register yeniden adlandırma, düz bellek, önbellekleme gibi örnekler var. Bu iddiayı da anlıyorum, ama ilk iddiayla ve yazı başlığının bağlamıyla nasıl bağlandığından emin değilim. Harfi harfine alınırsa, modern donanımda düşük seviyeli bir dil yapmanın kendisi imkânsız ve makine dili bile “yüksek seviyeli” demekmiş gibi görünüyor. O zaman sonuç, önce komut kümesi mimarisine çok daha fazla karmaşıklığı açığa çıkaran yeni nesil donanımlar yapmak, ancak ondan sonra bunlardan yararlanan düşük seviyeli bir dil tasarlamak olur.
İki argüman da değerli, ama bunları tek bir yazıda bir araya getirip başlığı “C düşük seviyeli bir dil değildir” koymak biraz dengesiz olmuş. İlk argüman bu başlığa uyuyor; ikincisi ise “Makine dili de düşük seviyeli bir dil değildir” başlıklı bir devam yazısında ele alınsa daha iyi olurdu.
Ama derleme süresinin uzun olduğunu ve derleyicinin beklenen optimizasyon düzeyine sonunda ulaşamadığını duydum. x86 ile uyumlu olmaması da benimsenmesine yardımcı olmadı.
Aklıma VLIW geliyor. Wikipedia’daki Itanium maddesine göre şöyle:
“Tek bir VLIW komut sözcüğü, bağımsızlık değerlendirmesi yapılmadan paralel yürütülebilecek birden fazla bağımsız komut içerebilir. Derleyici, aynı anda yürütülebilecek geçerli komut kombinasyonlarını bulmaya çalışmalıdır; fiilen, mevcut süperskalar işlemcilerin çalışma zamanında donanımla yaptığı komut zamanlamasını yapar.”
CPU tek akış paralelliğini arayüzünde dışa açıyorsa, bu derleme zamanında ele alınabilir ya da inline assembly ile doğrudan kararlaştırılabilir.
Bunun tutunamamasının nedeninin sektördeki ticari dinamikler mi, yoksa bu stratejinin gerçekten iyi olmamasına yol açan teknik nedenler mi olduğunu merak ediyorum.
Birincisi, derleyiciler bu tür komut zamanlamasında iyi değildi; daha sonra geliştiklerinde ise Itanium çoktan batmıştı. İkincisi, mevcut komut kümesi, yani x86, bunu çalışma zamanında donanımla oldukça iyi yapar hale geldi ve pratikte statik zamanlamadan biraz daha iyi sonuçlar verdi. Çünkü çalışma zamanında profilleme verisi vardır.
Linus’un bu konuyla biraz ilgili iyi bir çıkışı [0]’da var. “RISC insanları, 32 yazmacın tamamını verimli kullanan döngüler üretmesi için derleyiciyi optimize etmeye uğraşırken, x86 uygulayıcıları bunun yerine çipin çeşitli yükler altında hızlı çalışmasını sağladı ve devasa register renaming donanımı kullandı. Bellek yeniden adlandırmayı da görüyoruz.”
[0] https://yarchive.net/comp/linux/x86.html
VLIW bazı nişlerde gerçekten iyi çalışır. Sıralı yürütülen tek komutlara göre elle de derleyiciyle de programlaması daha zordur, ama donanımdaki zamanlamayı basitleştirir. Paketlenen komutların gecikme süreleri birbirine benzerse daha iyi çalışır.
Günümüzde temel tasarım bulmacası, bellek erişiminin aritmetikten çok daha fazla çevrim tüketmesidir. Birkaç çevrimlik aritmetiği birkaç yüz çevrimlik bellek yüklemesiyle paketlemenin pek anlamı yoktur. Bu yüzden VLIW, bellek erişiminin hızlı olduğunu bildiğinizde, kabaca L1 önbelleğe ya da ona denk bir yere sığacağını bildiğinizde iyi çalışır. DSP tarzı sistemlere uygun olmasının nedenlerinden birinin bu olduğunu düşünüyorum.
Dışa açılmış pipeline’lar da bu sistemlerin bazılarının ilginç bir özelliğidir. VLIW paketi içindeki bir komut bir yazmaca yazarsa, aynı yazmacı okuyan sonraki komutlar sonraki N çevrim boyunca eski değeri görür; yazma ancak bundan sonra görünür olur. Elle programlamak gerçekten kafa karıştırıcıdır, ama derleyici bu tür zamanlamayı halledebilir.
Yakın zamana kadar DSP ve HPC pazarın çok küçük bir bölümünü oluşturduğundan, dinamik zamanlama yapabilen mimariler daha fazla yatırım aldı ve bu pazarları bile domine etti.
GPU’larda elbette durum değişti ve GPU’lar gerçekten de statik zamanlamaya daha çok dayanıyordu. Ancak GPU’lar da daha çeşitli yüklere genişledikçe giderek daha dinamik öğeler kazanıyor.
https://news.ycombinator.com/context?id=37900987
Itanium bunu CPU olarak piyasaya sürmeye yönelik başlıca girişimdi. Şimdi AMD64 ve ARM egemen, ama gelecekte bunu tekrar görebiliriz gibi geliyor.