İçeriğe geç
academia.sh

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 Olcek bir 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ş for dö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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat