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

2 comments:

  1. Java icin ilk calistirmanin digerlerine yuksek olmasi compile isleminin ilk calistirmada mi yapilmasidir? Tesekkurler...

    ReplyDelete
  2. Uygulamayı -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation seçeneği ile çalıştırırsanız JIT davranışını izleyebilirsiniz. Şimdi ekrana hangi metotları JIT'lediği bilgisini yazacaktır.Aşağıda bu çıktıyı listeledim. Dikkatlice bakıldığında iyileştirmelerin çoğunun döngü ilk kez döndüğünde gerçekleştiğini görebiliriz. Bu elimizdeki uygulamanın tekbir kullanım senaryosundan ve basit bir akışa sahip olduğu için bu şekilde gerçekleşti. İlk döngüde uygulama şeklini aldı. Gerçekte uygulamanın şeklini alması dolayısı ile JSM'nin kodu en iyilemesi vakit alır, Bu nedenle Java uygulamalarına Benchmarking yaparken dikkatli olmak gerekir. Uygulamayı bir kere çalıştırıp süreye bakmak yanıltıcı sonuç verir. Uygulamayı bir süre boşta çalıştırıp, JIT derleyecinin optimizasyonları görmesini beklemek ve daha sonra ölçümleme yapmak uygun olur.

    Şimdi ekrana bir dizi log attığımız için süreler biraz arttı. Buna dikkat edin. Logsuz süreler daha düşük.

    179 1 3 java.lang.String::length (6 bytes)
    181 2 3 java.lang.String::hashCode (55 bytes)
    181 3 3 java.lang.String::charAt (29 bytes)
    186 4 3 java.lang.AbstractStringBuilder::ensureCapacityInternal (16 bytes)
    187 5 3 java.lang.Character::toLowerCase (9 bytes)
    ...
    ...
    730 158 4 java.util.stream.ReduceOps$5ReducingSink::accept (19 bytes)
    730 157 1 java.lang.Integer::sum (4 bytes) made not entrant
    732 155 3 java.util.stream.ReduceOps$5ReducingSink::accept (19 bytes) made not entrant
    733 160 % 3 java.util.Spliterators$IntArraySpliterator::forEachRemaining @ 49 (68 bytes)
    734 161 3 java.util.Spliterators$IntArraySpliterator::forEachRemaining (68 bytes)
    736 162 % 4 java.util.Spliterators$IntArraySpliterator::forEachRemaining @ 49 (68 bytes)
    747 160 % 3 java.util.Spliterators$IntArraySpliterator::forEachRemaining @ -2 (68 bytes) made not entrant
    755 163 4 java.util.Spliterators$IntArraySpliterator::forEachRemaining (68 bytes)
    763 161 3 java.util.Spliterators$IntArraySpliterator::forEachRemaining (68 bytes) made not entrant
    777 164 3 sun.misc.FDBigInteger::mult (64 bytes)
    Sum (parallel): 296376832 @ 0.312157798
    778 165 3 sun.misc.FDBigInteger:: (30 bytes)
    778 166 3 sun.misc.FDBigInteger::trimLeadingZeros (57 bytes)
    Sum (parallel): 296376832 @ 0.023211352
    Sum (parallel): 296376832 @ 0.023963444
    826 167 3 java.lang.StringBuilder:: (7 bytes)
    Sum (parallel): 296376832 @ 0.020885702
    857 168 1 java.util.stream.AbstractTask::getLocalResult (5 bytes)
    Sum (parallel): 296376832 @ 0.021664887
    874 169 3 java.util.Spliterators$IntArraySpliterator::estimateSize (11 bytes)
    881 170 n 0 sun.misc.Unsafe::compareAndSwapObject (native)
    Sum (parallel): 296376832 @ 0.022081986
    Sum (parallel): 296376832 @ 0.021059766
    912 171 n 0 java.lang.Thread::currentThread (native) (static)
    922 172 1 java.util.Spliterators$IntArraySpliterator::characteristics (5 bytes)
    Sum (parallel): 296376832 @ 0.025300538
    938 173 3 java.util.concurrent.CountedCompleter:: (10 bytes)
    938 174 1 java.util.stream.AbstractPipeline::getStreamAndOpFlags (5 bytes)
    942 175 n 0 sun.misc.Unsafe::compareAndSwapInt (native)
    947 176 3 java.util.stream.ReduceOps$ReduceTask::makeChild (6 bytes)
    953 178 3 java.util.stream.ReduceOps$ReduceTask::onCompletion (51 bytes)
    953 177 3 java.util.stream.AbstractTask::setLocalResult (6 bytes)
    Sum (parallel): 296376832 @ 0.033181479
    992 179 n 0 sun.misc.Unsafe::compareAndSwapLong (native)
    Sum (parallel): 296376832 @ 0.023405534

    ReplyDelete