Java Yavaş mı? : Java’nın Performansı Üzerine – IV
Bir önceki yazıda Monte Carlo simulasyonu ile Pi sayısını hesaplama ve asal sayı bulma amaçlı iki algoritma üzerinden C++ ile Java’nın performanslarını karşılaştırmıştım. Sağolsun bazı okurlarım düşüncelerini iletirken, iki okurum da iki algortimayı kendi makinaların da deneyip sonuçlarını benimle paylaştılar. İki okurumun da sonuçları, benim elde ettiğimin tersine çıkmış durumda, yani onların elde ettiği ölçümlerde bu iki algoritmada C++, Java’dan daha hızlı çalışmış. Hatta kendileri benim bu sonuçları alırken nasıl bir konfigürasyona sahip olduğumu da sordular. Bu durumda ben de bu serinin dördüncüsü olarak yazdığım yazıyı bir öteye itip, araya bu yazıyı aldım.
Öncelikle açıkçası şunu ifade etmem gerekir ki benim “Java Yavaş mı? : Java’nın Performansı Üzerine” yazı dizisinde ispatlamaya çalıştığım şey Java’nın C++’tan hızlı olduğu değildir. Derdim hangisinin hızlı olup olmadığını da anlamak değildir. Bu yazılarda anlatmaya çalıştığım şey öncelikle “hız” kavramının muğlaklığı ve “dilin hızı” ile “uygulamanın hızı” kavramlarının arasındaki fark ve bu noktada mimarinin önemidir. Ve malesef bunlar, bu işin pratiğini yapanların kafasında çok da aydınlanmış kavramlar da değiller. Sonrasında ise hedefim, Java’nın yavaş olduğu iddiasının içinin dolu olmadığını göstermektir. Bunu da olabildiğince delilli yapmaya çalışıyorum, bu blogda devamlı yaptığım gibi. Zaten aşırı ideolojik bir ülkede olmamızdan dolayı üzerine kafa patlatılmış, delilli cümlelerle konuşma yerine kahvehane seviyesinde, bilgilesizce edinilmiş fikirlerle tartıştığımız için, bu dizide ben “Java yavaş” cümlesinin de tam da bu cinsten bir ifade ve delilsiz ve mesnetsiz bir inanış olduğu göstermeye çalışıyorum. Öte yandan 90’lı yılların başından bu yana bu sektörde olan birisi olarak zaten sadece iki algoritma çalıştırmakla diller arasında gerçek bir performans kıyaslaması olamayacağını biliyorum.
Bu açıklamadan sonra şimdi ben kendi kullandığım configürasyonlardan bahsedeyim.
- Makinam, 16 GB RAM ve 8 çekirdekli i7 CPU’ya sahip, üzerinde El Capitan OS çalışan bir MacBook Pro.
- C++ için GNU g++ (GCC) 4.9.2 20141029 kullandım. Derleme komutu ise: g++ -O3 -std=c++11 -o SieveOfAtkin.out SieveOfAtkin.cpp
- Java için ise Java(TM) SE Runtime Environment (build 1.8.0_45-b14), Java HotSpot(TM) 64-Bit Server VM (build 25.45-b02, mixed mode). Java ile compile ederken de çalıştırıken de hiç bir flag kullanmadım, tamamen varsayılan seçenekler geçerliydi.
Sieve Of Atkin algoritması için farklı n girdisi için mili saniye cinsinden yukarıdaki Java ve C++ konfigürasyonlarının sonuçları aşağıdadır. 5 defa çalıştırma sonucunda elde edilen ortalama çalışma süreleri şunlardır:
Program |
10^6 |
10^7 | 10^8 | 10^9 |
2*10^9 |
SieveOfAtkin.cpp
g++ -O3 -std=c++11 |
7 |
69 | 1,021 | 18,434 |
46,246 |
SieveOfAtkin.java |
16 |
84 | 1.025 | 18,507 |
47,752 |
Yukarıdaki tablodaki ölçümlerden Java için olanları tekrar etmedim, verileri daha önceki ölçümlerdir. Sadece C++ ölçümlerini tekrar ettim, çünkü okurlardan C++ ölçümlerine itiraz edenler olmuştu. Makinamdaki C++ derleyicisini yeniledim. C++ kodunu Compile ederken de “-O3” flagini kullandım. Bu şekilde C++ ölçümlerinin ciddi olarak iyileştiği görülüyor. Bu sonuçları Java açısından yorumlarsak, Java’nın yavaş olmadığı, C++ ile başa-baş bir performans sergilediği görülüyor.
Daha önceki ve şimdiki C++ ölçümlerini kıyaslamak gerekirse, kullandığım farklı opsiyonlarla performanslar aşağıdaki gibi olmaktadır. Bu sonuçlardan özellikle “-O3” performans flaginin ciddi bir etiye sahip olduğu anlaşılıyor. Fakat merak ettiğim şey neden bu flagin varsayılan halde geçerli olmadığı? Uzun süredir C++ ile ilgilenmediğim için belli ki bu konulara detaylıca bakmam gerekli.
Program |
10^6 |
10^7 | 10^8 | 10^9 |
2*10^9 |
SieveOfAtkin.cpp
GNU g++ (GCC) 4.9.2 |
18 |
218 | 2,461 | 34,871 |
82,059 |
SieveOfAtkin.cpp
GNU g++ (GCC) 4.9.2 “g++ -O3 -std=c++11 -o“ |
7 |
69 | 1.021 | 18,434 |
46,246 |
Öte taraftan benzer ölçümleri uzun süre C ve C++ ile çalışmış bir arkadaşımdan da yapmasını istedim. Sağolsun Sieve of Atkin algoritması için detaylı ölçümler yapmış. Her farklı n girdisi için 3 ayrı çalıştırma yapmış ve ortalamasını ile beraber kaydetmiş. Elde ettiği ölçümler aşağıdaki gibi:
10^6 | ||||
Konfigürasyon |
1. run |
2. run | 3. run | Ort. |
GNU GCC (Release) -O3 -std=c++11 |
59 |
54 | 62 | 58 |
Java |
74 |
90 | 75 | 80 |
10^7 | ||||
Konfigürasyon |
1. run |
2. run | 3. run | Ort. |
GNU GCC (Release) -O3 -std=c++11 |
408 |
396 | 411 | 405 |
Java |
386 |
385 | 382 | 384 |
10^8 | ||||
Konfigürasyon |
1. run |
2. run | 3. run | Ort. |
GNU GCC (Release) -O3 -std=c++11 |
3,782 |
3,851 | 3,772 | 3,802 |
Java |
3,430 |
3,415 | 3,388 | 3,411 |
Arkadaşım, n = 10^9 için bir kaç farklı C++ compilerı kullanarak çok farklı sonuçlar elde etmiş.
10^9 | ||||
Konfigürasyon |
1. run |
2. run | 3. run | Ort. |
GNU GCC mingw32-g++ (Debug) -O2 -std=c++11 |
39,818 |
40,140 | 39,764 | 39,907 |
GNU GCC (Release) -O2 -std=c++11 |
37,604 |
37,598 | 38,642 | 37,948 |
GNU GCC (Release) -O3 -std=c++11 |
37,751 |
37,642 | 37,816 | 37,731 |
Visual C++ 2010 (Debug) |
69,142 |
– | – | 69,142 |
Visual C++ 2010 (Release) /Ox (Maximum Optimization) |
49,072 |
– | – | 49,072 |
Java |
34,619 |
34,418 | 34,409 | 34,482 |
2*10^9 | ||||
Konfigürasyon |
1. run |
2. run | 3. run | Ort. |
GNU GCC (Release) -O3 -std=c++11 |
75,175 |
– | – | 75,175 |
Java |
67,552 |
– | – | 67,552 |
Arkadaşımın ölçümleri elde ettiği makinasının konfigürasyonu da şöyle:
- Intel Core i7-2670QM CPU @ 2.2GHz 8 GB Bellek
- 64 Bit Windows 7 İşletim Sistemi (service Pack 1)
Yukarıdaki ölçümler, farklı makinalarda ve farklı compilerlarla yapılmış olması açısından Java’nın performansı noktasında daha objektif sonuçlar verdiği kesin. Benzer şekilde, bu sonuçlar Java ile C++’ın algoritmik performanslarının kıyaslanabilir olduğunu gösteriyor. Elde ettiğimiz sonuçlar, bu iki dil arasında CPU’yu çalıştırma noktasında çok da fazla bir performansının farkının olmadığını, eğer varsa da, girdi arttıkça performansın Java lehine ilerlediğini gösteriyor. Sonraki yazılarda bu fark üzerine konuşacağız.
Bol performanslı günler dilerim.
Toplam görüntülenme sayısı: 1203
Binnur Kurt
02 Aralık 2015 @ 21:28
1. Java kodunun başarımını jmh ile ölçmek daha doğru olurdu. Güzel bir haber: jmh, jdk 9’un içinden hazır çıkacak.
2. -O3 seçeneği ile derleyici kodu en iyiler ancak artık hata ayıklayamazsınız. Kullanılan en iyileme teknikleri durağan tekniklerdir. Derleyici kodun ancak durağan analizini yapabilir ve hızlandırmak için yapabilecekleri bu nedenle kısıtlıdır. Ancak java’da en iyileme kararları yürütme zamanında dinamik olarak verilir. JIT derleyici metodun tek şekilli, iki şekilli ya da çok şekilli olmasına göre değişik en iyileme tekniklerini uygulayabilir. Örneğin tek şekilli bir metodu inline edebilir. Bir metoduntek şekilli, iki şekilli ya da çok şekilli olması yürütme zamanında uygulamanın aldığı şekle göre zamanla değişen bir durumdur. C++’da derleyici virtual tanımlı metodun tek şekilli mi, iki şekilli mi yoksa çok şekilli mi olduğunu bilemez. Bu nedenle virtual tanımlı bir metodu her zaman virtual table üzerinden bağlamak zorundadır. Açıkçası bunu c++ geliştiricisi de bilemez, hatta kaliteli bir nesneye dayalı tasarımda bilmemelidir de. Bu JIT derleyicinin yapabildiklerine ilişkin basit bir örnek. Daha başka bir çok dinamik en iyileme tekniği bulunuyor.
Akin
02 Aralık 2015 @ 23:53
Selam Binnur,
İlk söylediğine katılıyorum ama derdim açıkçası çok da performans kıyası yapmak değildi, sonuçlara bir kac arkadas, haklı olarak, itiraz edince detaylandırdım. JDK 9’daki microbenchmarking ve özellikle de REPL’ı bekliyoruz 🙂
Genis acıklama icin de tesekkur ederim. Zaten Java’nın run-time optimizasyonu yapabilmesinin avantajı, benim ölçümlerde, n girdisi arttıkça performansının iyileşmesi gibi noktalarda ortaya çıkıyor. Ben C++’ın da run-timeda performansını arttırıcı, senin dynamic iyileme (optimization) yapan yapıların geliştirildiğini okumuştum bir yerlerde. Senin bilgi var mıdır? Paylaşabilir misin?
Teşekkür ederim.
Binnur Kurt
03 Aralık 2015 @ 03:11
1. Profile guided optimization
https://msdn.microsoft.com/en-us/library/e7k32f4k.aspx
2. Burada Java’da yazılmış bir uygulamanın, C++’da yazılmış bir uygulama kadar hızlı koşabildiğini ve bazen de şartlar oluşursa daha hızlı koşabildiğini bilmek gerekir. Kötü bir algoritma seçimini, yanlış bir mimari kararı hangi dilde kodlarsanız kodlayın yavaştır.
3. Tüm popüler diller (Java, C#, Objective C gibi) altta LLVM kullanır: http://llvm.org
Debug jvm edinebilirseniz, LLVM IR’leri konsoldan okuyabilir ve neler olup bittiğini izleyebilirsiniz.
4. C++ tarafında derleyici olarak clang ve intel compiler’ın performansına bakabilirsiniz. Muhtemelen hem VS hem de gcc’den daha iyi sonuç verecektir. C++’da hızlandırma için daha çok eğer algoritma elverişli ise paralelleştirme çözümlerine bakmak gerekir: OpenMP, Cilk, CUDA, OpenCL.
Akin
03 Aralık 2015 @ 12:23
Eyvallah Binnur, teşekkür ederim.