Ders 01 / 10
Kaynak, Bayt Kodu ve Sanal Makine
Java'da yazılan kaynak, çalıştırılan şey değildir: on iki yöntemin sekizi sınıf dosyasında kaynakta bulunmayan bir adım taşır ve toplam 13 eklenen adım sayılır; buna karşılık kaynakta duran bir birleştirme sınıf dosyasında hiç yoktur.
İçindekiler
Python müfredatı beş kursla kapandı ve kapanış kursu bir projenin sayısını tek soruyla tanımladı: aynı girdi kaç ayrı sonuç veriyor. O müfredat boyunca ölçülen şeylerin ortak bir yanı vardı — yazılan kaynak, yorumlayıcının gördüğü şeye yakın duruyordu. Bir sözdizim biçiminin altında hangi yöntemin çağrıldığı sorulduğunda yanıt çalışma zamanında aranıyordu, çünkü kaynak metin ile çalışan şey arasındaki mesafe kısaydı.
Java’da o mesafe kısa değildir. Kaynak ile çalıştırılan şeyin arasına bir derleyici (compiler) ve onun ürettiği bir sınıf dosyası (class file) girer; Java sanal makinesi (Java virtual machine) kaynağı değil o dosyayı okur. Aradaki fark bir biçim farkı değildir: sınıf dosyasında kaynakta yazılmamış şeyler bulunur. Bu dersin ve bütün kursun sorusu buradan çıkar — yazılan satır, sınıf dosyasında kaç adıma ve hangi adımlara dönüşüyor?
Zincir ve Bu Dersin Payı
Bilgisayarlar Nasıl Çalışır kursu derleyici, yorumlayıcı ve sanal makine üçlüsünü, bayt kodu (bytecode) kavramını ve anında derlemeyi (just-in-time compilation) kurdu; derleme aşamalarını da ayrı bir derste anlattı. Bunlar burada tekrarlanmaz. Orada kurulan kavramdı; burada ölçülen, bir derleyicinin bu kaynak için gerçekten ürettiği dosyadır.
Zincir üç durak taşır. Kaynak metin insanın yazdığı şeydir. Derleyici onu bir sınıf dosyasına çevirir; bu dosya bayt kodu taşır, yani sanal makinenin komut kümesiyle yazılmış bir komut dizisi. Sanal makine dosyayı yükler ve komutları yürütür. İki geçişten yalnız birincisi bu dersin konusudur: kaynak ile sınıf dosyası arasındaki fark.
Farkın ölçülebilir olması bir kolaylıktan gelir. Sınıf dosyasının biçimi tanımlıdır ve standart kitaplık hem o dosyayı okuyan bir arayüz hem de program içinden çağrılabilen bir derleyici taşır. Ölçüm için hiçbir dış araca gerek yoktur: bir metin veririz, derleriz, çıkan dosyayı okuruz. Kaynağı kendimiz yazdığımız için ne yazdığımızı biliriz; dosyada bulduğumuz fazlalık derleyicinin koyduğudur.
Ölçüm Çekirdeği
Kursun bütün ölçümleri tek bir yardımcı sınıftan geçer. Olcek, verilen kaynak metnini
derler, ürettiği sınıf dosyasını okur ve her yöntem için iki şey döndürür: kaç komut
(instruction) üretildiği ve kaynakta yazılmayan hangi adımın eklendiği. Eklenen adım
beş türden biridir — bir çağrı, bir dinamik çağrı, bir nesne ya da dizi üretimi, bir tip
denetimi, bir sayısal çevirme.
Ölçümün varsayımları:
- CZ1 — Kâhin kurgunun kendisidir: kaynağı biz yazdığımız için ne yazdığımızı biliriz, dolayısıyla sınıf dosyasında bulunan fazlalık bir çıkarım değil bir dökümdür.
- CZ2 — Derleyici ve sınıf dosyası okuyucusu standart kitaplığın parçasıdır ve program içinden çağrılır; hiçbir dış araç kullanılmaz.
- CZ3 — Sayılan birim komuttur. Sayılar bir derleyici koşumundan gelir ve o koşumda belirlenimcidir; dersin tezi tek bir komut sayısına değil, sayılar arasındaki yöne dayanır.
- CZ4 — Ölçüm hiçbir ortama bağlı veri okumaz: süre, bellek adresi, kimlik ve yol yazılmaz.
- CZ5 — Ölçülen kaynak on iki yöntem taşır ve bu yöntemler kursun ders ders ödeyeceği sözdizim biçimlerini kapsar; dilin bütün sözdizimi değildir.
// Olcek.java — ölçüm çekirdeği: derleyicinin sınıf dosyasına ne eklediğini okur
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.classfile.*;
import java.lang.classfile.instruction.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;
class Olcek {
record Yontem(String ad, int komut, List<String> eklenen) {}
static ClassModel derle(String kaynak, String sinif) throws Exception {
Path d = Files.createTempDirectory("olcek");
Path k = d.resolve(sinif + ".java");
Files.writeString(k, kaynak);
PrintWriter bos = new PrintWriter(Writer.nullWriter());
if (ToolProvider.findFirst("javac").orElseThrow()
.run(bos, bos, "-d", d.toString(), k.toString()) != 0)
throw new IllegalStateException("derlenmedi: " + sinif);
return ClassFile.of().parse(d.resolve(sinif + ".class"));
}
static List<Yontem> oku(String kaynak, String sinif) throws Exception {
List<Yontem> yontemler = new ArrayList<>();
for (MethodModel m : derle(kaynak, sinif).methods()) {
if (m.code().isEmpty()) continue;
List<String> eklenen = new ArrayList<>();
int komut = 0;
for (CodeElement e : m.code().get()) {
if (e instanceof Instruction) komut++;
if (e instanceof InvokeInstruction iv)
eklenen.add(kisa(iv.owner().asInternalName()) + "." + iv.name().stringValue());
else if (e instanceof InvokeDynamicInstruction id)
eklenen.add("dinamik:" + id.name().stringValue());
else if (e instanceof NewObjectInstruction n)
eklenen.add("yeni " + kisa(n.className().asInternalName()));
else if (e instanceof NewPrimitiveArrayInstruction a)
eklenen.add("yeni dizi " + a.typeKind().name().toLowerCase(Locale.ROOT));
else if (e instanceof NewReferenceArrayInstruction a)
eklenen.add("yeni dizi " + kisa(a.componentType().asInternalName()));
else if (e instanceof TypeCheckInstruction t && t.opcode() == Opcode.CHECKCAST)
eklenen.add("tip denetimi " + kisa(t.type().asInternalName()));
else if (e instanceof ConvertInstruction c)
eklenen.add("cevirme " + c.opcode().name().toLowerCase(Locale.ROOT));
}
yontemler.add(new Yontem(m.methodName().stringValue(), komut, eklenen));
}
return yontemler;
}
static String kisa(String ic) { return ic.substring(ic.lastIndexOf('/') + 1); }
}
Ölçülen kaynak da ayrı bir dosyada durur, çünkü bu derste iki kez okunacaktır.
// Kaynak.java — ölçülen kaynak metni: on iki yöntem, on iki sözdizim biçimi
class Kaynak {
static final String ORNEK = """
import java.util.List;
class Ornek {
static int topla(int a, int b) { return a + b; }
static Object kutula(int n) { Integer k = n; return k; }
static int kutuAc(Integer k) { int n = k; return n; }
static String birlestir(String a, int n) { return a + n; }
static String sabitBirlestir() { return "ba" + "sit"; }
static String donguyleBirlestir(String[] d) {
String s = "";
for (String x : d) s += x;
return s;
}
static int diziToplami(int[] d) { int t = 0; for (int x : d) t += x; return t; }
static int listeToplami(List<Integer> l) { int t = 0; for (int x : l) t += x; return t; }
static long yukselt(byte b, long u) { return b + u; }
static byte daralt(byte b) { b += 300; return b; }
static int degisken(int... sayilar) { return sayilar.length; }
static int degiskenCagrisi() { return degisken(1, 2, 3); }
}
""";
}
Yöntem Yöntem: Kaç Komut, Hangi Adım
// Ana.java — her yöntem için komut sayısı ve kaynakta yazılmayan adım
public class Ana {
public static void main(String[] args) throws Exception {
System.out.printf("%-18s %6s %s%n", "yontem", "komut", "kaynakta yazilmayan adim");
int yazilan = 0, eklenenli = 0, toplam = 0;
for (Olcek.Yontem y : Olcek.oku(Kaynak.ORNEK, "Ornek")) {
System.out.printf("%-18s %6d %s%n", y.ad(), y.komut(),
y.eklenen().isEmpty() ? "-" : String.join(", ", y.eklenen()));
if (y.ad().equals("<init>")) continue;
yazilan++;
if (!y.eklenen().isEmpty()) eklenenli++;
toplam += y.eklenen().size();
}
System.out.println("yazilan yontem: " + yazilan
+ " | eklenen adim tasiyan: " + eklenenli
+ " | toplam eklenen adim: " + toplam);
}
}
yontem komut kaynakta yazilmayan adim <init> 3 Object.<init> topla 4 - kutula 5 Integer.valueOf kutuAc 5 Integer.intValue birlestir 4 dinamik:makeConcatWithConstants sabitBirlestir 2 - donguyleBirlestir 24 dinamik:makeConcatWithConstants diziToplami 24 - listeToplami 20 List.iterator, Iterator.hasNext, Iterator.next, tip denetimi Integer, Integer.intValue yukselt 5 cevirme i2l daralt 7 cevirme i2b degisken 3 - degiskenCagrisi 16 yeni dizi int, Ornek.degisken yazilan yontem: 12 | eklenen adim tasiyan: 8 | toplam eklenen adim: 13
Sayıların Okunması
Yazılan on iki yöntemin sekizi sınıf dosyasında kaynakta bulunmayan bir adım taşıyor; toplam 13 eklenen adım. Kursun taşıyacağı ölçü budur: eklenen adım kuraldır, istisna değildir. Tablonun her satırında solda yazdığımız yöntem, sağda derleyicinin oraya koyduğu şey duruyor.
Satırlar tek tek okunduğunda eklemenin nereden geldiği görünür. Integer k = n kaynakta bir
atamadır, sınıf dosyasında bir Integer.valueOf çağrısıdır. a + n bir işleç gibi yazılır,
sınıf dosyasında bir dinamik çağrıdır. b + u satırında byte değerinin long ile
toplanabilmesi için bir çevirme komutu eklenmiştir. degisken(1, 2, 3) üç argümanlı bir
çağrı gibi durur; sınıf dosyasında önce bir dizi üretilir, sonra tek argümanlı bir çağrı
yapılır — bu yüzden üç komutluk yöntemin çağrısı 16 komut tutar.
Eklenen adım taşımayan dört yöntem de aynı ölçüde bilgi verir: topla, sabitBirlestir,
diziToplami ve degisken. Ortak yanları şudur — hiçbiri çalışma zamanında bir başkasına
soru sormaz. İki tam sayıyı toplamak, bir dizinin öğesini okumak, bir dizinin uzunluğunu
sormak: bunların hepsi sanal makinenin doğrudan komutlarıyla karşılanır ve araya bir çağrı
girmez. Eklenen adımın koşulu buradan okunur: derleyici, yazılan biçimin karşılığını komut
kümesinde bulamadığında araya bir adım koyar.
Bu dördüncü yöntem ayrıca eklenen adımın nereye düştüğünü gösteriyor. degisken üç komuttur
ve hiçbir adım taşımaz; çağrısı ise 16 komut tutar ve bir dizi üretimi taşır. Değişken
sayıda argüman alan bir yöntemin bedeli tanımında değil, çağrıldığı yerdedir. Bir biçimin
maliyetini ararken yalnız tanımına bakmak yanıltır.
Tabloda kaynakta hiç yazılmamış bir yöntem de var: <init>, üç komutluk bir yapıcı.
Kaynakta Ornek sınıfı için bir yapıcı yazılmamıştır; sınıf dosyasında bir tane vardır ve
tek işi taban sınıfın yapıcısını çağırmaktır. Bir sonraki ders bu satırın ne zaman
kaybolduğunu ölçer.
En keskin okuma iki satırın karşılaştırmasından çıkar. diziToplami ile listeToplami
kaynakta aynı döngü yazımını taşır — ikisi de gelişmiş for döngüsüdür. Dizide bu döngü
24 komut ve sıfır eklenen adım üretiyor; listede 20 komut ve beş eklenen adım.
Yani daha az komut, daha çok eklenen adım: komut sayısı ile eklenen adım sayısı aynı yöne
gitmez. Dizide sanal makinenin doğrudan komutları yeterken, listede bir yineleyici zinciri
kuruluyor — iterator, hasNext, next, bir tip denetimi ve bir intValue. İki ölçüm
ayrı şeyler söyler ve bir ölçüm, neyi saydığını söylemeden karşılaştırılamaz.
Aynı gözlem tersinden de kurulabilir. donguyleBirlestir ile diziToplami eşit komut
sayısı taşır — ikisi de 24 — ama biri bir dinamik çağrı taşır, öbürü hiçbir adım taşımaz.
Eşit komut sayısı, eşit iş anlamına gelmiyor. Komut saymak bir ölçüdür; eklenen adım saymak
başka bir ölçüdür ve ikisi birbirinin yerine geçmez.
Sınırlayıcı Ölçüm: Kaynakta Duran, Sınıf Dosyasında Olmayan
Buraya kadarki her satır bir ekleme gösterdi. Tezin sınırı da aynı tabloda duruyor:
sabitBirlestir kaynağında bir birleştirme yazar, sınıf dosyasında 2 komut ve sıfır
eklenen adım taşır. Aynı + işleci birlestirde bir dinamik çağrıya dönüştüğüne göre burada
ne oldu? Sınıf dosyasının komut adlarına bakalım.
// Iz.java — kaynakta duran birleştirme sınıf dosyasında duruyor mu
import java.lang.classfile.*;
import java.lang.classfile.constantpool.*;
import java.util.*;
public class Iz {
static List<String> opkod(ClassModel cm, String yontem) {
List<String> a = new ArrayList<>();
for (MethodModel m : cm.methods())
if (m.methodName().equalsString(yontem) && m.code().isPresent())
for (CodeElement e : m.code().get())
if (e instanceof Instruction i) a.add(i.opcode().name().toLowerCase(Locale.ROOT));
return a;
}
public static void main(String[] args) throws Exception {
ClassModel cm = Olcek.derle(Kaynak.ORNEK, "Ornek");
System.out.println("sabitBirlestir -> " + opkod(cm, "sabitBirlestir"));
System.out.println("birlestir -> " + opkod(cm, "birlestir"));
Set<String> dizgi = new HashSet<>();
for (PoolEntry pe : cm.constantPool())
if (pe instanceof StringEntry se) dizgi.add(se.stringValue());
for (String s : List.of("basit", "ba", "sit"))
System.out.printf("sinif dosyasinda \"%s\" dizgi sabiti: %s%n", s, dizgi.contains(s));
}
}
sabitBirlestir -> [ldc, areturn] birlestir -> [aload_0, iload_1, invokedynamic, areturn] sinif dosyasinda "basit" dizgi sabiti: true sinif dosyasinda "ba" dizgi sabiti: false sinif dosyasinda "sit" dizgi sabiti: false
sabitBirlestir iki komuttan oluşuyor: bir sabit yükle, dön. Ortada birleştirme yoktur.
Dahası, birleştirmenin işlenenleri de yoktur: sınıf dosyası "basit" dizgisini taşır,
"ba" ile "sit" orada bulunmaz. İki sabit derleme sırasında tek bir sabite katlanmıştır —
Bilgisayarlar Nasıl Çalışır kursunun kavram olarak kurduğu sabit katlama (constant
folding) budur, ve burada kavram değil ölçülen bir yokluk olarak görünüyor.
Böylece kursun ikinci iddiası doğuyor: eklenen adım her zaman ekleme değildir; bazen
çıkarmadır. Aynı + işleci iki yöntemde iki ayrı sonuç veriyor — birinde sıfır adım,
öbüründe bir dinamik çağrı. Ayrımı yaratan şey işlecin kendisi değil, işlenenlerinin
derleme zamanında bilinip bilinmediğidir. Yazılan biçime bakarak sınıf dosyasında ne
olacağını söyleyemeyiz.
Çevrilen Şey Hangi Makineye Yazıldı
Komut adları ikinci bir şey daha söylüyor. aload_0 ile iload_1 bir yerel değişken
yuvasını (local variable slot) numarasıyla anar, bir işlemci yazmacını değil. ldc bir
değeri yığıta (stack) koyar, areturn yığıttan alıp döner. Bu komut kümesi bir yığıt
makinesinindir ve hiçbir yerinde gerçek bir donanımın adı geçmez.
Aynı şey adlar için de geçerlidir. Tabloda listeToplami satırı List.iterator ve
Iterator.hasNext yazıyor: sınıf dosyası çağıracağı yöntemi bir adresle değil adıyla
taşır. Adın hangi koda karşılık geldiği derleme zamanında değil, dosya yüklenirken çözülür.
Taşınabilirliğin ölçüsü budur: derleyici, donanımı olan bir makineye değil, belirtimi olan bir makineye yazdı. Sınıf dosyası gerçek işlemciye çevrilmemiştir; bu çeviriyi, çalışırken, sanal makine kendi üstlenir — Bilgisayarlar Nasıl Çalışır kursunun anında derleme başlığıyla kurduğu iş budur. Bu kursun ölçtüğü şey o ikinci geçiş değil, birincisidir.
Özet
- Java’da kaynak ile çalıştırılan şey arasında bir derleyici ve bir sınıf dosyası vardır; sanal makine kaynağı değil, sınıf dosyasındaki bayt kodu okur.
- Ölçüm çekirdeği
Olcekbir kaynak metnini derler ve sınıf dosyasını okuyarak her yöntem için komut sayısını ve kaynakta yazılmayan adımları döker. - Yazılan on iki yöntemin sekizi eklenen adım taşır, toplam 13 eklenen adım sayılır; üstelik kaynakta hiç yazılmamış üç komutluk bir yapıcı da vardır.
- Aynı gelişmiş
fordöngüsü dizide 24 komut ve sıfır eklenen adım, listede 20 komut ve beş eklenen adım üretir: komut sayısı ile eklenen adım aynı yöne gitmez. - Kaynakta duran bir birleştirme sınıf dosyasında hiç yoktur; iki sabit tek sabite katlanır ve işlenenler dosyada bulunmaz. Eklenen adım bazen bir çıkarmadır.
- Sınıf dosyasının komutları yuva numarası ve yığıt işlemi anar, çağıracağı yöntemi adıyla taşır: çevrilen şey donanımı olan bir makineye değil, belirtimi olan bir makineye yazılmıştır.
Sonraki Adım
Bu derste sınıf dosyasının ne olduğu ölçüldü, ama içindeki komutların ne zaman çalıştığına hiç bakılmadı. Tabloda kaynakta yazılmamış bir yapıcı vardı ve onun ne zaman çağrıldığı söylenmedi; bir sınıfın kendi başlatma işinin nerede durduğu da sorulmadı. Sonraki ders programın yaşam döngüsünü ölçer: sanal makine hangi imzayı giriş noktası sayıyor, sınıf başlatma bloğu ile yapıcı hangi sırayla çalışıyor, ve kaynakta iki ad yazılan bir sınıfın sınıf dosyasındaki yüzeyi kaç üye taşıyor?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.