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 ; }
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(); } }
Ş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.
Java icin ilk calistirmanin digerlerine yuksek olmasi compile isleminin ilk calistirmada mi yapilmasidir? Tesekkurler...
ReplyDeleteUygulamayı -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.
ReplyDeleteŞ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