Derleme işleminde neden GPU kullanılmıyor?

Katılım
17 Aralık 2023
Mesajlar
7.343
Makaleler
2
Çözümler
34
Beğeniler
6.354
Yer
Denizli
Bildiğim kadarıyla GPU'lar, CPU'lardan bazen daha hızlı oluyor. Bu da Gentoo falan derlemeyi bayağı hızlandırır. Peki neden bunu akıl etmiyorlar?
 
GPU, CPU gibi çalışmıyor çünkü. Bunu ben de düşünmüştüm.


Kısaca sebep, iki donanım arasındaki mimari farklar. Bu yüzden derleyemiyoruz.
GPU'lar grafik işlemleri için dizayn edilmiş parçalar. Vektör hesaplama, matris işlemleri vs.
 
Ekran kartları kayar nokta(özellikle 32 bit) ve tam sayıda yüksek hızlı toplama ve çarpma işlemi yapmak için optimize edilmiştir, işlemci kadar versatil değildir.

En önemli şey ise mantık. Derleme süreci iç içe karmaşık mantık içerir. Derleyici belki binlerce karşılaştırma yapar ve optimizasyon için karmaşık maliyet model analizleri yapabilir.

İşlemci hem versatil yani birden fazla veri tipini işleyebilir ve sayılar arasında kompleks matematik yürütebilir. Aynı zamanda kompleks mantık işlemlerini de yapabilir. Mantıktan kasıt matematiksel mantık; ve/veya/ya da gibi.
 
Baştan Uyarayım: Baya bir uzun yazı olacak. Eğer compiler'ın nasıl çalıştığını merak etmiyorsanız geçin. Anlatımı C dili üzerinden yapacağım.

Derleyiciler, yazdığınız yüksek seviyeli kodu doğrudan makine diline çeviremez. Bunun yerine, birçok aşamadan geçirerek işler.


İşte tam da bu yazıda bu aşamaları açıklayacağım:

1. Lexical Analysis (Sözcüksel Analiz):​

Derleyici, insan gibi yazdığınız float mrb = 12.0f; gibi bir satırı doğrudan anlayamaz. Önce bu satırı parçalara ayırarak tokenize etmesi gerekir, bunu da bu şekilde yapar:
  • float → anahtar kelime (veri türü)
  • mrb → değişken ismi
  • = → atama operatörü
  • 12.0f → sayı
  • ; → noktalama
Gerçek hayattan örnek:
"Ben elma yedim."
→ Ben (zamir), elma (isim), yedim (fiil), . (noktalama)

2. Syntax Analysis (Sözdizimsel Analiz):​

Token'ler sıralama olmadan bir araya gelirse hiçbir şey oluşturmaz, derleyici, bir ağaç yapısı (parse tree) oluşturarak bunu kontrol eder:

C:
     =
    / \
float  mrb
        \
       12.0f

Eğer yazım hatası varsa burada hata verir.
Gerçek hayattan örnek:
"Ben elma yedim." doğrudur ama "Yedim elma ben." bozuk bir cümledir, hata verir.

3. Semantic Analysis (Anlamsal Analiz):​

Bu aşamada, sözdizimi doğru bile olsa, anlam hataları tespit edilir. Örnek:

kod.c
C:
#include <stdio.h>

void Fonksiyonum();

int main()
{
    int test = 20.2f; // Hatalı, integer'a float değerinde bir sayı veremezsiniz çünkü tamsayı bekler, ya hata alırsınız ya da uyarı alıp kayıplı bir dönüştürme yapmak zorunda kalırsınız.

    float test  = 212.0f; // Hatalı, "test" isimli değişken zaten yukarıda tanımlanmış.

    int mrb = mrb2; // Hatalı, mrb2 diye bir değişken yok.

    Fonksiyonum(); // Doğru, Fonksiyonun adresini tespit etmek derleyicinin işi değil bağlayıcının işi, eğer sorun varsa bağlayıcı hata verir.

    Fonksiyonum2(); // Hatalı, Fonksiyonum2() hiçbir yerde tanımlanmamış.

    return 0;
}

Gerçek hayattan örnek:
"Ben televizyonu kustum." cümlesinde gramer doğru olabilir ama mantık hatası içerir, yani hatalı.

4. Intermediate Code Generation (Ara Kod Oluşturma):​

Makine koduna geçmeden önce, derleyici kodu bir ara dile (örneğin LLVM IR) çevirir. Bu sayede platformdan bağımsız çalışabilir ve bir sonraki aşamaları daha verimli yapabilir.

Örneğin:
Kod:
// C kodu
int topla(int a, int b) { return a + b; }
Kod:
// Rust kodu
pub fn topla(a: i32, b: i32) -> i32 { a + b }

LLVM IR üzerinde aynı koda dönüşür:
Kod:
// LLVM-IR Kodu (LLVM-IR'de yorum yok aslında, ama aramızda kalsın :P)
define i32 @topla(i32 %a, i32 %b) {
    %result = add i32 %a, %b
    ret i32 %result
}

Gerçek hayattan örnek:
Diyelim ki elinizde Türkçe, İspanyolca ve Almanca bir cümle var, derleyici bunlarla ayrı ayrı uğraşmak yerine hepsini İngilizce diline çevirip oradan devam ediyor, böylelikle daha rahat iş yapabiliyor.

5. Optimizasyon:​

Derleyici, kodunuzu daha verimli çalışması için optimize eder. Örneğin:
C:
int a = 44 + 38; // Derleyici bunu doğrudan int a = 82; yapar, böylelikle işlemsiz olur ve kod hızlanır.
Kayan sayılarla ilgili çok daha ilginç bir örnek vereyim:
C:
void add4(float *a, float *b, float *result)
{
    for (int i = 0; i < 4; i++) {
        result[i] = a[i] + b[i];
    }
}
Makine dilinde kayan noktalı sayılar (floating point) tamsayılar gibi doğrudan aritmetik birimlerde (ALU) işlenemez. Bu işlemler özel olarak tasarlanmış FPU (Floating Point Unit) birimlerinde gerçekleştirilir. Ancak bu x86 üzerinde ana FPU (x87) yaklaşık 45 yıllık bir mimari temele dayandığı için berbat çalışır.

Günümüzde matris çarpımı gibi vektör-tabanlı işlemler için SIMD (Single Instruction, Multiple Data) tabanlı genişletmeler (örneğin SSE, AVX) kullanılır. Derleyiciler, derleme sırasında verdiğimiz optimizasyon bayraklarına göre bu işlemleri klasik FPU yerine SIMD registerları ile optimize edebilir.

Aşağıda aynı floating point işlemini hem klasik x87 FPU hem de AVX komut seti kullanarak derleyip elde ettiğimiz Assembly çıktısını karşılaştıracağız.

X87:
Kod:
add4:                           ; Fonksiyon girişi
    pushl   %ebp                ; ebp'yi (base pointer) stack'e kaydeder
    movl    %esp, %ebp          ; ebp'yi şu anki esp (stack pointer) değerine ayarlar, fonksiyon içi stack frame oluşturur
    subl    $16, %esp           ; lokal değişkenler için yer ayır (4 * 4)
    movl    $0, -4(%ebp)        ; döngüyü başlatan i değişkenini 4 bytelık bir şekilde 0 sayısına ayarla (int 4 byte'dır)
.L2:                            ; döngü başlangıcı
    movl    -4(%ebp), %eax      ; eax = i
    leal    0(,%eax,4), %edx    ; edx = i * 4 (float 4 byte'dır)
    movl    8(%ebp), %eax       ; eax = a (ilk argümanın adresi float* a)
    addl    %edx, %eax          ; eax = a + i*4 (c dilinde a[i] tarafına karşılık veriyoruz)
    flds    (%eax)              ; st(0) = a[i] (x87 FPU stackine yükle)

    movl    -4(%ebp), %eax      ; eax = i
    leal    0(,%eax,4), %edx    ; edx = i * 4
    movl    12(%ebp), %eax      ; eax = b (ikinci argümanın adresi float* b)
    addl    %edx, %eax          ; eax = b + i*4 (b[i]'nin adresi)
    flds    (%eax)              ; st(0) = b[i], st(1) = a[i]


    faddp   %st, %st(1)         ; st(1) = a[i] + b[i], sonra st(0) pop edilir
    ; faddp: FPU stackinin en üstündeki iki değeri toplar (st(0) + st(1))
    ; sonucu st(1)'e kaydeder ve st(0)'ı stack'ten çıkarır (pop)

    movl    -4(%ebp), %eax      ; eax = i
    leal    0(,%eax,4), %edx    ; edx = i * 4
    movl    16(%ebp), %eax      ; eax = result (üçüncü argümanın adresi float* result)
    addl    %edx, %eax          ; eax = result + i*4 (result[i]'nin adresi)
    fstps   (%eax)              ; result[i] = st(0); sonra st(0) pop edilir

    addl    $1, -4(%ebp)        ; i++
    cmpl    $3, -4(%ebp)        ; karşılaştırma, i 3 veya altında mı kontrol eder
    jle     .L2                 ; eğer öyleyse döngü tekrardan devam eder

    leave                       ; üstteki stackler temizlenir
    ret                         ; fonksiyon döndürülür

Kodu anlamanıza gerek yok, sadece ne kadar uzun olduğuna bakın. İşlemciler uzun talimatlarda daha düşük performansla çalışır ve daha yüksek güç tüketir. Aynısını AVX ile yapalım:
Kod:
add4:
    vmovups    xmm0, XMMWORD PTR [rdi]    ; a[0..3] -> xmm0
    vmovups    xmm1, XMMWORD PTR [rsi]    ; b[0..3] -> xmm1
    vaddps     xmm0, xmm0, xmm1           ; xmm0 = a[0]+b[0], a[1]+b[1], a[2]+b[2], a[3]+b[3] (element-wise)
    vmovups    XMMWORD PTR [rdx], xmm0    ; result = xmm0 (ama unaligned)
    ret                                   ; fonkisyon döndürür
xmm registerları 4 adresi aynı anda işleyebilir, resmen GPU gibi çalışır ve bu yapılara SIMD (Tek talimat, çoklu veri) deriz.

6. Code Generation (Kod Üretimi)​

Son aşamada ara kod platforma özgü Assembly'ye çevrilir ve çalıştırılmaya hazır hale gelir.


Peki bunları neden anlattım?​

Şu sorunun cevabı için:

GPU’lar neden derleyici gibi davranamaz?
Bir GPU’nun bu 6 aşamayı CPU kadar verimli yapamamasının sebebi mimarisinin ve komut setinin farklı olmasıdır.

Amaç?​

CPU:
  • Genel amaçlıdır (ALU, branch prediction, cache'ler ile optimize).
  • Sıralı ve mantıksal işlemlerde mükemmel.
  • Talimat başına düşük gecikme, yüksek esneklik (çok fazla şey yapabiliyor).
GPU:
  • Paralel işleme uygundur (kriptografi, yapay zeka, blockchain ve rendering gibi yerlerde mükemmel).
  • SIMD yapısı vardır (aynı işlemi çok veri üzerinde uygular).
  • Dallanma, sıralı karar alma, token analizi gibi işlerde verimsiz.

Derleyiciyi CPU'da kullanmak mantıklı bir iştir çünkü analiz, karar verme, hata denetimi gibi çok adımlı, sıralı mantıksal işlemleri yapabilir (GPU'da her bir karmaşık adımın bütün threadlarda yapıldığını hayal edin, kaos çıkar).

GPU ise bu iş için fazla hantal, fazla paralel, fazla "aynı anda aynı şeyi yap" mantığına sahiptir. Bu yüzden derleyicileri GPU üzerinde kullanırsanız tam da bu hissiyata kapılırsınız:



Şaka bir yana, CPU'lar 10 parmakla 10 marifet görürken GPU'lar 10 parmakla 1 marifet görüyor, ama CPU'dan milyon kez daha iyi yapıyor o marifeti.

Makale tarzında oldu biraz ama idare edin, umarım beğenirsiniz
 
Gayet güzel bir açıklama olmuş, elinize sağlık.
 
CPU komutlarını, GPU komutuna çeviren bir yazılım çıkarsa olur. Bir nevi CPU emülasyonu yapılmalı yani. Şu an olmaz. Çünkü CPU komutları ve mimarisi ile GPU komutları ve mimarisi farklı şeydir. C/C++, CPU odaklı derleme yapar.

Derlenen yazılımı da CPU anlar ve işler.
 
Yukaridaki arkadaslar cok guzel anlatmis ama bir iki kelam etmezsem icimde kalacak. GPU ile CPU arasindaki veri aktarim hizi da bu konunun gercege gecirilmemesi konusunda bir etkendir. PCIe koprusu hayli hizli olmasina ragmen buyuk dosya transferleri icin CPU'ya asla bir alternatif olamaz.

Bu yuzden grafik programlama yaparken gereken matrisler GPU'ya yollanir ve gereken "linear transformation" islemlerini ekran karti yapar. Is burada matematige ve ekran kartinin lineer cebir yapmaya ozel olarak tasarlanmis bir donanim olmasina dayaniyor tabii ki; shader'larin arkasindaki kodlari incelerseniz zaten 4x4 matrislerde genelde sinus ve kosinus degerleri ile islemlerin yapilarak ona gore piksellerin boyandigini goreceksiniz.

Genel amacli islemler yapmak icin daha yeni nesil GPU'larda CUDA ya da GPGPU cekirdekleri goruyoruz. Bunlar genelde islemcinin yukunu almak ve genel matematiksel/algoritmik hesaplar yapmak (finans uygulamalari, parcacik simulasyonlari, fizik simulasyonlari) icin kullaniliyor.

Bir cross-compiler belki bu yontemle calisabilir ama tekrar veri okumak icin PCIe koprusunun azizligine takiliyor; diger uygulamalarda kod ve icindeki veri yine bir nevi GPU'ya bagli oluyor (parcacik simulasyonlarindaki parcaciklari zaten yazilan kod uretiyor belli sabit ozelliklere gore, kripto para madenciligi uygulamalarinda zaten belli bir algoritmaya gore anahtar cozuluyor vs.) fakat derleme isi icin yine diskten veriyi bellege okuyup yorumlayip degerlendirmek gerekiyor, burada da islemci zaten GPU'dan cok daha ustun oluyor.

Ek olarak, meraklilar su videoya goz atabilir:

 
Bu siteyi kullanmak için çerezler gereklidir. Siteyi kullanmaya devam etmek için çerezleri kabul etmelisiniz. Daha Fazlasını Öğren.…