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、 重复上述步骤直至只剩一个有序文件;