JAVA 8 LAMBDA İFADELERİNİN PERFORMANS ÖLÇÜMLERİ



Java 8 ile birlikte gelen Lambda metotlar birçokları tarafından Java 5 te gelmiş olan Generic ifadeler kadar kökten bir yenilik olarak nitelenmekte Java dünyası için. Öncelikle JVM üzerinde çalıştığı halde Java diline bir alternatif olarak hızla yükselen Scala dilinin başarılı yönlerinin Java diline katılması açısından çok önemli bir eklemedir Lambda metotlar. Java dünyasının belki de en büyük gücü açık kaynak felsefesi ile kendi ekosisteminde rekabeti körükleyerek bu rekabetten başarıyla çıkan alt kümelerini sonra yine ana akıma katmasıdır. Daha önce Spring uygulama çatısından öğrendikleriyle EJB 3 oluşturulmuştu. Şimdi de Joda-Time ile birlikte Scala dilinin en önemli fikirlerinden işlevsel programlamayı bünyesine katıyor Java son sürümü ile birlikte.

Scala dili elbette işlevsel programlama paradigması üzerinde ve bazı kodlama kolaylıkları ile geliştiricilerin dikkatini çekmekte gecikmedi. İşlevsel programlama aslında geçmişi 1950 lerdeki LISP diline kadar uzanan eski bir düşünce şekli ve nesneye yönelik yaklaşımla popülerliğinden biraz kaybetmişse de son zamanlarda çok kilit bir faydası ile ön plana çıkmakta: Paralel programlama. Paralel programlama da yine bilgisayarların ilk zamanlarından beri bilinen fakat son zamanlarda mikroişlemcilerin gelişimi itibariyle daha da önem kazanan bir kavram ve bir işi aynı anda ve paralel çalışacak alt işlere bölerek işi daha kısa sürede bitirmeyi hedeflemekte. Intel işlemcilerinin Pentium 4 serisi ile ilk defa tosladığı frekans bariyeri işlemcilerin gelişimini yeniden şekillendirdi. Moore yasasına benzer şekilde işlemci frekansları zamanla katlanarak arttı ve bir yerde öyle bir noktaya gelindi ki 5Ghz hızında çalışacak işlemciler muazzam elektrik tüketimine ve ısınmaya sebep olmakta ve artık pratik olmaktan çıkmaktaydılar. Üreticiler de bilindik bir numaraya başvurdular ve Moore yasasının öngördüğü şekilde madem aynı alana iki kat fazla transistör koyabiliyoruz öyleyse niye iki ayrı işlemciyi yan yana koymayalım dediler ve günümüzün 8,12 çekirdekli Intel işlemcileri ortaya çıktı. İşte bu mecburiyetten ötürüdür ki günümüzde paralel programlama giderek önem kazanmaktadır. Yazdığınız program 8 çekirdekten sadece birini etkili kullanabiliyorsa kalan 7 çekirdek için ödenen para boşa gitmiş demektir.

Java dili de aslında 1.0 sürümünden beri çoklu threadlerle çalışmayı kolaylaştıracak API ler içermekte ve kod örnekleri ile göstereceğim üzere kullanımı nispeten kolay bu API lerin. Elbette bu bahsettiklerim Java ilk çıktığı zaman C++ gibi diğer zamanın önemli dilleri ile kıyaslandığında Java'yı öne çıkaran yeniliklerdi. Aradan uzun bir zaman geçtikten sonra bu sefer geliştiricilerin işlerini daha da kolaylaştırmak mümkün oldu. Java Thread sınıfı ile bir çok threadi paralel çalıştırmak ve birbirleriyle iletişim kurmalarını kolaylaştırmayı amaçlamıştı. Bu zamanına göre oldukça güzel bir soyutlamaydı belki ama yine de insanların Thread kavramına aşina olmaları bekleniyordu. Kritik kaynaklar, senkronizasyon ve thread öncelikleri gibi bir çok alt konu da paketle geliyordu malesef.

Scala dilinde ise geliştiricilerin haklı olarak sordukları temel bir soru cevaplanmış oldu: “Biz bir nesne üzerindeki bir işlemi niye bir nesne kümesine uyguladığımızda, paralel çalışmak için threadler gibi işletim sistemi düzeyinde kavramlarla uğraşalım ki?” Sorunun haklılığı şuradan gelmekteydi ki asıl iş mantığı ile uğraşmayı bir kenara bırakıp geliştiriciler belki de iş mantığı kodundan fazla destekleyici kod yazmak zorunda kalıyorlardı. Bu soruya cevap bir nesne üzerinde yapılan işlemi soyutlayarak getirildi. Java ağırlıklı olarak nesneye yönelik geliştirmeyi destekler. Her şey bir nesnedir ve sınıfı olmayan metot olamaz. Dilin birinci sınıf vatandaşı değildir metotlar. Bir metoda başka bir metodu parametre olarak veremezsiniz ya da bir metot başka bir metodu geriye döndüremez. Metotları nesnelerden koparamadığımız için de bir metodu farklı sınıftan nesneler için aynı şekilde çalıştıramayız. Çünkü bir metot bir sınıfa aittir ve başka sınıfta kullanmak için o sınıfa da ayrıca tanımlamak gereklidir. Bunun yapmanın elbette bazı yolları vardır Java'da. Interface ler tanımlayarak ilgili sınıfların bunu uygulamaları zorlanabilir örneğin. Böylece tüm sınıflar için aynı metot gözü kapalı çalıştırılabilir ama bu yöntemler aynı işi yapan scala kodlarına göre hep çok daha fazla elle kodlama gerektirmişti şimdiye kadar.

Java 8 ile gelen Lambda metotları sayesinde ise metotları sınıfları kullandığımız gibi kullanma imkanımız doğdu. Örneğin:

for (Ürün ürün : ürünler) {
    System.out.println(ürün.hashCode() + " ");
}

yerine:

ürünler.stream().forEach((ürün) -> System.out.println(ürün.hashCode() + " "));

Benchmark Sonuçları

Lambda ifadeleri kullanarak basitçe paralel işleme ile ne oranda bir performans artışı kaydedilebileceği üzerine bir benchmark oluşturdum:

Çalıştırılan makine:

Samsung NP550P dizüstü, Intel i7-3630QM CPU @ 2.40GHz ( 4 çekirdek 8 thread )


12 GB RAM ve 120 GB Sandisk Ultra SSD disk


Linux Mint 17 KDE işletim sistemi üzerinde JDK 1.8_05 ile denendi.

Java programı -Xmx1024m parametresi ile çalıştırıldı.

Senaryo:


Ürünlerin olduğu bir liste içerisinde ürünlerin bazı özelliklerine göre metin araması yapılarak eşleşen sonuçları belirlemek üzerine bir senaryo oluşturuldu.


Belli sayılarda ürünler temsili olarak yaratılıp bir listeye eklendikten sonra bu listenin içinde bir anahtar kelime ile arama yaptırıldı farklı algoritmalar kullanılarak:


1) Doğrusal arama: Burada klasik yöntemle listenin ilk elemanından başlayarak tüm elemanlar tek tek kıyaslandı.

2) Paralel arama: Burada sırasıyla 2,4,8 paralel thread için listenin farklı bölümlerinde arama yaptırıldı ve bu threadlerin sonuçları tek bir sonuçta birleştirildi.

3) Lambda ile arama: Burada ise tamamen Java 8 in getirdiği Lambda desteği kullanıldı.


private Set<Ürün> lambdaylaAra(final List<Ürün> ürünler, final String anahtarKelime) {
String anahtarKelimeHepsiKüçükHarf = anahtarKelime.toLowerCase();
Set<Ürün> sonuç = Collections.synchronizedSet(new HashSet<LambdaÖlçüm.Ürün>());
ürünler.parallelStream().filter(ü -> ürünUygun(ü, anahtarKelimeHepsiKüçükHarf)).forEach((ürün) -> sonuç.add(ürün));
return sonuç;
}

private Set<Ürün> paralelAra(final List<Ürün> ürünler, final String anahtarKelime, int threadSayısı) throws InterruptedException {
class AramaThread extends Thread {
private int başlangıç;
private int bitiş;
private Set<Ürün> sonuçlar;
private final String anahtarKelimeHepsiKüçükHarf = anahtarKelime.toLowerCase();
public AramaThread(int başlangıç, int bitiş, Set<Ürün> sonuçlar) {
this.başlangıç = başlangıç;
this.bitiş = bitiş;
this.sonuçlar = sonuçlar;
}

@Override
public void run() {
for (int i = başlangıç; i < bitiş; i++) {
Ürün ürün = ürünler.get(i);
if (ürünUygun(ürün, anahtarKelimeHepsiKüçükHarf)) {
sonuçlar.add(ürün);
}
}
}
}
// Thread-safe olması için Vector seçildi
final Set<Ürün> sonuç = Collections.synchronizedSet(new HashSet<Ürün>());
AramaThread[] threadler = new AramaThread[threadSayısı];
for (int i = 0; i < threadSayısı; i++) {
int başlangıç = ürünler.size() * i / threadSayısı;
int bitiş = başlangıç + ürünler.size() / threadSayısı;
if (bitiş > ürünler.size()) bitiş = ürünler.size();
threadler[i] = new AramaThread(başlangıç, bitiş, sonuç);
threadler[i].start();
}
// tüm threadlerin sonlanmasını bekliyoruz
for (AramaThread thread : threadler) {
thread.join();
}
return sonuç;
}
private Set<Ürün> doğrusalAra(final List<Ürün> ürünler, final String anahtarKelime) {
String anahtarKelimeHepsiKüçükHarf = anahtarKelime.toLowerCase();
Set<Ürün> sonuç = Collections.synchronizedSet(new HashSet<Ürün>());
for (Ürün ürün : ürünler) {
if (ürünUygun(ürün, anahtarKelimeHepsiKüçükHarf)) {
sonuç.add(ürün);
}
}
return sonuç;
}

Sonuçlar:




*) Görünüşe göre lambda ile arama, çalışılan küme büyüdükçe daha etkili hale gelmekte. Bu da bir overhead varlığını çağrıştırıyor.

*) 400000 ürün ve sonrası için yapılan denemelerde tek thread aramada bariz performans farkı muhtemelen bir bellek yetersizliği ya da JIT'in yetersizliğinden kaynaklanmış olabilir.

*) 300000 ürün için 2 threadli çalışma süresi defalarca benzer davrandı ve bir regresyon durumu söz konusu bu durum için.

*) Görüldüğü üzere Thread sınıfını kullanarak 45 satırda yaptığımız işi lambda kullanarak 9 satırda başarabilmişiz.

400000 ürün ve sonrasını bırakırsak en fazla 4 kat civarı bir hızlanma sağlayabildik paralel çalışma ile. Bu da aslında 4 çekirdeğin Hyperthreading desteği ile de olsa tam 8 çekirdek gibi davranmadığını gösterdi. Yine de teorik olarak 4 çekirdekle 8 çekirdek arası bir performans yakalamış olduk. Benzer mantıkla çekirdek sayısı artırıldıkça performansın doğrusal olmasa da yine kabul edilir ölçüde artırılabileceğini görmüş olduk.

Java 8 ile birlikte sunulan Lambda ifadeleri çoğu Java yazılımcısı için daha kısa ve anlaşılır kodlar ve hamallıktan kurtulma şeklinde algılanacak belki ama performans tarafındaki katkıları ise özellikle işlemcilerde çekirdek sayıları arttıkça giderek daha ön plana çıkacaktır. Daha şimdiden 10, 15 çekirdekli Intel işlemcileri mevcut ve 80 çekirdekli prototiplerle çalışılmakta. Cep telefonlarımızda bile 8 çekirdekli işlemcilerin bulunabildiği günümüzde çok thread le kodlamaya hızlı bir giriş yapmak isteyenler için lambda ifadelerini öğrenmek önem kazanacaktır.

Yorumlar

Popüler Yayınlar