1. 程式人生 > >答辯問題雜談

答辯問題雜談

1、“抖動”導致演算法效能下降

/**
     * 抖動 27146ms
     * @param a
     * @param b
     * @return
     */
    public static int[][] matrixClassic(int[][] a, int[][]b){

        if(a[0].length != b.length){
            return null;
        }
        int y = a.length;
        int x = b[0].length;
        int c[][] = new int[y][x];

        for(int i = 0; i < y ; i++ ){
            for(int j = 0; j < x; j++){
                for(int k = 0; k < b.length; k++){
                    c[i][j] += a[i][k] * b[k][j];
                }
            }
        }

        return c;
    }


    /**
     * 抖動改進版 4230 ms
     * @param a
     * @param b
     * @return
     */
    public static int[][] matrixImprove(int[][] a, int[][]b){

        if(a[0].length != b.length){
            return null;
        }
        int y = a.length;
        int x = b[0].length;
        int c[][] = new int[y][x];

        for(int i = 0; i < y ; ++i ){
            for(int k = 0; k < y; ++k){
                int temp = a[i][k];
                for(int j = 0; j < x; ++j){
                    c[i][j] += temp * b[k][j];
                }
            }
        }

        return c;
    }

        在禁用系統虛擬記憶體之後,兩個演算法耗時上沒有任何質的變化;

        CPU I5 6500 四核心四執行緒 三級快取6M

        曾經以為記憶體是很快很快的,當時不敢相信27ms是完全在記憶體中執行的(對計算機理解太膚淺了,努力學習希望對計算機有個相對深入的正確理解),當然事實擺在眼前。靜靜想想,演算法本身沒有複雜度的區別,這麼大的差距是和硬體有關聯。既然資料都一次性讀取到記憶體中,那麼和CPU之間只差快取記憶體了,I5 6500只有6M快取,也就是說這個“抖動”發生在Cache中。作業系統很複雜,我還不清楚Cache具體怎麼分配的,但是顯然十幾兆的檔案不可能一次性放到Cache中。假設二維矩陣按行讀到Cache,那麼matrixClassic在j層的迴圈會不斷的從主存中調入到Cache,而matrixImprove在k層將值儲存起來,j層實際上變化的只有b矩陣的列標,也即是說j層的變動都在行上面,也就減少了Cache和主存的調換。突然發現這是一個很好的區域性性原理的體現。當禁用CPU快取之後,得到的結果理論上沒有什麼區別(我花了好久想去禁用快取,但是聯想電腦實在沒找到,百度說品牌電腦可能被廠商禁用了)。

2、對於partitionedMatrix執行緒數設定多少最合適(針對I5 6500 8G記憶體)

 /**
     * 分塊矩陣 1061ms
     * @param a
     * @param b
     * @return
     */
    public static int[][] partitionedMatrix(int[][] a, int[][] b, int d, int t) throws InterruptedException {

        if(a[0].length != b.length){
            return null;
        }
        int y = a.length;
        int x = b[0].length;
        int c[][] = new int[y][x];
        final int length = a.length/d;
        final int[][] af = a;
        final int[][] bf = b;
        final int[][] cf = c;
        final int df = d;

        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(t);

        for(int i = 0; i < d; ++i){
            final int index = i;
            fixedThreadPool.execute(new Runnable() {
                public void run() {
                    matrixImprove( af,  bf,  cf, index * length, length, df);
                }
            });
        }

        fixedThreadPool.shutdown();
        fixedThreadPool.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);

        return c;
    }


    public static void matrixImprove(int[][] a, int[][]b, int[][] c, int start, int length, int d){

        for(int t = 0; t < d; ++t){
            for(int i = start; i < start + length ; ++i ){
                for(int k = 0; k < b.length; ++k){
                    int temp = a[i][k];
                    for(int j = t * length; j < (t + 1) * length; ++j){
                        c[i][j] += temp * b[k][j];
                    }
                }
            }
        }

    }

        首先CPU流水線工作的時候,根據統籌思想要儘可能讓佔用最長的最充分利用,所以IO密集型一般有:執行緒數 = CPU核心數/(1-阻塞係數)而計算密集型有:執行緒數 = CPU核心執行緒數*2(為啥是2倍而不是+1之類的或許和JAVA本身的優化還有硬體也有關係吧,後續再研究,先打好基礎哈哈);我們來做以下嘗試(是以10個矩陣順序相乘,畢竟一次相乘誤差影響比重過大):

 執行緒數

任務數

2

4

6

8

10

16

16

1141

1109

1141

1156

1047

1440

84

1547

1172

1109

1235

1156

1109

256

1954

1500

1344

1375

1391

1438

                                                                                                                                            Ps:表中時間以ms計

        從表中分析任務控制在84左右比較理想,而執行緒數控制在8左右也即是CPU執行緒的兩倍左右比較理想,任務或者執行緒過多本身消耗就變成一個很大的負擔。至於如何尋找出具體情況下的理想執行緒數並沒有固定的公式,我想目前只能根據自身知識經驗結合具體情況親自測一測才能得出結論,當然如果不是要求那麼高,理論知識已經告我我們大致的範圍了。


3、10G檔案,1G主存,排序

        特地回去找教材確認了下外排和內排;外部排序指的是大檔案的排序,即待排序的記錄儲存在外儲存器上,待排序的檔案無法一次裝入記憶體,需要在記憶體和外部儲存器之間進行多次資料交換,以達到排序整個檔案的目的;內部排序是指待排序列完全存放在記憶體中所進行的排序過程;上學的時候只學習了十種左右的內部排序演算法,外排僅僅有這麼一個概念。然而理論與實際的差距就體現出來了,單單從理論上講演算法和內排外排都沒有關係,但是實際上任何一個演算法想在計算機上實現都不可能與硬體分離,這時候也就涉及到外排和內排了,而工作中最常用的也就是多路歸併排序(其實除了這個我也沒找到其他外部排序演算法,可能這就是工作經驗豐富的人用歸併排序指代外部排序的原因吧)

        首先,對於檔案大於主存,主存作為瓶頸資源應當充分利用,但是檔案如果拆分成一份1G的話那麼,讀檔案—排序—寫檔案的順序執行有種單道批處理的即視感,所以根據流水線思想把記憶體拆分成多塊(具體拆分成幾塊要根據演算法耗時和IO耗時來確定,這邊暫且定為兩份以做示例)。將10G檔案拆分成20個512M檔案,每次讀入512M並排序(隨便內部排序),同時有一份512M記憶體進行IO。這裡在進行步驟2歸併的時候,可以用堆或者敗者樹來獲取最小值(至於二者區別有時間我也想研究下)。順便提一下堆的定義:堆中某個節點的值總是不大於或不小於其父節點的值;堆總是一棵完全二叉樹。說來有點尷尬,堆其實就是老師教的大根堆和小根堆。

演算法流程如下:

1、   每次讀入512M檔案進入主存並排序,排序之後輸出檔案file_num.dat;

2、   將k個檔案的第一個資料取出比較,獲取最小的值,迴圈上述步驟並輸出到檔案中形成新的檔案,即將k個file_num.dat合併為一個有序檔案輸出,;

3、   重複上述步驟直至只剩一個有序檔案;