Wednesday, December 9, 2015

Java SE 9'da Temel Tipler Üzerinde Çalışan Akımlar (=Stream)

Java Programlama Dili, Java SE 8 ile birlikte yeni bir paradigmayı daha destekliyor: Fonksiyonel Programlama. Java SE 8 ile birlikte gelen λ ifadeleri ise fonksiyonel programlama yapmamıza olanak sağlıyor. λ ifadeleri sayesinde fonksiyonlar artık dilin birinci sınıf vatandaşı olarak işlem görecek. Fonksiyon tipinden bir değişken tanımlayabilecek ve fonksiyona parametre olarak başka bir fonksiyonu geçirebileceğiz. Java SE 8 öncesinde, LambdaJ, Functional Java, Guava gibi çeşitli kütüphaneler aracılığı ile fonksiyonel programlama yapabiliyorduk. Ama şimdi λ ifadeleri hem dilin bir parçası haline geldi hem de yüksek başarımla çalışıyorlar. Java'da fonksiyonel programlama ile esas olarak amaçlanan çok çekirdekli programlamadır.
Java 8'de bakış açımızı da değiştirmemiz gerekiyor. Fonksiyonel programlama ile bildirimsel programlamaya (=declarative programming) geçiş yapıyoruz. Bunu basit bir örnek üzerinde anlatmak istiyorum:
for (int i=1;i<10;++i)
    System.out.println(i);
Yukarıdaki kodda 9 defa dönen basit bir for döngüsü oluşturuyoruz. Döngüyü oluştururken işlemciye adım adım neler yapması gerektiğini söylüyoruz:
1. adım: i bir tamsayıdır
2. adım: i'nin başlangıç değeri 1'dir.
3. adım: i'nin değeri 10'dan küçükse ekrana i'nin değerini yaz, aksi halde 5. adıma git
4. adım: i'nin değerini bir artır.
5. adım: Dur
Aynı problemin çözümünü, Java 8'i ile gelen Stream API'nin yeteneklerini kullanarak kodlamaya çalışalım:
IntStream.range(1,10).forEach(System.out::println);
Burada döngü kurmakla ilgili detaylarla ilgilenmedik. Sadece ne istediğimizi söyledik. Bu bildirimsel programlama olarak adlandırılır. Şimdi ise her iki yaklaşımla ama bu kez dişe dokunur bir iş yapalım, [1-2147483646] aralığındaki tam sayıların toplamını hesaplayalım:
package com.example.study;

import java.util.stream.LongStream;

public class Exercise {
 static final int RUN = 15;

 public static void main(String[] args) {
  long metricParallelStream = 0, metricForLoop = 0;
  for (int i = 0; i < RUN; ++i) {
   metricForLoop += sumWithForLoop();
  }
  for (int i = 0; i < RUN; ++i) {
   metricParallelStream += sumWithLongStream();
   System.gc();
  }
  System.out.println(String.format("For Loop   : %-16d", metricForLoop));
  System.out.println(String.format("LongStream : %-16d", metricParallelStream));
  System.out.println(String.format("Speedup    : %-2.3f", 
                                              (double) metricForLoop / metricParallelStream));
 }

 private static long sumWithLongStream() {
  long start = System.nanoTime();
  long sum = LongStream.range(1, Integer.MAX_VALUE).parallel().sum();
  long stop = System.nanoTime();
  printPerformance("LongStream", sum, start, stop);
  return stop - start;
 }

 private static void printPerformance(String method, long sum, long start, long stop) {
  System.out.println(String.format("%-16d (%-12s) @ %-16d", 
                                                 sum, method, stop - start));
 }

 private static long sumWithForLoop() {
  long sum = 0;
  long start = System.nanoTime();
  for (long i = 1; i < Integer.MAX_VALUE; sum += i, ++i)
   ;
  long stop = System.nanoTime();
  printPerformance("For-loop", sum, start, stop);
  return stop - start;
 }
}
Bu problemi hem for döngüsü hem de LongStream sınıfını kullanarak çözdük:
for (long i = 1; i < Integer.MAX_VALUE; sum += i, ++i);
LongStream.range(1, Integer.MAX_VALUE).parallel().sum();
LongStream ile elde ettiğimiz çözümün iki önemli kazanımı bulunuyor:
  • Bildirimsel programlama yaptığımız için yanlış yapmanız neredeyse imkansız.
  • parallel() çağrısı ile hiç paralel programlama bilmemiz gerekmeden çok çekirdekli sistemlerde sonuca hızlı ulaşmamızı sağlayacak şekilde paralel çalışacak bir çözüm elde ettik.
Yukarıdaki kodu 2 fiziksel çekirdekli, 4 sanal işlemcili bir dizüstü bilgisayarda çalıştırıldığında aşağıdaki sonuca ulaştık:
2305843005992468481 (For-loop    ) @ 1309016611      
2305843005992468481 (For-loop    ) @ 1307834288      
2305843005992468481 (For-loop    ) @ 1321886282      
2305843005992468481 (For-loop    ) @ 1336129993      
2305843005992468481 (For-loop    ) @ 1341218910      
2305843005992468481 (For-loop    ) @ 1308587198      
2305843005992468481 (For-loop    ) @ 1320821780      
2305843005992468481 (For-loop    ) @ 1450435201      
2305843005992468481 (For-loop    ) @ 1367910679      
2305843005992468481 (For-loop    ) @ 1310250661      
2305843005992468481 (For-loop    ) @ 1314651941      
2305843005992468481 (For-loop    ) @ 1366452480      
2305843005992468481 (For-loop    ) @ 1320181355      
2305843005992468481 (For-loop    ) @ 1313093162      
2305843005992468481 (For-loop    ) @ 1313353438      
2305843005992468481 (LongStream  ) @ 991360829       
2305843005992468481 (LongStream  ) @ 822089904       
2305843005992468481 (LongStream  ) @ 841850713       
2305843005992468481 (LongStream  ) @ 819578699       
2305843005992468481 (LongStream  ) @ 822906446       
2305843005992468481 (LongStream  ) @ 828978169       
2305843005992468481 (LongStream  ) @ 835416084       
2305843005992468481 (LongStream  ) @ 874176581       
2305843005992468481 (LongStream  ) @ 815826464       
2305843005992468481 (LongStream  ) @ 839485656       
2305843005992468481 (LongStream  ) @ 815087102       
2305843005992468481 (LongStream  ) @ 825911518       
2305843005992468481 (LongStream  ) @ 842565855       
2305843005992468481 (LongStream  ) @ 845280683       
2305843005992468481 (LongStream  ) @ 875872887       
For Loop   : 20001823979     
LongStream : 12696387590     
Speedup    : 1.575
LongStream sınıfının kullanıldığı çözümde yaklaşık 1.5 katlık bir hızlanma elde edildiği görülüyor. parallel() çağrısı kullanılmadığı durumda LongStream ile elde ettiğimiz çözümün for döngüsü ile ettiğimiz çözüme göre bir miktar daha yavaş olduğunu belirtmeliyim.
3n+1 Problemi
Matematikte 3n+1 problemi olarak bilinen bir problem vardır: 
Hangi sayıdan başlarsanız başlayın yukarıdaki kurala göre dizinin elemanlarını hesapladığınızda 1 değerine  ulaşırsınız. Örnek olarak 17'den başlayalım ve yukarıdaki kurala göre dizinin elemanlarını sırayla hesaplayalım:
17 -> 52 -> 26 -> 13 -> 40 -> 20 -> 10 -> 5 -> 16 -> 8 -> 4 -> 2 -> 1
17'den başladık ve sonunda 1'e ulaştık. Dizinin 1'e gittiğini görüyoruz ama limitinin 1 olduğunu ispatlayamıyoruz. Bu yazıda ispatlamaya çalışmayacağız, merak etmeyin. Sadece farklı yaklaşımlarla  3n+1 problemini kodlayacağız. Önce problemi buyruksal programlama ile çözelim:
int n = 17;
while (n > 1) {
    n =  (n%2==1) ? 3 * n +1 : n / 2;
    System.out.println(n);
}
Şimdi de IntStream sınıfından yararlanarak Java SE 9'da bildirimsel programlama ile çözelim:
IntStream.iterate(17, i -> i % 2 == 1 ? 3 * i + 1 : i / 2)
        .takeWhile(i -> i > 1)
        .forEach(System.out::println);
takeWhile metodu Java 9'da geldi. Parametre olarak bir Predicate alıyor. Predicate fonksiyonu false üretene kadar akım kesilmiyor.
Asal Sayı Üretelim
Asal Sayılar, kendisi ve 1 dışında tam böleni olmayan sayılar olarak tanımlanırlar. 2,3,5,7,11 ilk asal sayılar. Tüm asal sayıları verecek genel bir formül yok. Kaç tane asal sayı var? Sonsuz sayıda! Ama ne kadar sonsuz? Az? Çok? Tüm asal sayıları verecek genel bir formül yok ama bir n verildiğinde n'ye kadar en fazla kaç tane asal sayı olduğunu söyleyebiliyoruz:
Şimdi ilk k asal sayıyı üretecek kodu Java 9'da yazalım:
package com.example.study;

import java.util.stream.IntStream;

/**
 *
 * @author Binnur Kurt (binnur.kurt@gmail.com)
 */
public class PrimeNumbers {

    public static void main(String[] args) {
        int k = 100;
        IntStream.iterate(3, i -> i + 2)
                .filter( n -> 
                    n==2 || 
                    (n % 2 == 0) || 
                    IntStream.iterate(3, i -> i + 2)
                            .filter(i -> (n % i) == 0)
                            .findFirst().getAsInt() == n
                )
                .limit(k)
                .forEach(System.err::println);
    }
}
Buyruksal programlama ile kodlamayı size bırakıyorum. Kolay gelsin!
Pi Sayısını Üretelim
Pi sayısı herhangi bir çemberin çevresinin çapına oranı olarak tanımlanır. Tanımı basit olsa da ölçmesi o kadar kolay değil! Eski Çinli matematikçiler 3 yaklaşık değerini kullanmışlar. Biraz işin kolayına kaçmışlar! Pi kesirli bir sayı değil: kesirli sayılarda bir periyotluk vardır. Pi sayısında bir periyotluk bulmak mümkün değil:
Biz de elimizden geldiğince Stream API kullanarak noktadan sonrasını hesaplamaya çalışalım:
package com.example.study;

import java.util.stream.LongStream;

public class ComputePi {

   public static void main(String[] args) {
        double pi= LongStream.iterate(1, i -> (i<0) ? (-i+2) : -(i+2) )
           .mapToDouble( i -> 4./i )
           .limit(1_000_000_000)
           .sum();
        System.out.println(pi);
   }

}
Ben bu kadarını yapabildim: 3.1415926525897935. Artık siz tam rakamı bulursunuz!

No comments:

Post a Comment