java中如何將巢狀迴圈效能提高500倍

funnyZpC發表於2022-03-07

java中如何將巢狀迴圈效能提高500倍

轉載請註明出處https://www.cnblogs.com/funnyzpc/p/15975882.html

前面

似乎上一次更新在遙遠的九月份,按照既定的時間線應該要補5篇博文才對得起這懶惰的半年?, 近期工作強度雖不大,但也時有煩心的事兒,比如這忽冷忽熱的天氣、反反覆覆的疫情、不大不小的房貸、還有我那半死不活的手機,當然咯,手機這月必須得換了,準備xperia 5 Ⅲ或者iPhone SE ,資金若是充裕的話也給老爸換一部(耳機也安排上),各位覺得如何呢;哈哈?,扯遠了,現在就來填一下坑(補一篇部落格)。
首先,我面對的問題是:兩撥資料都從db抽取到應用(主要是mysql的AP能力太感人了),在應用裡面做巢狀迴圈處理的時候發現十分的緩慢,看到cnblogs的網友有做優化,遂就順帶就學了一手,似乎是好了許多,但是對於極致效能追求的我怎能就這樣馬馬虎虎地過呢。。。oh不能!!!
現在開始: show me code ~?

程式碼及基本業務邏輯

我們是從db抽出兩撥資料,兩撥資料需要做匹配同時還要配合著配置項計算相關的金額,計算金額無非就是BigDecimal嘛,這裡略去哈~ ...下面我就demo出兩撥測試資料及最原始的程式碼邏輯,很簡陋哈~?
oh,對了,我電腦配置為8核16GB 256SSD => MacBook Pro ,所以各位電腦執行效率有差異很正常哈?

package com.mee.base;

import cn.hutool.core.collection.ConcurrentHashSet;
import org.junit.jupiter.api.Test;

import java.time.Instant;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

public class BigDataLoopTest {

    // 簡單的業務邏輯程式碼
    @Test
    public void test00(){
        List<Integer> lst_5w = this.build5W();
        List<Integer> lst_60w = this.build60W();
        Set<Integer> count = new HashSet<>(lst_5w.size());
        long s = Instant.now().toEpochMilli();
        for(int i = 0;i<lst_60w.size();i++){
            for(int j = 0;j<lst_5w.size();j++){
                Integer val = lst_5w.get(j);
                if(val%2==0 && lst_60w.get(i).equals(val)){
                    count.add(val);
                    System.out.println(val);
                    // 這裡加不加break似乎效能相差無幾~
//                    break;
                }
            }
        }
        System.out.println("匹配到個數"+count.size()+" 耗時"+(Instant.now().toEpochMilli()-s)/1000D+"秒");
    }

   //  60萬資料
    private List<Integer> build60W(){
        List<Integer> lst = new ArrayList<>(600000);
        SHOT_STATIC_IT.getAndSet(100000);
        for(int i=0;i<600000;i++){
            lst.add( genSeq());
        }
        return lst;
    }

   // 5萬資料
    private List<Integer> build5W(){
        List<Integer> lst = new ArrayList<>(100000);
        SHOT_STATIC_IT.getAndSet(1);
        for(int i=100000;i<600000;i++){
            int val = genSeq();
            if(val%7==0){
                lst.add(val);
            }
            if(lst.size()==50000){
                return lst;
            }
        }
        return lst;
    }

    // 構造數
    private static final AtomicInteger SHOT_STATIC_IT = new AtomicInteger(1);
    public static int genSeq(){
        if(SHOT_STATIC_IT.intValue()>990000){
            SHOT_STATIC_IT.getAndSet(1);
        }
        return SHOT_STATIC_IT.getAndIncrement();
    }

}

整體耗時 60.318秒 64.304秒`

以上test00部分即為業務邏輯,不用笑話,寫的確實很low哈哈,主要就是比較兩撥資料匹配到的列印出來,同時這個數要能被2整除才行~ ,當然接下來的優化主要針對test00進行優化哈~?

第一波是看得到的優化::去掉不必要的冗餘迴圈+在需要的時候果斷break

這是看得到的優化:

    @Test
  public void test01(){
      List<Integer> lst_5w = this.build5W();
      List<Integer> lst_60w = this.build60W();
      Set<Integer> count = new HashSet<>(lst_5w.size());
      long s = Instant.now().toEpochMilli();
      for(int i = 0;i<lst_60w.size();i++){
          Integer val = lst_60w.get(i);
          if(val%2 == 0){
              for(int j = 0;j<lst_5w.size();j++){
                  Integer val2 = lst_5w.get(j);
                  if(val.equals(val2)){
                      count.add(val);
                      System.out.println(val2);
                      break;
                  }
              }
          }
      }
      System.out.println("匹配到個數"+count.size()+" 耗時"+(Instant.now().toEpochMilli()-s)/1000D+"秒");
  }

來,看看效率如何->9.958秒 10.123秒 (為兩次執行結果)
wow,太棒了,我們得到了6x左右的優化,贊?
試想一下,如果我們做一個功能,呼叫一次,使用者需要等待10s,這樣合適嘛?️,再試試看~

第二波優化::來自部落格網友的助攻->內大外小

這裡主要方式是將大list放到內層,小list迴圈放到外層,試試看:

public void test02(){
        List<Integer> lst_5w = this.build5W();
        List<Integer> lst_60w = this.build60W();
        Set<Integer> count = new HashSet<>(lst_5w.size());
        long s = Instant.now().toEpochMilli();
        for(int j = 0;j<lst_5w.size();j++){
            Integer val = lst_5w.get(j);
            if(val % 2 == 0) {
                for (int i = 0; i < lst_60w.size(); i++) {
                    if (lst_60w.get(i).equals(val)) {
                        count.add(val);
                        System.out.println(val);
                        break;
                    }
                }
            }
        }
        System.out.println("匹配到個數"+count.size()+" 耗時"+(Instant.now().toEpochMilli()-s)/1000D+"秒");
    }

執行時間為=>6.314秒 6.306秒(兩次執行結果)
相對於前一次,我們得到了40%的優化,看起來也不錯,只是還需要等6s+, 小小的一步。。。聽網友說,他們還有其他方案,再試試看~

第三波優化:for迴圈引數提出迴圈內+迴圈引數常量化final

程式碼示例:

    @Test
    public void test03(){
        List<Integer> lst_5w = this.build5W();
        List<Integer> lst_60w = this.build60W();
        Set<Integer> count = new HashSet<>(lst_5w.size());
        long s = Instant.now().toEpochMilli();
        int j;
        final int j_len = lst_5w.size();
        int i;
        final int i_len = lst_60w.size();
        for(j = 0;j<j_len;j++){
            Integer val = lst_5w.get(j);
            if(val % 2 == 0){
                for(i = 0;i<i_len;i++) {
                    if (lst_60w.get(i).equals(val)) {
                        count.add(val);
                        System.out.println(val);
                        break;
                    }
                }
            }
        }
        System.out.println("匹配到個數"+count.size()+" 耗時"+(Instant.now().toEpochMilli()-s)/1000D+"秒");
    }

oh,似乎沒有明顯的優化,而且執行效率也降低了許多哦?=> 7.382秒 6.376秒(兩次執行結果)
ennnn....,java提供的迴圈方式多種,病急的時候我們會亂投醫,尤為盲目的時候。。。

第四波優化:使用for增強方式=>for :

    @Test
    public void test04(){
        List<Integer> lst_5w = this.build5W();
        List<Integer> lst_60w = this.build60W();
        Set<Integer> count = new HashSet<>(lst_5w.size());
        long s = Instant.now().toEpochMilli();
        int i;
        final int i_len = lst_60w.size();
        for(Integer val:lst_5w){
            if(val % 2 == 0) {
                for (i = 0; i < i_len; i++) {
                    if (lst_60w.get(i).equals(val)) {
                        count.add(val);
                        System.out.println(val);
                        break;
                    }
                }
            }
        }
        System.out.println("匹配到個數"+count.size()+" 耗時"+(Instant.now().toEpochMilli()-s)/1000D+"秒");
    }

它似乎只回到了初次優化的效率=> 6.323秒 6.342秒(兩次執行結果) ;此時,我們遺忘了很久的工具它似乎帶來了一線光明 ?

第五波優化:並行流多執行緒=>parallelStream

 @Test
    public void test05(){
        List<Integer> lst_5w = this.build5W();
        List<Integer> lst_60w = this.build60W();
        Set<Integer> count = new ConcurrentHashSet<>(lst_5w.size());
        long s = Instant.now().toEpochMilli();
        final int i_len = lst_60w.size();
        lst_5w.parallelStream().forEach(val->{
            if(val % 2 == 0){
                for(int i = 0;i<i_len;i++) {
                    if (lst_60w.get(i).equals(val)) {
                        count.add(val);
                        System.out.println(val);
                        break;
                    }
                }
//                for(Integer val2:lst_60w){
//                    if (val2.equals(val)) {
//                        System.out.println(val);
//                        break;
//                    }
//                }
            }
        });
        System.out.println("匹配到個數"+count.size()+" 耗時"+(Instant.now().toEpochMilli()-s)/1000D+"秒");
    }

執行效率=> 2.61s 2.44s (兩次執行結果)
難以置信,它相比以上 整整提高了1倍的效率,當你以為在多執行緒下洋洋得意的時候,以為它只能在2.5s左右徘徊嘛???

NO NO NO。。。。☝️☝️☝️

第六波優化::終極優化之=>HashMap

我想,很多使用java多年的同學都很難想到此,其實一開始我也不知道???,只是一個偶然的時間瞟了一眼HashMap的原始碼 從此發現了天機。。。?
final code:

   public void test06(){
        List<Integer> lst_5w = this.build5W();
        List<Integer> lst_60w = this.build60W();
        final Integer value = 1;
        Set<Integer> count = new HashSet<>(lst_5w.size());
        HashMap<Integer,Integer> map_60w = new HashMap<>(lst_60w.size(),1);
        for(Integer key:lst_60w){
            map_60w.put(key,value);
        }
        long s = Instant.now().toEpochMilli();
        for(Integer val:lst_5w){
            if(val % 2 == 0) {
                Integer val2 = map_60w.get(val);
                if (null!=val2 /*&& val2.equals(val)*/) {
                    count.add(val);
                    System.out.println(val);
                    continue;
//                    break;
                }
            }
        }
        System.out.println("匹配到個數"+count.size()+" 耗時"+(Instant.now().toEpochMilli()-s)/1000D+"秒");
    }

oh,天。。。它只需要=>0.082秒 0.099秒 0.095秒 (三次執行結果)
我只是試試看的心態,結果著實震撼到我了...0.1s都不需要,不要自行車,不要摩托車,我們只要?

最後

>>> 60/0.095
631.578947368421

500x的效率提升,標題著實有點兒保守了,各位不妨在各自電腦上試試看,當然如果您有其他優化思路 麻煩也告知下哈(建設性的更好)???
現在是 2022-03-07 21:50 各位晚安?

相關文章