Ders 04 / 11
Anında Derleme
Sınıf dosyasında hiçbir optimizasyon kararı yoktur: aynı yöntem üç yüz bin kez çağrılsa da sınıf dosyasından okunan komut listesi birebir aynı kalır. Kararı veren taraf programın kendisi değildir; kararın dayandığı alıcı tip sayısını ise koşarken çağıran iş parçacığı üretir. Bir çağrı yeri tek alıcılıdan çok alıcılıya geçtiğinde varsayım geçersizleşir — bu ders hız ölçmez, yalnız komut sayısını ve alıcı tip sayısını sayar.
İçindekiler
Önceki derste sınıf dosyasında bulunmayan bir karar görüldü: hangi nesnenin ne zaman toplanacağı. Bu ders sınıf dosyasında bulunmayan başka bir kararı ölçer — bu kez temizlikle değil, hızla ilgili bir karar. Bir yöntemin gövdesi sınıf dosyasında hiç değişmeden dururken, aynı yöntem birinci çağrısında ile bininci çağrısında aynı biçimde koşmayabilir. Bilgisayarlar Nasıl Çalışır kursu anında derlemeyi kavram olarak kurdu ve bu ders onu tekrarlamaz; bu ders yalnız ölçülen davranışa bakar — karar sınıf dosyasında mı duruyor, yoksa başka bir yerde mi?
Sınıf Dosyasında Optimizasyon Kararı Yoktur
Sınıf dosyası derlenip diske yazıldığı andan itibaren sabittir; hiçbir çalışma zamanı olayı onun içeriğini değiştirmez. Bir yöntem ister hiç çağrılmasın ister milyonlarca kez çağrılsın, o yöntemin sınıf dosyasındaki komut listesi aynı kalır — çünkü “hangi yöntem sık çağrılıyor, ne zaman hızlandırılsın” kararı sınıf dosyasının kendisine ait bir bilgi değildir. Bu, iddia olarak bırakılmaz; aşağıda ölçülür.
Bu, önceki üç derste kurulan örüntünün dördüncü tekrarıdır. Yükleyici hangi sınıfı tanımladığını sınıf dosyasına yazmaz; sınıf başlatmasını hangi iş parçacığının koşturduğu sınıf dosyasında görünmez; erişilemez bir nesneyi kimin topladığı da sınıf dosyasının konusu değildir. Dördüncüsü de aynı biçimdedir: sınıf dosyası bir yöntemin ne yaptığını tam olarak söyler, ama o yöntemin çalışma zamanında nasıl koşturulacağı — yorumlanarak mı, hızlandırılmış bir biçimde mi — hiçbir zaman sınıf dosyasının bir parçası olmaz. Dört ölçüm de aynı sonuca varıyor: sınıf dosyası kaynağın bir çevirisidir, ama çalışma zamanının kendisi değildir.
Ölçüm, Java Temelleri kursunun kurduğu Olcek aracını kullanır — burada bir ölçü adı olarak
değil, yalnızca sınıf dosyasından bir yöntemin komut listesini okuyan bir araç olarak.
- JV16 —
Olcekyalnız bir araçtır; sınıf dosyasından bir yöntemin komut listesini okur, hiçbir “eklenen adım” sınıflandırması yapmaz. - JV17 — Aynı yöntem, gerçekten yüklenip binlerce kez çağrıldıktan sonra, komut listesi aynı sınıf dosyasından yeniden okunur; hiçbir yeniden derleme yapılmaz.
- JV18 — Çağrı sayısı sayılır (kaç kez çağrıldığı); süre hiç ölçülmez, hiçbir zaman damgası okunmaz.
// Olcek.java — sinif dosyasindan bir yontemin komut listesini okuyan arac (Java Temelleri'nden, yalnizca arac olarak)
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.classfile.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;
class Olcek {
static Path 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());
ToolProvider.findFirst("javac").orElseThrow().run(bos, bos, "-d", d.toString(), k.toString());
return d;
}
static List<String> komutlar(Path dizin, String sinif, String yontem) throws Exception {
ClassModel cm = ClassFile.of().parse(dizin.resolve(sinif + ".class"));
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;
}
}
// SicakYol.java — bir yontem cok kez cagrilsa da sinif dosyasindaki komut listesi degismiyor
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.util.List;
public class SicakYol {
public static void main(String[] args) throws Exception {
String kaynak = """
public class Hesaplama {
public static int uygula(int x) { return x * 2 + 1; }
}
""";
Path dizin = Olcek.derle(kaynak, "Hesaplama");
List<String> once = Olcek.komutlar(dizin, "Hesaplama", "uygula");
URLClassLoader yukleyici = new URLClassLoader(new URL[] { dizin.toUri().toURL() });
Class<?> sinif = Class.forName("Hesaplama", true, yukleyici);
Method yontem = sinif.getMethod("uygula", int.class);
int cagriSayisi = 300_000;
int toplam = 0;
for (int i = 0; i < cagriSayisi; i++) toplam += (int) yontem.invoke(null, i);
List<String> sonra = Olcek.komutlar(dizin, "Hesaplama", "uygula");
System.out.println("cagri sayisi : " + cagriSayisi);
System.out.println("komut listesi cagridan once : " + once);
System.out.println("komut listesi cagridan sonra : " + sonra);
System.out.println("iki liste birebir ayni mi : " + once.equals(sonra));
System.out.println("toplam sifir degil mi (kosti): " + (toplam != 0));
}
}
cagri sayisi : 300000 komut listesi cagridan once : [iload_0, iconst_2, imul, iconst_1, iadd, ireturn] komut listesi cagridan sonra : [iload_0, iconst_2, imul, iconst_1, iadd, ireturn] iki liste birebir ayni mi : true toplam sifir degil mi (kosti): true
uygula yöntemi üç yüz bin kez, gerçekten yüklenmiş bir sınıf üzerinden çağrıldı — toplam
sıfır çıkmadı, yani çağrılar gerçekten koştu. Buna karşılık aynı sınıf dosyası çağrılardan
önce ve sonra ayrı ayrı okundu ve iki komut listesi birebir aynı çıktı. Bu, “değişmedi
çünkü ölçülmedi” değildir — aynı dosya, aynı yöntem, iki ayrı okuma, aynı sonuç. Yöntemin
sık çağrıldığını fark etmek, onu hızlandırmaya karar vermek, o kararı gerçekten uygulamak —
bunların hiçbiri sınıf dosyasına yazılmaz. Karar, programın kendi kaynak metninde de,
derlediği sınıf dosyasında da hiç yoktur; çalışma zamanında, programın kendisinin
oluşturmadığı bir yerde doğar.
Bilgisayarlar Nasıl Çalışır kursunun kurduğu gibi, sanal makine bayt kodunu doğrudan yorumlayabilir ya da bir bölümünü çalıştığı işlemciye özgü komutlara çevirebilir; hangi yolun seçildiği, o bölümün ne sıklıkla koşturulduğuna bağlıdır. Ama bu çeviri, üretildiği yerde — çalışan programın kendi belleğinde — kalır; kaynağa da, sınıf dosyasına da hiç geri yazılmaz. Aşağıdaki ölçüm bu ayrımı doğrudan sınar: aynı sınıf dosyası, hızlandırılmaya en uygun duruma getirildikten sonra bile, kendi diskteki hâliyle karşılaştırılır.
Ölçümün Method.invoke ile, dolaylı bir çağrı üzerinden yapılması bilinçli bir seçimdir:
Hesaplama sınıfı bu dersin diğer sınıflarıyla aynı dizine derlenmedi, ayrı bir yükleyiciyle
yüklendi ve adı yalnız bir metin olarak taşındı. Bunun nedeni, aynı sınıf dosyasını hem
gerçekten çalıştırıp hem de çağrılardan sonra yeniden okuyabilmektir; sınıf normal bir
komşu dosya olarak yazılsaydı, çalışan kopyanın bayt kodunu ayrıca okumak için yine aynı
yoldan geçmek gerekirdi. Okunan şey, yalnız aynı kaynaktan tekrar derlenmiş bir kopya değil,
gerçekten üç yüz bin kez çağrılmış olan sınıfın kendisidir.
Çağrı Yerinden Geçen Alıcı Tipi: Tek Alıcılıdan Çok Alıcılıya
Bir kararın dayanacağı bir veri olması gerekir, ve o veri de bir yerden gelir. Bir arayüz
üzerinden yapılan her çağrı — bir çağrı yeri (call site) — zamanla hangi somut tiplerle
karşılaştığının izini biriktirebilir. Bu ders o izi kendi sayacıyla tutar: bir çağrı
yerinden geçen alıcıların kaç ayrı tipten geldiğini getClass() kimlikleriyle sayar. Bir
çağrı yeri yalnız tek bir tiple karşılaştıysa tek alıcılı, birden çok tiple karşılaştıysa
çok alıcılı sayılır.
Bu ayrımın önemi şuradan gelir: Islenebilir i referansının sınıf dosyasındaki karşılığı
i.deger() çağrısıdır ve bu çağrı hangi somut sınıfı taşıdığını derleme zamanında
söylemez — TipA da olabilir, TipB de. Bir çağrı yeri gerçekte hep aynı tek tiple
karşılaşıyorsa, o bilgi bir hızlandırma fırsatıdır: her çağrıda “hangi sınıf bu” sorusunu
yeniden sormak yerine, geçmişte hep aynı yanıtın geldiği varsayılabilir. Ama bu varsayım
sınıf dosyasının bir parçası değildir — çünkü sınıf dosyası derlendiği anda, çağrı yerinin
çalışma zamanında kaç ayrı tiple karşılaşacağı henüz bilinmez. Islenebilir arayüzünün kaç
gerçekleştirimi olduğu da bu bilgiyi vermez: bir arayüzün on gerçekleştirimi olabilir, ama
tek bir çağrı yeri koşum boyunca bunlardan yalnız birini görebilir; alıcı sayısı arayüzün
kendi özelliği değil, o çağrı yerinin geçmişte gördüklerinin bir özetidir.
- JV19 — Çağrı yerinden geçen alıcı tip sayısı kendi kurduğumuz bir kümeyle
(
getClass()kimlikleriyle) sayılır; bu, çalışma zamanının kendi profil düzeneğinin değil, o düzeneği modelleyen kendi sayacımızın sonucudur.
// Islenebilir.java — cagri yerinden gecen alicilarin ortak arayuzu
interface Islenebilir { int deger(); }
// TipA.java — cagri yerindeki ilk alici tip
class TipA implements Islenebilir { public int deger() { return 1; } }
// TipB.java — cagri yerine sonradan katilan ikinci alici tip
class TipB implements Islenebilir { public int deger() { return 2; } }
// CagriYeri.java — arayuz uzerinden tek bir cagri yeri
class CagriYeri { static int cagir(Islenebilir i) { return i.deger(); } }
// AliciTipi.java — cagri yerinden gecen alici tip sayisi kendi sayacimizla sayilir
import java.nio.file.Path;
import java.util.*;
public class AliciTipi {
public static void main(String[] args) throws Exception {
Set<Class<?>> gorulenTipler = new HashSet<>();
TipA a = new TipA();
int cagriSayisi = 100_000;
for (int i = 0; i < cagriSayisi; i++) {
gorulenTipler.add(a.getClass());
CagriYeri.cagir(a);
}
System.out.println("cagri sayisi (yalniz TipA) : " + cagriSayisi);
System.out.println("tek alicili cagri yeri : " + (gorulenTipler.size() == 1));
TipB b = new TipB();
gorulenTipler.add(b.getClass());
CagriYeri.cagir(b);
System.out.println("ikinci gerceklestirim yuklendi : " + (gorulenTipler.size() == 2));
System.out.println("cok alicili cagri yeri (varsayim gecersiz): " + (gorulenTipler.size() > 1));
String kaynak = """
interface Islenebilir { int deger(); }
class CagriYeri { static int cagir(Islenebilir i) { return i.deger(); } }
""";
Path dizin = Olcek.derle(kaynak, "CagriYeri");
System.out.println("cagir yonteminin komut listesi : " + Olcek.komutlar(dizin, "CagriYeri", "cagir"));
}
}
cagri sayisi (yalniz TipA) : 100000 tek alicili cagri yeri : true ikinci gerceklestirim yuklendi : true cok alicili cagri yeri (varsayim gecersiz): true cagir yonteminin komut listesi : [aload_0, invokeinterface, ireturn]
Yüz bin çağrı boyunca CagriYeri.cagir yalnız TipA örnekleriyle karşılaştı; sayaç tek
kaldı. TipB’nin ilk örneği çağrı yerinden geçtiği an sayaç ikiye çıktı ve çağrı yeri çok
alıcılı oldu. Gerçek çalışma zamanı, tek alıcılı bir çağrı yerini bu bilgiye dayanarak
hızlandırabilir — alıcının hep aynı tip olacağını varsayarak; ikinci tip ortaya
çıktığında bu varsayım geçersizleşir ve çalışma zamanı optimizasyondan döner. Bu ders bu
dönüşü gerçek bir olay olarak yakalamaz (bunu yapmak gerçekleştirime özgü bir araç
gerektirirdi); yalnız varsayımı geçersiz kılan koşulu — ikinci tipin çağrı yerine katılışını
— kendi sayacıyla modeller. Alttaki cagir yönteminin komut listesi ise iki durumda da
aynıdır: invokeinterface komutu hangi somut tipin geleceğini söylemez, yalnız “bir
Islenebilir çağır” der; alıcı sayısının bir mi iki mi olduğu karar sınıf dosyasında hiçbir
iz bırakmaz.
Kararın dayandığı bu veriyi kim üretti? Kod, gorulenTipler kümesine her çağrıdan önce
kendi ekleme yaptı — bu ekleme çağıran iş parçacığının kendi kodunun bir parçasıdır,
ayrı bir taraf tarafından değil. Gerçek çalışma zamanının profil verisi de aynı ilkeyle
üretilir: bir çağrı yerinden geçen her çağrı, kararı verecek tarafa (çalışma zamanının
kendisine) veri bırakır, ve bu veriyi biriktiren adımın kendisi çağıran iş parçacığının
yürüttüğü kodun bir parçasıdır. Kararı veren taraf çağıran değildir, ama kararın
dayanacağı veriyi üreten çağırandır.
Bu ayrım önceki dersteki “kim koşturuyor” ile “kim görüyor” ayrımına benzer, ama aynısı değildir. Orada bir değeri yazan ile onu gören iki ayrı iş parçacığıydı; burada veriyi üreten (çağıran) ile o veriye dayanarak karar veren (çalışma zamanı) iki ayrı taraftır, ikisi de aynı iş parçacığı kavramıyla anılamaz — çünkü kararı veren taraf bu derste hiçbir zaman bir iş parçacığı kimliğiyle gözlemlenmez, yalnız sınıf dosyasında bulunmayışıyla tanınır.
Sınırlayıcı Ölçüm: Sıcak Yol Bir Süre İddiası Değildir
Bu derste hiçbir yerde süre ölçülmedi ve ölçülemez: üç yüz bin çağrının ne kadar sürdüğü, tek alıcılı ile çok alıcılı bir çağrı yerinin hangisinin daha hızlı koştuğu — bunların hiçbiri bu derste yazılmıyor, çünkü ikisi de çalıştığı makineye, o anki yüke ve gerçekleştirime bağlıdır. Sayılan tek şey komut sayısı ve alıcı tip sayısıdır; ikisi de programın kendi ürettiği, koşumdan koşuma sabit kalan verilerdir.
Sıcak yol terimi bu yüzden bir hız iddiası değil, çağrı yoğunluğuyla tanımlı bir
bölgedir: bir yöntem çok çağrıldığı için sıcaktır, hızlı koştuğu için değil. Yukarıdaki
ölçüm uygula yöntemini üç yüz bin kez çağırarak onu yoğun bir çağrı bölgesine soktu, ama
bu derste ne çağrı öncesi ne çağrı sonrası hiçbir süre karşılaştırması yapılmadı — yalnız
çağrı sayısı ve sınıf dosyasının değişmediği sayıldı. Sıcaklığın bir hız kazancına
dönüşüp dönüşmediği bu dersin sorusu değildir.
Bu sınır iki yönde de işler. Bir yandan, üç yüz bin çağrının “yavaş” ya da “hızlı” olduğuna dair hiçbir iddia yazılmadı — tek iddia, sınıf dosyasının değişmediğiydi. Öte yandan, tek alıcılı bir çağrı yerinin çok alıcılı olandan “daha hızlı” koştuğuna dair de hiçbir iddia yazılmadı — tek ölçülen, alıcı sayısının bir mi iki mi olduğuydu. İki ölçüm de “ne kadar hızlı” sorusuna hiç yaklaşmadan, yalnız “kaç” ve “aynı mı” sorularına yanıt verdi; bu kursun ölçüm disiplini burada da aynen geçerlidir.
Özet
- Sınıf dosyası derlendiği andan sonra sabittir: bir yöntem üç yüz bin kez çağrılsa da komut listesi çağrı öncesi ve sonrası birebir aynı kalır.
- Bir yöntemin ne zaman ve nasıl hızlandırılacağı kararı sınıf dosyasında hiç yoktur; karar çalışma zamanında, programın kendisinin oluşturmadığı bir yerde doğar.
- Bir çağrı yeri, geçen alıcıların kaç ayrı tipten geldiğine göre tek alıcılı ya da çok alıcılı sayılır; ikinci bir gerçekleştirim çağrı yerine katıldığında tek alıcılı varsayım geçersizleşir — bu ders bu geçersizleşmeyi (optimizasyondan dönüşü) kendi sayacıyla modeller, gerçek bir olay olarak yakalamaz.
- Kararın dayanacağı veriyi (alıcı tip sayısını) üreten taraf çağıran iş parçacığıdır; kararı veren taraf ise çağıran değildir.
- Bir çağrı yerinin alıcı sayısı bir mi iki mi olursa olsun, o çağrıyı taşıyan yöntemin komut listesi sınıf dosyasında değişmez.
- Sıcak yol çağrı yoğunluğuyla tanımlıdır, süreyle değil; bu ders hiçbir süre ölçmez, yalnız komut sayısını ve alıcı tip sayısını sayar.
Sonraki Adım
Bu konu boyunca sınıf dosyasında bulunmayan üç karar görüldü: sınıfı hangi yükleyicinin
tanımladığı ve hangi iş parçacığının başlattığı, erişilemez bir nesneyi kimin topladığı, ve
bir yöntemi ne zaman hızlandıracağı. Üçünde de kararı veren taraf programın kendisi
değildi — ne kaynak metninde, ne de ondan derlenen sınıf dosyasında hiçbir yerde
bulunamadı. Ama kodu koşturan iş parçacıklarını oluşturan yine programın kendisidir:
new Thread(...) satırını programın kendisi yazar, start() çağrısını programın kendisi
yapar. Sıradaki konu oradan başlar — bir iş parçacığı programın içinden nasıl var ediliyor,
ve gövdesini hangi taraf koşturuyor?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.