April 19, 2015

Parallel Stream - Part 2

ပြီးခဲ့သော အခန်းဆက်ဖြင့် Parallel Stream အား အသုံပြုပုံကို ဖော်ပြခဲ့ပါသည်။ Parallel Stream အား အသုံးပြုခြင်းအားဖြင့် လွယ်ကူစွာပင် Parallel Processing ကို အသုံးပြုနိုင်မည်ဖြစ်သော်လည်း၊ သတိမမူပဲ အသုံးပြုပါက မလိုလားအပ်သော ပြဿနာများကို ဖြစ်စေနိုင်ပါသည်။ ထို့ကြောင့် ဤအခန်းတွင် Parallel Stream အား အသုံးပြုရာတွင် သတိပြုသင့်သော အခြေခံ အချက်များကို ဖော်ပြသွားပါဦးမည်။


Overhead

Parallel Stream အား အသုံးပြုရာတွင် Sequential Stream နှင့်စာလျင် Overhead ဖြစ်စေမည့် Process များ အတော်များပါသည်။ ထို့ကြောင့် အသုံးပြုမည့် Element ပမာဏ နည်းပါးပါက Parallel Stream ကို အသုံးပြုခြင်းက Performance ကို ကျဆင်းစေတတ်ပါသည်။

CPU ၏ Core အရေအတွက်ပေါ်တွင်မူတည်မည် ဖြစ်သော်လဲ၊ အနည်းဆုံး Element အရေအတွက် ၁၀၀၀၀ လောက်မှ Parallel Stream အား အသုံးပြုရသည့် အကျိုးကို သိမြင်နိုင်မည်ဖြစ်ပါသည်။

ထို့ကြောင့် အသုံးပြုရင်း Performance ပိုင်းဆိုင်ရာတွင်ထူးခြားမှု့မရှိဟု မြင်ပါက Element ၏ အရေအတွက်ကို ပြန်လည် စမ်းစစ်သင့်ပါသည်။


Because of Fork and Join

ကျွန်တော်တို့ အောက်ပါ ကုဒ်များကို Run ကြည့် ပါမည်။
    public static void main(String[] args) {

        BiConsumer<String, Supplier<OptionalDouble>> tester = (message, job) -> {
            LocalDateTime start = LocalDateTime.now();
            OptionalDouble result = job.get();
            LocalDateTime end = LocalDateTime.now();
            
            System.out.printf("%s , Result : %s, Time : %d%n", 
                    message, 
                    result, 
                    Duration.between(start, end).toNanos());
        };
        
        // range
        tester.accept("Test 1", () -> {
            return IntStream
                    .range(0, 1_000_000)
                    .parallel()
                    .filter(a -> a % 2 == 0)
                    .average();
        });
        
        // iterate
        tester.accept("Test 2", () -> {
            return IntStream
                    .iterate(0, a -> a + 1 )
                    .limit(1_000_000)
                    .parallel()
                    .filter(a -> a % 2 == 0)
                    .average();
        });
    }
အထက်ပါ နမူနာ၏ စာကြောင်း ၁၆နှင့် ၂၅တို့၏ Statement များသည် သုညမှ ၁၀၀၀၀၀၀ အကြားရှိ စုံကိန်းများ၏ ပျမ်းမျှတန်ဖိုးကို ရှာဖွေနေသည်မှာ အတူတူပင်ဖြစ်၏။ မတူညီသည်မှာ ၎င်းတို့အား စတင်စေသည့် နေရာပင်ဖြစ်၏။ စာကြောင် ၁၆တွင် range ဖြင့်စတင်ကာ စာကြောင်း ၂၅ တွင် iterate ဖြင့်စတင်ပါသည်။

ရလဒ်မှာအောက်ပါအတိုင်းဖြစ်ပါသည်။
Test 1 , Result : OptionalDouble[499999.0], Time : 33000000
Test 2 , Result : OptionalDouble[499999.0], Time : 118000000

နှစ်ခုလုံး၏ အဖြေမှာ အတူတူပင်ဖြစ်သော်လည်း၊ Test 2 အားလုပ်ဆောင်ရာတွင် Test 1 ထက် သိသိသာသာ ကြာမြင့်သည်ကို တွေ့ရပါသည်။

အဘယ့်ကြောင့် ဆိုသော် Parallel Stream အား လုပ်ဆောင်စေရာတွင် မူလအရင်းအမြစ်အတွင်းရှိ Element များအား အစိတ်စိတ် အပိုင်းပိုင်းခွဲ၍ အပြိုင်လုပ်ဆောင်စေပါသည်။ အစိတ်စိတ် အပိုင်းပိုင်းခွဲရာတွင် Fork And Join အား အသုံးပြုနေပြီး အတွင်းတွင် divide and conquer algorithm အား အသုံးပြု နေပါသည်။ အဆိုပါ Algorithm သည် မူလ Problem အား အသေးငယ်ဆုံး အနေအထား အထိ နှစ်ပိုင်းခွဲ အဖြေရှာ၍ ရလဒ်စုစုပေါင်းအား ရှာဖွေပါသည်။

ထို့ကြောင့် iterate ကဲ့သို့ အဆုံးမရှိသော ရင်းမြစ်များသည် စုစုပေါင်းပမာဏအား ရှာဖွေရန်ခက်ခဲပြီး၊ Divide And Conquer Algorithm တွင် အသုံးပြုရန်ခက်ခဲပါသည်။ ထို့ကြောင့် Test 2 ၏ Performance မှာ Test 1 ထက် ဆိုးရွားနေခြင်းဖြစ်ပါသည်။

iterate အပြင် generate သည်လည်း မူလအရေအတွက်ကို ခန့်မှန်းရခက်ခဲပါသဖြင့် Parallel Stream များတွင် အသုံးမပြုသင့်ပေ။ 

ထို့အပြင် File များမှတိုက်ရိုက် ပြုလုပ်သော Stream များအားလည်း Parallel Stream တွင် အသုံးမပြုသင့်ပါ။ ဥပမာအားဖြင့် Files#lines နှင့် BufferedReader#lines method များသည် ဖိုင်လ် အတွင်းရှိ စာကြောင်းများအား တစ်ကြောင်းစီ ဖတ်၍ Stream အတွင်းသို့ ထည့်သွင်းနေသောကြောင့် Process မဆုံးမခြင်း စုစုပေါင်းပမာဏအား မသိနိုင်ပါ။ ထို့ကြောင့် File များအား အသုံးပြုရာတွင် အရင်ဆုံး Files#readLines ဖြင့် စာကြောင်းများအား ဖတ်ယူပြီးမှ Parallel Stream အဖြစ်အသုံးပြုသင့်ပါသည်။


Accessing External Variable


ဒီတစ်ခေါက်တော့ ဉာဏ်စမ် ပဟေဠိလေးဖြင့် စကြည့်ပါမည်။
public class SumTest {
    
    public static void main(String[] args) {
        
        SumTest sum = new SumTest();
        List<Integer> list = IntStream.rangeClosed(0, 10_000_000)
                .mapToObj(a -> new Integer(a))
                .collect(Collectors.toList());
        
        BiConsumer<List<Integer>, 
            Function<List<Integer>, Long>> tester = (data, function) -> {
                LocalDateTime start = LocalDateTime.now();
                long result = function.apply(data);
                LocalDateTime end = LocalDateTime.now();
                
                System.out.format("%d : %d%n", Duration.between(start, end).toNanos(), result);
        };
        
        tester.accept(list, sum::getSum1);
        tester.accept(list, sum::getSum2);
        tester.accept(list, sum::getSum3);
    }
    
    private long sum;
    
    long getSum1(List<Integer> list) {
        sum = 0L;
        
        list.parallelStream().forEach(a -> sum += a);
        
        return sum;
    }
    
    private LongAdder adder;
    
    long getSum2(List<Integer> list) {
        adder = new LongAdder();

        list.parallelStream().forEach(a -> adder.add(a));
        
        return adder.longValue();
    }
    
    long getSum3(List<Integer> list) {
        return list.parallelStream().mapToLong(a -> a.longValue()).sum();
    }

}

အထက်ပါကုဒ်များသည် အလုပ်လုပ်ပါမည်လော။  getSum1, getSum2 နှင့် getSum3 တို့တွင် မည်သည့် method ၏ ရလဒ်သည် အမှန်ကန်ဆုံးနှင့် Performance အကောင်းဆုံးဖြစ်မည်နည်း။


getSum1 method


သော့ချက်မှာ စာကြောင်း ၂၇ရှိ sum variable ဖြစ်သည်။ စာကြောင်း ၂၉ ရှိ Lambda Expression အတွင်းမှ Reference လုပ်နေသောကြောင့် final ဖြစ်ရန်လိုအပ်ပါသည်။ သို့ရာတွင် စာကြောင်း ၂၄ တွင် Private member အဖြစ် Declare လုပ်ထားပြန်သဖြင့် Lambda Expression အတွင်းမှ ဆက်သွယ် အသုံးပြုနိုင်ပမည်။ ထိုကြောင့် Error မတက်ပဲ အလုပ်လုပ်မည်ဖြစ်သည်။
သို့သော်လဲ အသုံးပြုနေသည်မှာ Parallel Stream ဖြစ်ပြီး long type မှာ Thread Safe  မဖြစ်သောကြောင့် အဖြေမှာမှန်မည်မဟုတ်။
ထို့ကြောင့် Parallel Stream အား အသုံးပြုသောအခါ Thread Safe မဖြစ်သော Variable များအား ရှောင်ရှားသင့်ပါသည်။


getSum2 method


အဆိုပါ method သည် Thread Safe ဖြစ်စေရန် Java SE 8 တွင် အသစ်ဖြည့်စွက်ထားသော LongAdder Objecct အားအသုံးပြုထားပါသည်။ Thread Safe ဖြစ်သည်မှာ မှန်သော်လဲ၊ ပြင်ပရှိ Object တစ်ခုထဲအား Thread များမှ တစ်ပြိုင်ထဲ Access လုပ်ခြင်းသည် Bottle Neck ကို ဖြစ်ပွါးစေ၏ Performance ကို ကျဆင်းစေနိုင်ပါသည်။
ထို့ကြောင့် Parallel Stream များအတွင်းမှ တတ်နိုင်သလောက် ပြင်ပ Variable များ အား ဆက်သွယ် အသုံးပြုခြင်းကို ရှောင်ရှားသင့်ပါသည်။

getSum3 method


အထက်ပါ မက်သတ်အတွင်းတွင် External Variable များအား အသုံးမပြုပဲ Parallel Stream တွင်း၌သာ စုစုပေါင်းအားရှာဖွေစေပါသည်။ ရလဒ်မှာ အောက်ပါအတိုင်းဖြစ်ပါသည်။
94000000 : 15028796145232
79000000 : 50000005000000
56000000 : 50000005000000

getSum1 ၏ရလဒ်သည် မှားယွင်း၍ Performance မှာ အဆိုးရွားဆုံးဖြစ်ပါသည်။
getSum2 ၏ရလဒ်သည် မှန်ကန်သော်လည်း Performance မှာ သိပ်မကောင်းပါ။
getSum3 ၏ရလဒ်သည် မှန်ကန်ပြီး Performance မှာလည်း အကောင်းဆုံးဖြစ်သည်ကို တွေ့ရပါသည်။


ဆက်ပါဦးမည်။လေးစားစွာဖြင့်
မင်းလွင်

No comments:

Post a Comment