Bir derleyici geliştirmek çok zor bir iştir. Ancak, neyse ki, LLVM gibi projelerin geliştirilmesiyle bu sorunun çözümü büyük ölçüde basitleştirilmiştir; bu, tek bir programcının bile performans açısından C'ye yakın yeni bir dil yaratmasına olanak tanır. sistem çok az dokümantasyonla donatılmış çok miktarda kodla temsil edilir. Bu eksikliği gidermeye çalışmak için, bugün çevirisini yayınladığımız materyalin yazarı, Go'da yazılmış kod örneklerini gösterecek ve bunların ilk olarak Go'ya nasıl çevrildiğini gösterecek.
İlk örnek
Burada bakacağım ilk fonksiyon, sayıları toplamaya yönelik basit bir mekanizmadır:
func myAdd(a, b int) int{
return a + b
}
Bu işlev çok basittir ve belki de hiçbir şey bundan daha basit olamaz. Aşağıdaki Go SSA koduna çevrilir:
func myAdd(a int, b int) int:
entry:
t0 = a + b int
return t0
Bu görünümde veri türü ipuçları sağ tarafa yerleştirilir ve çoğu durumda göz ardı edilebilir.
Bu küçük örnek zaten SSA'nın bir yönünün özünü görmenize olanak sağlıyor. Yani kodu SSA formuna dönüştürürken her ifade kendisini oluşturan en temel parçalara bölünür. Bizim durumumuzda komut return a + b
aslında iki işlemi temsil eder: iki sayının toplanması ve sonucun döndürülmesi.
Ayrıca burada programın temel bloklarını görebilirsiniz; bu kodda yalnızca bir blok vardır - giriş bloğu. Aşağıda bloklar hakkında daha fazla konuşacağız.
Go SSA kodu kolayca LLVM IR'ye dönüştürülür:
define i64 @myAdd(i64 %a, i64 %b) {
entry:
%0 = add i64 %a, %b
ret i64 %0
}
Burada farklı sözdizimsel yapılar kullanılmasına rağmen işlevin yapısının temelde değişmediğini fark edebilirsiniz. LLVM IR kodu, C'ye benzer şekilde Go SSA kodundan biraz daha güçlüdür. Burada, işlev bildiriminde, önce döndürdüğü veri türünün açıklaması vardır, bağımsız değişken adından önce bağımsız değişken türü belirtilir. Ek olarak, IR ayrıştırmayı basitleştirmek için küresel varlıkların adlarının önüne şu sembol gelir: @
ve yerel adlardan önce bir sembol var %
(bir işlev aynı zamanda küresel bir varlık olarak kabul edilir).
Bu kodla ilgili dikkat edilmesi gereken bir nokta, Go'nun tür temsil kararının int
Derleyiciye ve derlemenin hedefine bağlı olarak 32 bit veya 64 bit değer olarak temsil edilebilen LLVM, IR kodunu oluşturduğunda kabul edilir. Bu, LLVM IR kodunun birçok kişinin düşündüğü gibi platformdan bağımsız olmamasının birçok nedeninden biridir. Bir platform için oluşturulan bu tür kod, başka bir platform için kolayca alınıp derlenemez (bu sorunu çözmeye uygun olmadığınız sürece)
Dikkat edilmesi gereken bir diğer ilginç nokta ise türün i64
işaretli bir tam sayı değildir: sayının işaretini temsil etmesi açısından nötrdür. Talimata bağlı olarak hem imzalı hem de imzasız sayıları temsil edebilir. Toplama işleminin temsili durumunda bunun bir önemi yoktur, bu nedenle işaretli veya işaretsiz sayılarla çalışmanın bir farkı yoktur. Burada, C dilinde işaretli bir tamsayı değişkeninin taşmasının tanımsız davranışa yol açtığını, dolayısıyla Clang ön ucunun işleme bir bayrak eklediğini belirtmek isterim. nsw
(imzalı sarma yok), bu da LLVM'ye eklemenin asla taşmayacağını varsayabileceğini söyler.
Bu, bazı optimizasyonlar için önemli olabilir. Örneğin iki değerin eklenmesi i16
32 bitlik bir platformda (32 bitlik kayıtlarla), aralıkta kalabilmek için ekleme sonrasında bir işaret genişletme işlemi gerekir i16
. Bu nedenle, makine kayıt boyutlarına dayalı olarak tamsayı işlemlerini gerçekleştirmek genellikle daha verimlidir.
Bu IR koduyla bundan sonra ne olacağı şu anda bizi pek ilgilendirmiyor. Kod optimize edilir (ancak bizimki gibi basit bir örnekte hiçbir şey optimize edilmez) ve ardından makine koduna dönüştürülür.
İkinci örnek
Bakacağımız bir sonraki örnek biraz daha karmaşık olacak. Yani bir dilim tam sayının toplamını alan bir fonksiyondan bahsediyoruz:
func sum(numbers []int) int {
n := 0
for i := 0; i < len(numbers); i++ {
n += numbers[i]
}
return n
}
Bu kod aşağıdaki Go SSA koduna dönüştürülür:
func sum(numbers []int) int:
entry:
jump for.loop
for.loop:
t0 = phi [entry: 0:int, for.body: t6] #n int
t1 = phi [entry: 0:int, for.body: t7] #i int
t2 = len(numbers) int
t3 = t1 < t2 bool
if t3 goto for.body else for.done
for.body:
t4 = &numbers[t1] *int
t5 = *t4 int
t6 = t0 + t5 int
t7 = t1 + 1:int int
jump for.loop
for.done:
return t0
Burada zaten kodu SSA formunda temsil etmeye yönelik daha fazla tipik yapıyı görebilirsiniz. Belki de bu kodun en belirgin özelliği, yapılandırılmış akış kontrol komutlarının bulunmamasıdır. Hesaplamaların akışını kontrol etmek için yalnızca koşullu ve koşulsuz atlamalar vardır ve bu komutu akışı kontrol etmeye yönelik bir komut olarak düşünürsek, bir dönüş komutu vardır.
Aslında burada programın küme parantezleri kullanılarak (C dil ailesindeki gibi) bloklara bölünmemesine dikkat edebilirsiniz. Assembly dillerini anımsatan etiketlerle bölünür ve temel bloklar halinde sunulur. SSA'da temel bloklar, bir etiketle başlayan ve temel blok tamamlama talimatlarıyla biten bitişik kod dizileri olarak tanımlanır; örneğin - return
и jump
.
Bu kodun bir başka ilginç detayı da talimatla temsil edilmektedir. phi
. Talimatlar oldukça sıra dışıdır ve anlaşılması biraz zaman alabilir. bunu hatırla myAdd
yukarıda gösterilmiştir ancak bu bölümde tartışılan fonksiyon gibi daha karmaşık fonksiyonlar için uygun değildir sum
. Özellikle döngünün yürütülmesi sırasında değişkenler değişir i
и n
.
SSA, sözde talimat kullanılarak değişken değerlerin atanmasına ilişkin kısıtlamayı atlar phi
(adı Yunan alfabesinden alınmıştır). Gerçek şu ki, C gibi diller için kodun SSA temsilinin üretilmesi için bazı hilelere başvurmanız gerekiyor. Bu talimatın çağrılmasının sonucu değişkenin geçerli değeridir (i
veya n
) ve parametre olarak temel blokların bir listesi kullanılır. Örneğin şu talimatı göz önünde bulundurun:
t0 = phi [entry: 0:int, for.body: t6] #n
Anlamı şu şekildedir: Eğer önceki temel blok bir blok ise entry
(giriş), ardından t0
bir sabittir 0
ve eğer önceki temel blok for.body
, o zaman değeri almanız gerekir t6
bu bloktan. Bunların hepsi oldukça gizemli görünebilir, ancak SSA'nın çalışmasını sağlayan şey bu mekanizmadır. İnsan açısından bakıldığında, tüm bunlar kodun anlaşılmasını zorlaştırır, ancak her değerin yalnızca bir kez atanması, birçok optimizasyonu çok daha kolay hale getirir.
Kendi derleyicinizi yazarsanız genellikle bu tür şeylerle uğraşmanıza gerek kalmayacağını unutmayın. Clang bile tüm bu talimatları oluşturmuyor phi
bir mekanizma kullanır alloca
(sıradan yerel değişkenlerle çalışmaya benzer). Daha sonra, bir LLVM optimizasyon geçişi çalıştırırken çağrıldı alloca
SSA formuna dönüştürüldü. Ancak TinyGo, zaten SSA formuna dönüştürülmüş olan Go SSA'dan girdi alır.
Söz konusu ara kod parçasının bir başka yeniliği, dilim elemanlarına indeks yoluyla erişimin, adresi hesaplama işlemi ve sonuçta ortaya çıkan işaretçinin referansını kaldırma işlemi şeklinde temsil edilmesidir. Burada sabitlerin IR koduna doğrudan eklendiğini görebilirsiniz (örneğin - 1:int
). Fonksiyonun olduğu örnekte myAdd
bu kullanılmadı. Artık bu özellikleri bir kenara bıraktığımıza göre, bu kodun LLVM IR formuna dönüştürüldüğünde neye dönüştüğüne bir göz atalım:
define i64 @sum(i64* %ptr, i64 %len, i64 %cap) {
entry:
br label %for.loop
for.loop: ; preds = %for.body, %entry
%0 = phi i64 [ 0, %entry ], [ %5, %deref.next ]
%1 = phi i64 [ 0, %entry ], [ %6, %deref.next ]
%2 = icmp slt i64 %1, %len
br i1 %2, label %for.body, label %for.done
for.body: ; preds = %for.loop
%3 = getelementptr i64, i64* %ptr, i64 %1
%4 = load i64, i64* %3
%5 = add i64 %0, %4
%6 = add i64 %1, 1
br label %for.loop
for.done: ; preds = %for.loop
ret i64 %0
}
Daha önce olduğu gibi burada da diğer sözdizimsel yapıları içeren aynı yapıyı görebiliriz. Örneğin aramalarda phi
değerler ve etiketler değiştirildi. Ancak burada özellikle dikkat edilmesi gereken bir şey var.
Başlangıç olarak burada tamamen farklı bir fonksiyon imzasını görebilirsiniz. LLVM dilimleri desteklemez ve sonuç olarak bir optimizasyon olarak bu ara kodu oluşturan TinyGo derleyicisi bu veri yapısının açıklamasını parçalara ayırır. Üç dilim öğesini temsil edebilir (ptr
, len
и cap
) bir yapı (yapı) olarak kullanılabilir, ancak bunları üç ayrı varlık olarak temsil etmek bazı optimizasyonlara izin verir. Diğer derleyiciler, hedef platformun işlevlerinin çağrı kurallarına bağlı olarak dilimi başka şekillerde temsil edebilir.
Bu kodun bir başka ilginç özelliği de talimatın kullanılmasıdır. getelementptr
(genellikle GEP olarak kısaltılır).
Bu talimat işaretçilerle çalışır ve bir dilim öğesine işaretçi elde etmek için kullanılır. Örneğin C dilinde yazılmış aşağıdaki kodla karşılaştıralım:
int* sliceptr(int *ptr, int index) {
return &ptr[index];
}
Veya buna aşağıdaki eşdeğerle:
int* sliceptr(int *ptr, int index) {
return ptr + index;
}
Burada en önemli şey talimatların getelementptr
referans kaldırma işlemlerini gerçekleştirmez. Sadece mevcut işaretçiyi temel alarak yeni bir işaretçi hesaplar. Talimat olarak alınabilir mul
и add
donanım düzeyinde. GEP talimatları hakkında daha fazla bilgi edinebilirsiniz
Bu ara kodun bir başka ilginç özelliği de talimatın kullanılmasıdır. icmp
. Bu, tamsayı karşılaştırmalarını uygulamak için kullanılan genel amaçlı bir talimattır. Bu talimatın yürütülmesinin sonucu her zaman türünde bir değerdir. i1
— mantıksal değer. Bu durumda anahtar kelime kullanılarak bir karşılaştırma yapılır. slt
(küçük olarak işaretlenmiştir), daha önce tür tarafından temsil edilen iki sayıyı karşılaştırdığımız için int
. İki işaretsiz tam sayıyı karşılaştırıyorsak, şunu kullanırız: icmp
ve karşılaştırmada kullanılan anahtar kelime şu olacaktır: ult
. Kayan nokta sayılarını karşılaştırmak için başka bir komut kullanılır: fcmp
, benzer şekilde çalışır.
sonuçlar
Bu materyalde LLVM IR'nin en önemli özelliklerini ele aldığıma inanıyorum. Elbette burada çok daha fazlası var. Özellikle, kodun ara temsili, optimizasyon geçişlerinin, kodun derleyici tarafından bilinen ve IR'de başka türlü ifade edilemeyen belirli özelliklerini hesaba katmasına olanak tanıyan birçok ek açıklama içerebilir. Mesela bu bir bayrak inbounds
GEP talimatları veya bayrakları nsw
и nuw
talimatlara eklenebilecek add
. Aynı şey anahtar kelime için de geçerli private
, optimize ediciye, işaretlediği işleve geçerli derleme biriminin dışından başvurulmayacağını belirtir. Bu, kullanılmayan argümanların ortadan kaldırılması gibi birçok ilginç prosedürler arası optimizasyona olanak tanır.
LLVM hakkında daha fazla bilgiyi şurada bulabilirsiniz:
Sevgili okuyucular! LLVM'yi kullanıyor musunuz?
Kaynak: habr.com