Wednesday, April 3, 2013

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

Son 6-7 yıldır artık çok çekirdekli sistemler üzerinde uygulama geliştiriyoruz. Ancak bir çok programlama dili bu çok çekirdekli sistemlerin sağladığı işlemci gücünü tam olarak kullanabilmeze olanak sağlayacak programlama yapılarını bize sunmuyor. Hala sanki sistemde tek bir işlemci varmış gibi uygulama geliştiriyoruz. Üstelik çoklu iş parçacıklı yapılar (örneğin Java'daki Thread) iş parçacıklarını çekirdeklere dağıtmayı garanti etmediğinden ya da dağıtım (örneğin POSIX Thread kütüphanesi (pthread) kullanıldığı için) verimli olmadığından istenilen sonucu vermiyor. Üstelik elimizdeki problemi  paralel olarak çözülebilir parçalara ayırıp, sonra her bir parçayı bir iş parçası yaratarak çözmeye çalıştığımızda, ölçeklenebilir olmayan bir mimariye sahip oluyoruz. Daha akıllı ve senkronizasyon maliyeti daha düşük çözümlere ihtiyacımız var. Java 7 Böl/Katıl çatısı olarak adlandırılan yeni bir çözüm sunarak çok çekirdekli sistemlerde uygulama geliştirmek için nihayet 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 görevleri tanımladığımız RecursiveAction ve RecursiveTask sınıflarını kullanmaktadır. Java 8'de ise bu yeteneğin, paralel kaplar ve Dönüştür-İndirge çerçevesini gerçekleştirebilmemize olanak sağlayan başta Lambda ifadeleri olmak üzere gelen yenilikler ile geliştirildiğini görüyoruz.
Java 8'de çok çekirdekli programlama için ilk karşılaştığımız yetenek paralel sıralama yapmamızı sağlayan Arrays yardımcı sınıfının parallelSort() metodudur. Aşağıda 5 milyon elemanlı bir tamsayı dizisinin paralel sıralanması ile kazanılan hızlanmayı ölçen bir kod yer alıyor:


package com.example.test;

import java.util.Arrays;
import java.util.Date;
import java.util.Random;


/**
 *
 * @author Binnur Kurt
 */
public class ParallelArrays {

    private static final int ARRAY_SIZE = 5_000_000;
    private static final int BENCHMARK_LOOP = 100;
    private static long start;
    private static long stop;

    public static void main(String[] args) {
        Random random = new Random(new Date().getTime());
        int[] orig = new int[ARRAY_SIZE];
        int[] work = new int[ARRAY_SIZE];
        for (int i = 0; i < ARRAY_SIZE; ++i) {
            orig[i] = random.nextInt();
        }
        for (int i = 0; i < BENCHMARK_LOOP; ++i) {
            System.arraycopy(orig, 0, work, 0, ARRAY_SIZE);
            start = System.nanoTime();
            Arrays.parallelSort(work);
            stop = System.nanoTime();
            System.err.println("Parallel Sort ["+(i+1)+"]: " + 
                               (stop - start));
        }
        for (int i = 0; i < BENCHMARK_LOOP; ++i) {
            System.arraycopy(orig, 0, work, 0, ARRAY_SIZE);
            start = System.nanoTime();
            Arrays.sort(work);
            stop = System.nanoTime();
            System.err.println("Serial Sort ["+(i+1)+"]: " + 
                               (stop - start));
        }
    }
}

Paralel sıralamada Java 7 ile gelen Böl/Katıl çatısı kullanılmaktadır. Henüz Java 8 son halini almamıştır, geliştirilmeye açık yönleri bulunmaktadır. Dolayısı ile Java 8 çıktığında başarımın bundan daha yüksek olması muhtemeldir. 4 çekirdekli 8 mantıksal işlemcisi bulunan i7 işlemcili bir dizüstü bilgisayarda çalıştırıldığında aşağıdaki şekilde gösterildiği gibi yaklaşık dört katlık (tam değer 4.48) bir hızlanma sağlanmıştır. Yatay eksen döngüdeki her bir sıralamayı, düşey eksen ise nano saniye biriminde geçen süreyi göstermektedir.




Java 8 ile birlikte sadece diziler üzerinde değil Collection API içinde yer alan kaplar üzerinde de paralel işler çalıştırmak mümkün olabilmektedir. Yukarıda diziler için yapılan karşılaştırmanın bir benzerini kaplar için yapan kod aşağıda verilmiştir:

package com.example.test;

import com.example.domain.Circle;
import java.awt.Color;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.stream.Stream;

/**
 *
 * @author Binnur Kurt
 */
public class ParallelCollections {

    private static final int ARRAY_SIZE = 5_000_000;
    private static final int BENCHMARK_LOOP = 100;
    private static long start;
    private static long stop;

    public static void main(String[] args) {
        Random random = new Random(new Date().getTime());
        List<Circle> orig = new ArrayList<>(ARRAY_SIZE);
        List<Circle> work = new ArrayList<>(ARRAY_SIZE);
        for (int i = 0; i < ARRAY_SIZE; ++i) {
            orig.add(new Circle(0, 0, random.nextDouble(), 
                     new Color(random.nextInt())));
        }
        Comparator<Circle> comp= 
            (Circle c1, Circle c2) -> 
               (c1.getRadius()-c2.getRadius())>=0.0 ? +1 : -1;
        for (int i = 0; i < BENCHMARK_LOOP; ++i) {
            work = new ArrayList<>(orig);
            System.gc();
            start = System.nanoTime();
            Circle circle = work.parallelStream()
                                .sorted(comp) 
                                .findFirst().get();
            stop = System.nanoTime();
          System.err.println("Parallel Collection: " + (stop - start));
        }
        for (int i = 0; i < BENCHMARK_LOOP; ++i) {
            work = new ArrayList<>(orig);
            System.gc();
            start = System.nanoTime();
            Collections.sort(work);
            Circle maxCircle = work.get(0);
            stop = System.nanoTime();
       System.err.println("Serial Collection: " + (stop - start));
        }
    }
}

Yukarıdaki kod aynı dizüstü bilgisayarda çalıştırıldığında aşağıdaki şekilde gösterildiği gibi yaklaşık dört katlık bir hızlanma sağlanmıştır. Bu sefer hızlanma dizilere göre bir miktar daha iyidir. Tam rakam 4.74'dür. Yatay eksen döngüdeki her bir sıralamayı, düşey eksen ise nano saniye biriminde geçen süreyi göstermektedir.




Üçüncü yenilik ise Dönüştür-İndirge çatısını gerçekleştirebilmemize olanak sağlayan Collection API'deki gelişmelerdir. Şimdi bu çatı kullanılarak yazılmış bir kodu inceleyeceğiz:

package com.example.test;

import com.example.domain.Circle;
import java.awt.Color;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;

/**
 *
 * @author Binnur Kurt
 */
public class MapReduce {

    private static final int ARRAY_SIZE = 5_000_000;
    private static final int BENCHMARK_LOOP = 100;
    private static long start;
    private static long stop;

    public static void main(String[] args) {
        Random random = new Random(new Date().getTime());
        List<Circle> orig = new ArrayList<>(ARRAY_SIZE);
        List<Circle> work = new ArrayList<>(ARRAY_SIZE);
        for (int i = 0; i < ARRAY_SIZE; ++i) {
            orig.add(new Circle(0, 0, random.nextDouble(), new Color(random.nextInt())));
        }
        work = new ArrayList<>(orig);        
        for (int i = 0; i < BENCHMARK_LOOP; ++i) {
            start = System.nanoTime();
            double sum = work.parallelStream()
                             .filter(c -> c.getRadius() > 0.5)
                             .map(c -> c.area())
                             .reduce(Double::sum).get();
            stop = System.nanoTime();
         System.err.println("Parallel Collection: " + (stop - start));
            work.clear();
            work.addAll(orig);
            System.gc();
        }
        work.clear();
        work.addAll(orig);
        System.gc();
        for (int i = 0; i < BENCHMARK_LOOP; ++i) {
            start = System.nanoTime();
            double sum = 0.0;
            for (Circle c : work) {
                if (c.getRadius() > 0.5) {
                    sum += c.area();
                }
            }
            stop = System.nanoTime();
       System.err.println("Serial Collection: " + (stop - start));
        }
    }
}

Kaplar üzerinde yapılan tarama ve hesaplama işleri hem dış yineleyici kullanıldığından hem de seri olarak gerçekleştirildiğinden yavaştır. Java 8 dönüştür ve indirge çatısı paralel Stream soyutlamasını kullandığı için aynı iş hattı yapısına benzer şekilde dönüştürme (=map) ve indirgeme (=reduce) işlemlerininin paralel olarak gerçeklenmesine olanak sağlamaktadır. Bu çatının kullanıldığı yukarıdaki çözümde, bu kez hızlanma daha düşük seviyede gerçekleşti. Ancak 3.73'lük bir hızlanma sağlanabildi:



Bu sonuçlardan rahatlıkla görüleceği gibi Java 8 platformunun çok çekirdekli sistemler üzerinde önemli bir hızlanma sağlaması beklenmelidir. Üstelik bu iyileştirmeden, mevcut uygulamalarımız, kodda fazla bir değişiklik yapılmaksızın yararlanabilir. Ancak bu hızlanmanın ancak uygulamamıza adanmış bir sistemde sağlanabileceğini unutmayın. Çünkü tüm çekirdekleri kullandığımızda diğer uygulamaların işlemci zamanından çalıyoruz. Dolayısı ile diğer uygulamalar için bu durum başarımın düşmesi anlamına gelir.
Test kodlarını NetBeans 8 projesi olarak bu adresten indirebilirsiniz.

1 comment:

  1. Güzel makale. Concurrency ve parallelism'e değinen nadir yazılardan olmuş. Elinize sağlık.

    ReplyDelete