Java Yavaş mı? : Java’nın Performansı Üzerine – II
Bir önceki yazıda kaldığımız yerden devam edelim.
Java Yavaş mıdır?
Java dil olarak yavaş değildir. Çünkü, Java hemen hemen native çalışır. Java’nın yorumlanan (interpreted) yapısının onun performansını hiç bir zaman native çalışan C/C++ gibi diller seviyesine çıkmasına izin vermeyeceği aslında eskimiş, çoktan demode olmuş, geçen yüzyılın bir iddiasıdır. JVMlerin barındırdığı Just In Time (JIT) derleyicileri sık çalışan Java kodlarını çalışma zamanında bulur ve native koda derler öyleki artık bu kodlara yapılan tüm çağrılar native olarak çalışır. Varsayılan değeri 10,000 olan “-XX:CompileThreshold” parametresi bu amaçla kullanılır. Java’nın 1.2 sürümüyle birlikte gelen ve 1.3 sürümünde varsayılan JVM olan HotSpot ismini zaten buradan almaktadır.
Ayrıca Java’nın yorumlanan (interpreted) yapısı ve pek çok çalışma zamanı (run-time) iyileştirmesine imkan verir. Bu iyileştirmeler nihayetinde HotSpot’ın görevleri arasındadır. Dolayıısyla en azından algoritmik performansı göz önüne alırsak, Java’nın C/C++’tan daha yavaş olduğunu söylemek zordur. Bu anlamda Java’nın daha yavaş ya da daha hızlı olduğu durumsaldır, kullanılan algoritmaya da bağlıdır.
C++ ve Java Performans Karşılaştırması
Bu konuyu iki örnekle göstermek istiyorum. Aynı algoritmayı C++ ve Java’da tamamen aynı şekilde yazarak, algoritmik açıdan bu iki dilin performansları karşılaştırılabilir. Ama unutmayalım ki bu türden karşılaştırmalar uygulama performansı ile ilgili bir şey söylemedikleri gibi sadece algoritmik dil performansı hakkında fikir vermeye yöneliktir.
İlk algoritma asal sayıları bulmak için kullanılan, Sieve of Eratosthenes‘in bir ilerlemiş hali olan Sieve of Atkin algoritması olsun.
Önce SieveOfAtkin.cpp
/* * File: SieveOfAtkin.cpp * Author: akin * * Created on October 7, 2015, 12:27 AM */ #include "SieveOfAtkin.h" #include <iostream> #include <cmath> #include <vector> #include <ctime> using namespace SieveOfAtkin; using namespace std; int main() { int n; cout << "Enter the number up to which prime numbers are counted: " << endl; cin >> n; clock_t start = clock(); int numberOfPrimes = countPrimesUsingSieveOfAtkins(n); clock_t end = clock(); int time = (int) (1000 * ((float) end - start) / CLOCKS_PER_SEC); cout << "Number of primes up to " << n << " : " << numberOfPrimes << endl; cout << "Time: " << time << " ms." << endl; cout << endl; return 0; } namespace SieveOfAtkin { using namespace std; int countPrimesUsingSieveOfAtkins(int n) { bool* is_prime = new bool[n + 1]; is_prime[2] = true; is_prime[3] = true; for (int i = 5; i <= n; i++) is_prime[i] = false; int limit = ceil(sqrt(n)); for (int x = 1; x <= limit; x++) { for (int y = 1; y <= limit; y++) { int num = (4 * x * x + y * y); if (num <= n && (num % 12 == 1 || num % 12 == 5)) is_prime[num] = true; num = (3 * x * x + y * y); if (num <= n && (num % 12 == 7)) is_prime[num] = true; num = 3 * x * x - y * y; if ((x > y) && (num <= n) && (num % 12 == 11)) is_prime[num] = true; } } for (int i = 5; i <= limit; i++) if (is_prime[i]) for (int j = i * i; j <= n; j += i) is_prime[j] = false; int numberOfPrimes = 2; for (int i = 5; i <= n; i++) if (is_prime[i]) numberOfPrimes++; return numberOfPrimes; } }
Şimdi de SieveOfAtkin.java
package org.javaturk.performance.algorithm.prime; import java.util.Scanner; /** * http://www.sanfoundry.com/java-program-sieve-of-atkin-algorithm/ * * @author akin * */ public class SieveOfAtkin { public static void main(String[] args) { System.out.println("Please enter a number to which to list prime numbers:"); Scanner sc = new Scanner(System.in); int n = sc.nextInt(); long start = System.currentTimeMillis(); int numberOfPrimes = countPrimesAsUsingSieveOfAtkins(n); long end = System.currentTimeMillis(); long miliSeconds = end - start; System.out.println("There are " + numberOfPrimes + " prime numbers up to " + n + "."); System.out.println("Time to find them: " + miliSeconds + " ms."); } private static int countPrimesAsUsingSieveOfAtkins(int n) { /** initialize the sieve **/ boolean[] prime = new boolean[n + 1]; prime[2] = true; prime[3] = true; int limit = (int) Math.ceil(Math.sqrt(n)); /** * put in candidate primes: integers which have an odd number of * representations by certain quadratic forms **/ for (int x = 1; x <= limit; x++) { for (int y = 1; y <= limit; y++) { int num = 4 * x * x + y * y; if (num <= n && (num % 12 == 1 || num % 12 == 5)) prime[num] = true; num = 3 * x * x + y * y; if (num <= n && num % 12 == 7) prime[num] = true; num = 3 * x * x - y * y; if ((x > y) && (num <= n) && (num % 12 == 11)) prime[num] = true; } } for (int i = 5; i <= limit; i++) if (prime[i]) for (int j = i * i; j <= n; j += i) prime[j] = false; int numberOfPrimes = 2; for (int i = 5; i <= n; i++) if (prime[i]) numberOfPrimes++; return numberOfPrimes; } }
Ben bu iki programı bendeki iki ayrı makinada defalarca test ettim. Makinalarım şu özelliklerde:
- 16 GB RAM ve 8 çekirdekli i7 CPU’ya sahip, üzerinde El Capitan OS çalışan MacBook Pro
- 8 GB RAM ve 8 çekirdekli i7 CPU’ya sahip, üzerinde Windows 10 çalışan PC
Bu iki programı çalıştırmak için Mac’de Java için JDK 1.8.0_25 JVM, C++ için de XCode 7.1 kullanılmıştır.(Sonradan eklendi: XCode 7.1 varsayılan durumda C++ derleyicisi olarak gcc (GCC) 4.2.x kullanıyor. Ben bu yazıyı yazdıktan sonra makinamdaki GCC’yi en son sürüme yükselttim dolayısıyla artık gcc (GCC) 4.9.2 var ama bu sürümle ölçümleri henüz tekrarlamadım, tekrarlayıp yayınlayacağım.) Çalışma zamanı için tamamen varsayılan yapılar kullanılmış, hiç bir parametre ya da flag verilmemiştir.
Farklı n girdisi için mili saniye cinsinden Java ve C++ programlarının çalışma süreleri şunlardır:
Program |
10^6 |
10^7 | 10^8 | 10^9 |
2*10^9 |
SieveOfAtkin.java |
16 |
84 | 1,025 | 18,507 |
47,752 |
SieveOfAtkin.cpp |
18 |
218 | 2,461 | 34,871 |
82,059 |
Bendeki ve arkadaşlarımdaki Windows makinalarda da aynı JVM ve GNU C++ compilerıyla derleyerek yapılan çalışmalarda da yukarıdakine benzer sonuçlar elde ettim.
Sizden ricam, sizin de bu iki programı makinalarınızda çalıştırmanız ve aldığınız sonuçları burada yorum olarak bizlerle paylaşmanız.
Bir sonraki yazıda Monte Carlo simulasyonu ile Pi’ye yakınsamaya çalışacağız 🙂
Toplam görüntülenme sayısı: 836
Yaşar Safkan
24 Ekim 2015 @ 23:32
Yazdığınız çok doğru. Ufak iki şey yumurtlayasım geldi ama:
1. “Yorumlanan” deyince, doğru, ama yorumlama bir ara seviyede oluyor. Yani, “bytecode” dediğimiz, aslında var olmayan bir makinanın (bir ara yaptılardı sanki ama) komutları (instruction) çalıştırılıyor. Yani, direkt olarak kaynak kod yorumlanmıyor. Çok bir şey farkedeceğinden değil; ancak tamamen yorumlanarak çalışan dillerde, bir derleme adımı olmuyor. Javada ise, işimize de gelen bir derleme adımı var ki, bizi belli açılardan vaktiyle hizaya sokuyor.
Yorum yazıya doğru gidiyor ama, şunu da belirtmek lazım: Dilin yorumlanması veya derlenmesi, bir gerçeklenme problemi. Mesela, Javascript’in tanımında “yorumlanan” lafı geçiyor. Ancak mesela V8 önce derlemeyi tercih ediyor.
2. “Java yavaş mı” sorusunun cevabı, biraz “hadeleyn” civarında. Gerçekten de bu sözlerin doğruluk payı taşıdığı zamanlar, geçen yüzyıldaydı.
Saf Java koduna karşı, saf C kodu durumunda, verdiğiniz örnekler durumu gösteriyor.
İlginçlik, enteresan donanımlarda çıkıyor ortaya. Mesela, GPU kullanımı meselesi var. Paralel veri parçaları üzerinde hesaplama ağırlıklı işler için, GPU, CPU’yu dövüyor. (Bitcoin mining ile uğraşan var mı?) Öyle bir mesele için, JVM içinde bir şey yapmak mümkün olmayabiliyor. Elbette bu tip şeyler için de Java projeleri var. Ancak nihayetinde birinin o “native code”u yazması gerekiyor bir noktada.
Java’nın bugün ulaştığı bu yüksek verimlilik de, yıllar içinde hem derleme optimizasyonları (JIT derlemeden bahsediyorum) hem de muhtelif platformlardaki JVM’lerin daha kaliteli ve altta yatan donanımı daha iyi kullanmasının eseri.
Bunu, zamanında C kodu içine assembler yazılması durumuna benzetiyorum. Linux’un ilk kernel kodlarında, bunlar bol miktarda vardı. Esas sebep, derleme için kullanılan (o zaman tek opsiyon) GCC’nin, o kadar da iyi optimizasyon yapamamasıydı. Modern GCC versiyonlarında, optimizasyonlar o kadar kudretli hale geldi ki, elle GCC’nin ürettiği koddan daha hızlı kod üretmek neredeyse imkansız hale geldi. Kernel kodundaki pek çok assembler kısmı da bu sebepten sökülüp atıldı.
Şimdi aynı vaziyet, Java için gerçekleşiyor. Yani, çok da şaşılacak bir durum yok aslında.
Akin
25 Ekim 2015 @ 00:48
Yaşar hocam yorumunuz ve katkınız için teşekkür ederim. Turkiye’deki bilişim camiası performans deyince sadece donanımı anlıyor. Bu konularda belki daha çok yazıp çizmek lazım.