Showing posts with label Fork/Join Framework. Show all posts
Showing posts with label Fork/Join Framework. Show all posts

Tuesday, September 16, 2014

Java 8'in Çok Çekirdekli Programlama Başarımı

Java 8'in, Mart 2014'de duyurulmasının ardından 6 ay geçti. Şu an JDK'nın en güncel sürümü 8u20. Artık platform olgunlaşmaya başladı. Başarımı test etmenin tam zamanı. Java 8 ile pek çok yenilik ile tanıştık. Detaylar için bu makaleye göz atmanızda fayda var. Java 8'deki yeniliklerin ortak hedefi, çok çekirdekli platformlarda yüksek başarım elde etmek. Uygulama başarımı tek boyutlu bir büyüklük olarak ölçülemez. Birim zamanda çıkarılan iş, yanıt süresi, bellek ayak izi gibi birden fazla ölçüte bakmak gerekebilir. Üstelik bu ölçütler arasında bir ödünleşim (=trade-off) de vardır. Hepsini birden iyileştirmek mümkün olmayabilir.  Bu yazıda, Java 8 platformu üzerinde veri paralelliği farklı yöntemlerle gerçeklenecek ve karşılaştırması yapılacaktır. Temel olarak üç farklı yöntem izlenecektir:
I. Callable İplikler (=Threads), Future ve Executor Kullanımı
Runnable ipliklerin en kötü yönü run() metodunun herhangi bir parametre alamaması ve ipliğin bir değer döndürememesidir. Bu yüzden görevi yaratan iplik ile sonucu üretecek iplik arasında mutlaka bir senkronizasyon kurmak gerekir. Java SE 5 ile gelen Callable arayüzü ile artık sonucu call() metodundan dönmek ve Future arayüzünün get() çağrısı ile bu sonuca asenkron bir şekilde ulaşmak mümkündür. Java SE 5 ile gelen yeniliklerden biri de İplik havuzudur (=Thread Pool). Farklı havuz türleri ihtiyaca göre seçilebilir: Her zaman  sabit sayıda ipliğin yer aldığı Fixed Thread Pool, Parlamalı çalışma modu için Cached Thread Pool, Yığın işler için Single Thread Pool, Periyodik işler için Scheduled Thread Pool. Ancak buradaki tüm yapılar alt düzeydedir. Bu yüzden bu yapı üzerinde uygulama geliştirmek vakit alır, test etmesi zordur, yarış durumları (=race conditions) ve ölümcül kilitlenme (=deadlock) gibi durumların iyi düşünülmüş olması gerekir.
II. Fork/Join Çatısısnın (=Framework) Kullanımı
Java SE 7 ile gelen Fork/Join çatısı temelde İş Çalma (=Work Stealing) algoritmasına dayanır. Java 7'deki Fork/Join çatısı kullanımına ilişkin detayları bu yazıdan okuyabilirsiniz. Fork/Join çatısı I'e göre kodlaması daha kolay olsa da hala alt düzey bir API sunmaktadır. Üstelik problemi iki alt parçaya ayırmak ile seri olarak çözümü arasında genellikle veri boyutu üzerinden verilmesi gereken bir karar vardır.
III. Dönüştür-İndirge (=Map-Reduce) Çatısının Kullanımı
Dönüştür-İndirge çatısı Java SE 8 ile gelmiştir ve alt tarafta Fork/Join çatısını kullanır. Collection API ile hazır olarak gelen paralel kaplar ve Dönüştür-İndirge çerçevesi kullanılarak, çekirdek sayısına göre ölçeklenebilir çözümlere hızlıca ulaşabilir.

Örnek Problemin Tanımı

Karşılaştırmada seçilen problem uzayını tanıyalım:
public class Programmer implements Serializable {

    private int id;
    private String name;
    private String surname;
    private int age;
    private ProgrammingLanguage programmingLanguage;
    . . .
}

public class ProgrammingLanguage implements Serializable {

    private String name;
    private int level;
    . . .
}

Önce 60 milyon Programmer sınıfından rastgele nesne yaratıp bir ArrayList'in içine atıyoruz. Amacımız bu listenin içinden 40 yaşın üstünde "en bilgili" Java programcısını bulmak. Bunun için yukarıda tanımladığımız üç yaklaşımı da kullanacağız ve süreleri karşılaştıracağız.

Seri Çözüm

Çözümün doğruluğunu sınamak ve hızlanmayı karşılaştırabilmek için problemi seri olarak çözmek uygun olur:

private static long serialSolve(List<Programmer> list) {
        long start = System.nanoTime();
        Programmer oldest = null;
        for (Programmer programmer : list) {
            if (programmer.getAge() > 40 && programmer.getProgrammingLanguage().getName().equalsIgnoreCase("java")) {
                if (oldest == null) {
                    oldest = programmer;
                } else if (programmer.getProgrammingLanguage().getLevel() > oldest.getProgrammingLanguage().getLevel()) {
                    oldest = programmer;
                }
            }
        }
        System.err.println("Oldest [Serial]: " + oldest);
        long stop = System.nanoTime();
        return (stop - start);
    }

Callable İplik ve Future Kullanımı

private static long parallelSolveCallableThread(List<Programmer> list) throws InterruptedException {
        long start = System.nanoTime();
        Programmer oldest = null;
        int numberOfSegments = DATA_SIZE / SERIAL_THRESHOLD;
        ExecutorService es = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
        List<Callable<Programmer>> callables = new ArrayList<>(numberOfSegments);
        for (int i = 0, j = 0; i < numberOfSegments; ++i, j += SERIAL_THRESHOLD) {
            callables.add(new ProcessingThread(j, SERIAL_THRESHOLD, list));
        }
        List<Future<Programmer>> partialSolutions = es.invokeAll(callables);

        try {
            oldest = partialSolutions.get(0).get();
            for (int i = 1; i < partialSolutions.size(); ++i) {
                Programmer programmer = partialSolutions.get(i).get();
                if (programmer.getProgrammingLanguage().getLevel() > oldest.getProgrammingLanguage().getLevel()) {
                    oldest = programmer;
                }
            }
        } catch (InterruptedException | ExecutionException ex) {
            Logger.getLogger(PerformanceTest.class.getName()).log(Level.SEVERE, null, ex);
        }
        System.err.println("Oldest [Callable Thread]: " + oldest);
        long stop = System.nanoTime();
        es.shutdown();
        return (stop - start);
    }

public class ProcessingThread implements Callable<Programmer> {

    private final int start;
    private final int length;
    private final List<Programmer> list;

    public ProcessingThread(int start, int length, List<Programmer> list) {
        this.start = start;
        this.length = length;
        this.list = list;
    }

    @Override
    public Programmer call() throws Exception {
        Programmer oldest = list.get(start);
        Programmer programmer;
        for (int i = start + 1, j = 1; j < length; ++j, ++i) {
            programmer = list.get(i);
            if (programmer.getAge() > 40 && programmer.getProgrammingLanguage().getName().equalsIgnoreCase("java")) {
                if (programmer.getProgrammingLanguage().getLevel() > oldest.getProgrammingLanguage().getLevel()) {
                    oldest = programmer;
                }
            }
        }
        return oldest;
    }

}

Fork/Join Çatısının Kullanımı

private static long parallelSolveForkJoin(List<Programmer> list) {
        long start = System.nanoTime();
        ForkJoinPool pool = new ForkJoinPool();
        ForkListProcessing flp = new ForkListProcessing(0, list.size(), list);
        Programmer oldest = pool.invoke(flp);
        System.err.println("Oldest [FJ]: " + oldest);
        long stop = System.nanoTime();
        return (stop - start);
    }

public class ForkListProcessing extends RecursiveTask<Programmer> {

    private final int start;
    private final int length;
    private final List<Programmer> list;
    private static final int SERIAL_THRESHOLD = 5_000_000;

    public ForkListProcessing(int start, int length, List<Programmer> list) {
        this.start = start;
        this.length = length;
        this.list = list;
    }

    @Override
    public Programmer compute() {
        if (length <= SERIAL_THRESHOLD) {
            return serialSolver();
        } else {
            int startLeft = start;
            int lengthLeft = length / 2;
            int startRight = start + lengthLeft;
            int lengthRight = length - lengthLeft;
            RecursiveTask<Programmer> left = new ForkListProcessing(startLeft, lengthLeft, list);
            RecursiveTask<Programmer> right = new ForkListProcessing(startRight, lengthRight, list);
            left.fork();
            right.fork();
            Programmer leftSolution = left.join();
            Programmer rightSolution = right.join();
            if (leftSolution.getProgrammingLanguage().getLevel() >= rightSolution.getProgrammingLanguage().getLevel()) {
                return leftSolution;
            } else {
                return rightSolution;
            }
        }
    }

    private Programmer serialSolver() {
        Programmer oldest = list.get(start);
        Programmer programmer;
        for (int i = start + 1, j = 1; j < length; ++j, ++i) {
            programmer = list.get(i);
            if (programmer.getAge() > 40 && programmer.getProgrammingLanguage().getName().equalsIgnoreCase("java")) {
                if (programmer.getProgrammingLanguage().getLevel() > oldest.getProgrammingLanguage().getLevel()) {
                    oldest = programmer;
                }
            }
        }
        return oldest;
    }

}


Dönüştür-İndirge Çatısının Kullanımı

private static long parallelSolveMapReduce(List<Programmer> list) {
        long start = System.nanoTime();
        Programmer oldest = list.parallelStream()
                .filter((Programmer prg) -> prg.getAge() > 40 && prg.getProgrammingLanguage().getName().equalsIgnoreCase("java"))
                .reduce((left, right) -> left.getProgrammingLanguage().getLevel() >= right.getProgrammingLanguage().getLevel() ? left : right).get();

        System.err.println("Oldest [MapReduce]: " + oldest);
        long stop = System.nanoTime();
        return (stop - start);
    }

Deneysel Sonuçlar

Örnek uygulama 4 fiziksel çekirdeğin ve 8 sanal işlemcinin olduğu i7 işlemcili bir dizüstü bilgisayarda (Asus N56VZ-S4255H), Hotspot Java Sanal Makinası üzerinde koşturulmuştur. Kullanılan Hotspot parametreleri aşağıdaki gibidir: -Xms5096m -Xmx6096m -XX:+UseG1GC -Xcomp.
Sürelerin yer aldığı tabloyu aşağıda izleyebilirsiniz:
Serial Fork/Join Map-Reduce Callable Thread
2668505068 287335266 2944504598 371450954
1520665434 256402330 220530644 297969676
1466104840 248664714 216625912 344167024
1464082558 253262636 218372802 298162542
1516772250 255020642 239981168 302813920
1524937500 248614678 214424022 298976328
1466208756 248644184 213896322 371450954
1465564738 250441532 213332274 299036198
1478366830 384069164 211912956 299329982
1459489336 248061746 211580254 295550550
Tablodaki değerler yöntemin aldığı süreyi nano saniye biriminde vermektedir. Her bir satır yinelemeyi göstermektedir. Her bir yöntem 10 defa yinelenerek tekrar çalıştırılmıştır. Karşılaştırmayı daha kolay yapabilmek için aşağıdaki grafikten  yararlanabilirsiniz:
Aynı uygulamanın i7 işlemcili 2 fiziksel çekirdeği 4 sanal işlemcisi bulunan Samsung ultrabook (NP-900X4C)'daki başarımı aşağıdaki gibi gerçekleşmiştir:
Serial Fork/Join Map-Reduce Callable Thread
19741743117 580408617 4701616707 626018473
1806017710 448612143 496646177 424259928
1848274359 417033783 502213371 484369695
1597769920 462913357 499692308 408970979
1610160940 416298935 486345985 423605953
1606404182 435899678 534527811 426688621
1625213014 408824009 486342701 626018473
1632420684 422062362 576433047 435159082
1680902197 592661288 489187262 724467204
1653464440 430231904 493916565 406734823
Yukarıdaki tabloyu daha kolay yorumlayabilmek için aşağıdaki grafikten yararlanabilirsiniz:

En iyi sonucu 8 sanal işlemcili makinada Map-Reduce ile 4 sanal işlemcili makinada Callable Thread kullanarak elde ediyoruz. Genel olarak sonuçların biri birine çok yakın olduğunu söyleyebiliriz. Map-Reduce'un farkını daha yüksek çekirdek sayılarında görebiliriz. Map-Reduce ile elde edilen en önemli kazanç geliştirme süresinin kısa olması, testinin ve bakımının kolay olmasıdır. Çözüm yaklaşık 10 satırlık bir kod parçasından oluşmaktadır. Burada Callable Thread ve Fork/Join çözümleri özellikle en iyilenmemiştir, ortalama başarımları karşılaştırılmaya çalışılmıştır. Kodun tamamına NetBeans projesi olarak bu adresten ulaşabilirsiniz.
View Binnur Kurt's profile on LinkedIn

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.