Showing posts with label Multi-core. Show all posts
Showing posts with label Multi-core. Show all posts

Tuesday, September 16, 2014

Java 8'deki MapReduce Çatısının Başarımının C++11 ile Karşılaştırılması

Java SE 8 ile gelen yenilikler ile birlikte artık C++11 ile Java 8'in karşılaştırılabilir konuma geldiğini söyleyebiliriz. C++11 ve Java 8 artık her ikisi de birer fonksiyonel programlama yapabildiğimiz dillere dönüştüler. C++11 ile nihayet Thread tabanlı programlama dilin parçası haline geldi. Java'nın en güçlü yönü Java programlama dili değil, Java platformudur. Yazılımın başarımını etkileyen bir çok farklı etmen bulunur: yazılımın mimarisi, donanım, işletim sistemi. Java'nın öğündüğü "bir kere yaz her yerde çalışsın" söylemi Hotspot sayesinde gerçekleşir. Her yerde çalışır ama aynı başarımda değil. Örneğin Hotspot'un Linux platformlarda Windows işletim sistemine göre daha yüksek başarım sergilediğini biliyoruz. Hotspot'un en güçlü yönü, çalışma zamanında, Java derleyicisinin bytecode'a dönüştürdüğü ikili kodu üzerinde çalıştığı işlemcinin doğrudan çalıştırabileceği doğal makina kodlarına dönüştürebilmesidir. Bunu yaparken çeşitli iyileştirmeler de yapar. Bu iyileştirmelerin bir listesi için bu adrese göz atabilirsiniz. Hotspot'un içinde basit bir profiler yer alır ve çalışan uygulamanın şeklini almasını bekler ve aldığı şekle uygun en iyilemeler yapar. Hatta bu en iyilemeleri bazen şartlar artık geçerli olmadığı için geriye aldığı da olur. Buna karşılık C++'da derleyicinin yapabildiği iyileştirmeler derleme aşamasında gerçekleşir ve bu nedenle kısıtlıdır. Eğer GNU derleyici kullanıyorsanız iyileştirmeleri en üst düzeyde açmak için -O3 seçeneğini vermeniz gerekir.
Bu yazıda C++11'in dil özellikleri ile Java 8'in dil özellikleri karşılaştırılmayacaktır. Bunun yerine, büyükçe bir dizinin elemanlarının toplamını hesaplamak gibi basit problemi her iki dilde de paralel olarak çözmeye çalışarak başarımı karşılaştırılacaktır. Her iki dilde de karşılaştırma yapabilecek yetkinliğe sahip olduğum söylenebilir.
Önce C++11 ile başlayalım:
#include <iostream>
#include <thread>
#include <future>
#include <algorithm>

using namespace std;

const int SIZE=80000000;
int numbers[SIZE];

template <typename iter>
void init_problem(iter beg,iter end){
    int i=1;
 for (iter p=beg;p!=end;p++,++i)
     *p= i;
}

template <typename iter>
int parallel_sum(iter begin, iter end){
    long len= distance(begin,end);
    if (len <= 10000000){
    return accumulate(begin,end,int()); 
 }  
    iter mid= begin + len /2;
    auto handle_left= 
    async(launch::async,parallel_sum<iter>,begin,mid);   
    auto handle_right= 
    async(launch::async,parallel_sum<iter>,mid,end);   
    return handle_left.get()+
        handle_right.get();
}

int main(){
 init_problem(numbers,numbers+SIZE);
 int sum= parallel_sum(numbers,numbers+SIZE);
 cout << "Sum (parallel): " << sum << endl ; 
}
Kodda karşılaştığınız auto, async, future C++11 ile gelen yenilikler. Kodda üretken programlamanın (=Generic Programming) gücü hissediliyor. Aynı testi std::vector ile kodda neredeyse hiçbir değişiklik yapmadan gerçekleştirebiliriz. Ama iş paralel programlamaya gelince hala alt düzey yapıları kullanmamız gerekiyor. Bu problemde gerekmedi ama barrier, semaphore gibi yapılar hazır gelmiyor. Bizim mutex yada atomic değişkenler kullanarak bu yapıları gerçekleştirmemiz bekleniyor.
Yukarıdaki kodu derlemek için GNU C++ derleyicisi kullanıldı:

g++ -lpthread -O3 -std=c++11 -o parsum parsum.cpp

Şimdi aynı problemi Java 8'de MapReduce çatısını kullanarak çözelim:

public class ParallelSum {

    private static final int SIZE = 80_000_000;
    private static int numbers[] = new int[SIZE];

    private static void init_problem(int[] array) {
        for (int i = 0, j = 1; i < array.length; ++i, ++j) {
            array[i] = j;
        }
    }

    public static void solve() {
        int sum= Arrays.stream(numbers).parallel().sum();
        System.err.println("Sum (parallel): " + sum );        
    }

    public static void main(String[] args) {
        init_problem(numbers);
        for (int i=0;i<100;++i)
            solve();
    }
}
Java 8'de çözüm aslında tek satırdan oluşuyor: int sum= Arrays.stream(numbers).parallel().sum();
Şimdi başarımları karşılaştırabiliriz. Çalışma sürelerine bakalım. Her iki uygulama da 10 defa çalıştırıldı ve süreler milisaniye hassasiyetinde ölçüldü. 4 çekirdekli 8 sanal işlemcili makinada başarım aşağıdaki şekilde gerçekleşti:
C++11 Java 8
0.018948 0.155995
0.01986 0.01995
0.022167 0.019785
0.020191 0.018711
0.020895 0.019071
0.020394 0.018406
0.021172 0.019326
0.019818 0.019334
0.020595 0.019033
0.021555 0.019154

Karşılaştırmayı daha kolay yapabilmek için aşağıdaki grafikten  yararlanabilirsiniz:


Burada JIT derleyicinin iyi iş çıkardığını görüyoruz: çalışma süresini 0.15 saniyeden 0.019 saniyeye düşürüyor. Yinelemelerde işlem sürelerinin başa baş gittiğini görüyoruz. Kodları karşılaştırdığımızda, bu durumun, Java 8 ve Hotspot için bir zafer olduğunu söyleyebiliriz. 
View Binnur Kurt's profile on LinkedIn