Monday, April 29, 2013

Java 7 ile gelen Böl-Katıl Çatısının Başarım Analizi

Geliştirdiğimiz uygulamaların yüksek başarımla çalışması hepimizin arzu ettiği bir durum. Ancak bu her zaman ulaşılması kolay bir hedef olmayabilir. Özellikle günümüzde yazılımdan beklentilerin sürekli arttığı göz önüne alındığında. Başarımı iyileştirmeyi mutlaka yazılım geliştirme sürecinin (müşteriden isteklerin alınması, sistem çözümleme, detaylı tasarım, mimari tasarım, gerçekleme, sınama, bakım) bir parçası haline getirmeliyiz. Başarımı en iyilemek yazılım ortaya çıktıktan sonra yapılan bir etkinlik olmamalı. Java uygulamalarının başarımının en iyilenmesi ise farklı katmanlarda çalışmayı gerektirir. Donanım katmanı, İşletim sistemi katmanı, Java Sanal Makinası (JSM) katmanı ve elbette uygulama katmanı. Burada yer alan her bir katmandaki başarımı artırmak için ne yazık ki elimizde sihirli bir değnek bulunmuyor. Donanım katmanındaki gelişmeler elimizi güçlendiriyor: Çok çekirdekli  mimariler, büyük bellekli bilgisayar sistemleri, daha yüksek kapasiteli cep bellekler (L0, L1, L2, L3), katı hal diskler, daha hızlı ara bağlaşım birimleri, çok çekirdekli yüksek başarımlı ekran kartı işlemcileri. İşletim sistemleri bu donanım kaynaklarını uygulamalar ve kullanıcılar arasında verimli bir şekilde dağıtmaktan ve yönetmekten sorumlu çok sayıda servisten oluşuyor: proses sıralayıcı, sanal bellek yönetimi, giriş/çıkış yönetimi, kullanıcı yönetimi, dosya sistemi, pencereleme sistemi. Linux (Red Hat, Suse), Unix (Oracle Solaris, IBM AIX, HP-UX), Microsoft Windows gibi işletim sistemleri bu güncel sistem kaynaklarını iyi yönettikleri söylenebilir. Yine de genel amaçlı işletim sistemi olmalarından kaynaklanan bazı darboğazlar yaşıyorlar. 
Java uygulamaları doğrudan işletim sistemi üzerinde çalışmazlar. Bir sanal makinaya ihtiyaç duyarlar: Java Sanal Makinası (JSM). JSM'nin görevi, kabaca, Java uygulamalarını oluşturan bytecode olarak isimlendirilen makina komutlarını, üzerinde çalıştığı işletim sisteminin ve işlemcinin anlayacağı ve çalıştırabileceği forma çevirmektir. Java platformunun en güçlü tarafının JSM olduğu söylenebilir. JSM bu dönüşümü yaparken devingen en iyileme yapabilmektedir. C/C++'da kod yazdığınızda ise derleyici ancak durağan en iyileme yapabilir. Durağan en iyileme ise başarımda kısıtlı bir iyileşme sağlayabilir. JSM içinde başarımı belirlemede önemli işlevi olan iki bileşen yer alır: Tam Anında Derleme ve Çöp Toplayıcı. Java uygulamaları çalışmaya başladıklarında yorumlamalı olarak çalışırlar. JSM uygulamayı çalıştırırken kesitini de alır. Eğer bir iyileştirme görüyorsa sınıf metotlarını bytecode'dan işlemcinin doğrudan çalıştırabileceği doğal komutlara dönüştürür.
JSM satın alabileceğiniz, üretebileceğiniz bir işlemci tanımlar. Bu işlemcinin bir komut kümesi, yığın temelli adresleme kipleri, saklayıcı kümesi, yığın göstergesi, program sayacı, bellek modeli vardır. Çoğu zaman JSM'yi yazılımsal olarak ediniriz. Oracle, hemen hemen tüm işletim sistemleri için bir JSM yayınlıyor. Bu adresten elinizdeki işletim sistemi için bir JRE ya da JDK indirebilirsiniz. Eğer amacınız sadece Java uygulamalarını çalıştırmak ise Java Runtime Environment (JRE) yeterli olacaktır. Eğer uygulama geliştirmek isterseniz Java Geliştirme Çantasını (Java Development Kit, JDK) indirmeniz gerekir. Şu an Javanın 7 sürümü var. 2014 yılında 8 sürümünün çıkmasını bekliyoruz. Her yeni sürümde dilde bazı eklentiler, mevcut API'lerde değişiklikler ve yeni API'ler ile tanışıyoruz. Bu arada yeni sürümlerde bazen JSM'de de değişiklikler olabiliyor. Şu ana kadar ki yeni sürümlerde Java 1.2 ve Java SE 5'de dilde ciddi sayılabilecek yenilik geldi. Java 7 ile dilde gelen yenilikler daha çok geliştiricinin kodlama başarımını iyileştiren türden. Ancak Java 7'de JSM'de başarımı artırabilecek bazı gelişmeler de var:

  1. Sınıf yükleyici artık çok iş parçacıklı. Bu sınıfların eş zamanlı olarak yüklenebilmesine olanak sağlıyor. Böylelikle çok parçacıklı uygulamaların açılış zamanı başarımı bir miktar iyileştirecektir. 
  2. invokedynamic. Uzun bir aradan sonra JSM yeni bir komuta kavuştu. Bu daha çok özellikle dinamik tipli dillerin JSM üzerinde doğal olarak çalışabilmesini amaçlıyor. Dinamik tipli diller ile yazılan uygulamalar Java 6'da Scripting API (JSR 223) aracılığı ile zaten çalıştırılabiliyordu. Ancak bu Reflection API ile sağlandığından yavaştı. Şimdi Ruby gibi dillerle yazılan uygulamaların JSM üzerinde Scripting API gibi ara bir çözüme ya da katmana ihtiyaç duymadan doğal olarak ve yüksek başarımla çalışmasını bekliyoruz.
  3. -XX:-TieredCompilation: JSM içinde iki tür tam anında derleyici bulunur: Sunucu tipi ve İstemci tipi. JSM sunucu tipinde ise derleyici uygulamanın birim zamanda daha çok iş çıkarmasını sağlayacak şekilde davranır ve daha çok en iyileme seçeneği açıktır. JSM istemci tipinde ise tam anında derleyici, uygulamanın açılış zamanını iyileştirecek şekilde davranır. Java 7 ile gelen TieredCompilation seçeneğinde ise sunucu ve istemci tipindeki JSM'lerin iyi özelliklerinin birleştirilmeye çalışıldığını görüyoruz.
  4. -XX:+UseG1GC: Java 7'de yeni bir çöp toplayıcımız var: Garbage First (G1). G1 artımsal, paralel, kuşaklara dayalı, eş zamanlı, uygulamanın tüm iş parçacıklarını durdurduğu bir fazı olan bir çöp toplayıcı. Java 6 v öncesindeki (Mostly) Concurrent Mark-Sweep (CMS) yerini alması amaçlanıyor. CMS'ye göre iki önemli üstünlüğü bulunuyor: i) parçalanma oluşmuyor ii) çöp toplama ve duraksatma süresi için bir üst sınır verilebiliniyor. CMS ile G1 karşılaştırması ve G1'in detayları için bu sunumu okuyabilirsiniz.
  5. Çok çekirdekli mimariler üzerinde programlama: Java 7 Böl/Katıl çatısı olarak adlandırılan çok çekirdekli sistemlerde uygulama geliştirmek için bir çözüm sunuyor. Bu çözüm, Concurrency API ile gelen yeni bir iş parçası havuzu olan ForkJoinPool ve bu havuza atayacağımız iş parçacıklarını tanımladığımız RecursiveAction ve RecursiveTask sınıflarını kullanmaktadır. 
Bu yazıda Böl/Katıl çatısının çekirdek sayısına göre ölçeklenebilirliğini inceleyeceğiz. Örnek uygulama olarak görüntü işlemede kullanılan bulanıklaştırma işlemini seçtim. Kod aşağıda listelenmiştir:
1:  package com.example.forkjoin;  
2:  import com.example.util.ImageFrame;  
3:  import java.awt.image.*;  
4:  import java.io.*;  
5:  import java.util.concurrent.*;  
6:  import javax.imageio.*;  
7:  public class ForkBlur extends RecursiveAction {  
8:    protected static int BENCHMARK_LOOP_SIZE = 10;  
9:    protected static int SERIAL_THRESHOLD = 10_000;  
10:    private final int[] mSource;  
11:    private final int mStart;  
12:    private final int mLength;  
13:    private final int[] mDestination;  
14:    private int mBlurWidth = 15;  
15:    public ForkBlur(int[] src, int start, int length, int[] dst) {  
16:      mSource = src;  
17:      mStart = start;  
18:      mLength = length;  
19:      mDestination = dst;  
20:    }  
21:    protected void serialComputation() {  
22:      int sidePixels = (mBlurWidth - 1) / 2;  
23:      for (int index = mStart; index < mStart + mLength; index++) {  
24:        // Calculate the average.  
25:        float rt = 0, gt = 0, bt = 0;  
26:        for (int mi = -sidePixels; mi <= sidePixels; mi++) {  
27:          int mindex = Math.min(Math.max(mi + index, 0), mSource.length - 1);  
28:          int pixel = mSource[mindex];  
29:          rt += (float) ((pixel & 0x00ff0000) >> 16);  
30:          gt += (float) ((pixel & 0x0000ff00) >> 8);  
31:          bt += (float) (pixel & 0x000000ff);  
32:        }  
33:        rt = rt / mBlurWidth;  
34:        gt = gt / mBlurWidth;  
35:        bt = bt / mBlurWidth;  
36:        int dpixel = (0xff000000)  
37:            | (((int) rt) << 16)  
38:            | (((int) gt) << 8)  
39:            | ((int) bt);  
40:        mDestination[index] = dpixel;  
41:      }  
42:    }  
43:    @Override  
44:    protected void compute() {  
45:      if (mLength < SERIAL_THRESHOLD) {  
46:        serialComputation();  
47:        return;  
48:      }  
49:      int split = mLength / 2;  
50:      invokeAll(new ForkBlur(mSource, mStart, split, mDestination),  
51:          new ForkBlur(mSource, mStart + split, mLength - split, mDestination));  
52:    }  
53:    public static BufferedImage blur(BufferedImage srcImage,int blurWidth) {  
54:      int w = srcImage.getWidth();  
55:      int h = srcImage.getHeight();  
56:      int[] src = srcImage.getRGB(0, 0, w, h, null, 0, w);  
57:      int[] dst = new int[src.length];  
58:      ForkBlur fb = new ForkBlur(src, 0, src.length, dst);  
59:      fb.setBlurWidth(blurWidth);  
60:      ForkJoinPool pool = new ForkJoinPool();  
61:      long startTime = System.currentTimeMillis();  
62:      pool.invoke(fb);  
63:      long endTime = System.currentTimeMillis();  
64:      System.err.println("Image blur took " + (endTime - startTime) + " milliseconds.");  
65:      BufferedImage dstImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);  
66:      dstImage.setRGB(0, 0, w, h, dst, 0, w);  
67:      return dstImage;  
68:    }  
69:    public int getBlurWidth() {  
70:      return mBlurWidth;  
71:    }  
72:    public void setBlurWidth(int mBlurWidth) {  
73:      this.mBlurWidth = mBlurWidth;  
74:    }  
75:    public static void main(String[] args) throws Exception {  
76:      String filename = "src/red-tulips.jpg";  
77:      File file = new File(filename);  
78:      BufferedImage image = ImageIO.read(file);  
79:      BufferedImage blurredImage = null;  
80:      int processors = Runtime.getRuntime().availableProcessors();  
81:      System.err.println(Integer.toString(processors) + " processor"  
82:          + (processors != 1 ? "s are " : " is ")  
83:          + "available");  
84:      for (int k = 15; k < 64; k += 2) {  
85:        for (int j = 1; j <= 10; ++j) {  
86:          System.err.println("Threshold is " + SERIAL_THRESHOLD);  
87:          SERIAL_THRESHOLD = 1_000 * j;  
88:          for (int i = 0; i < BENCHMARK_LOOP_SIZE; ++i) {  
89:            blurredImage = blur(image,k);  
90:            System.gc();  
91:          }  
92:        }  
93:        SERIAL_THRESHOLD = 100_000_000;  
94:        for (int i = 0; i < BENCHMARK_LOOP_SIZE; ++i) {  
95:          blurredImage = blur(image,k);  
96:          System.gc();  
97:        }  
98:      }  
99:    }  
100:  }  

Problemi seri olarak çözmekle parçalamak arasındaki kararı SERIAL_THRESHOLD ile veriyoruz. Denemelerde SERIAL_THRESHOLD sabitinin değeri 10,000 olarak kullanılmıştır. Farklı boyutlarda bulanıklaştırma süzgeci kullanılmıştır. Böl/Katıl çatısı ve seri çözümlerin süzgeç boyutuna göre süzgeçleme süresi ve hızlanma aşağıdaki şekillerde gösterilmiştir:



Test olarak kullanılan imge aşağıda verilmiştir. İmge altı milyon (3,000x2,000) benekten oluşmaktadır.  Böl ve katıl çatısında problemi seri olarak çözmek daha etkin oluncaya kadar   bölmeye devam ediyoruz. Burada seri olarak çözme kararı için bir eşik değeri (SERIAL_THRESHOLD) kullanıyoruz. Eğer benek sayısı 10,000'in altında ise seri olarak çözmeye karar veriyoruz:
45:      if (mLength < SERIAL_THRESHOLD) {  
46:        serialComputation();  
47:        return;  

Buna göre elimizdeki test imgesi için 600 görev oluşacaktır. Bu görevler Java 7 ile gelen ForkJoinPool havuzundaki çekirdek sayısı kadar iş parçacığı tarafından çalıştırılacaktır. Testlerde 8 çekirdekli bir makina kullanılmıştır. Dolayısı ile havuzda 8 iş parçacığı bulunmaktadır. Bu iş parçacıklarının özelliği her birinin bir çekirdeğe bağlanmış olmasıdır.

Hızlanma oranı, süzgecin boyutu arttıkça işlemci sayısına yaklaştığı görülmektedir. Seri çözümde, katlama süresi süzgeç boyutu ile doğrusal bir şekilde artarken, Böl/Katıl çatısı ile Java 7'de oluşturulan çözümde ise neredeyse sabit kalmaktadır. Uygulamanın kaynak kodunu NetBeans 7 projesi olarak bu adresten indirebilirsiniz.




No comments:

Post a Comment