归并排序(C++ mpi 并行实现)

news2024/9/23 1:23:50

文章目录

  • 主要思路
  • 1. 串行归并排序
  • 2. 进程的分发
  • 3. 对接收到的子数组进行排序
  • 4. 合并数组
  • 5.输出排序后的数组
  • 6.进程分发部分的优化
  • 7.完整代码

主要思路

  1. 我们首先实现串行的归并排序;
  2. 实现进程的分发;
  3. 排序其中的每个子部分;
  4. 进程的合并通信,并实现对有序子数组的归并(注意,这里的合并复杂度应该是O(n)的,不然并行就失去了意义)。

通过以上4步,就可以实现并行的归并排序了。

1. 串行归并排序

// 归并排序,输入一个vector的引用
void mergeSort(vector<int>& vec) {
	if (vec.size() <= 1) return;
	int mid = vec.size() / 2;
	vector<int> left(vec.begin(), vec.begin() + mid);
	vector<int> right(vec.begin() + mid, vec.end());
	mergeSort(left);
	mergeSort(right);
	int i = 0, j = 0, k = 0;
	while (i < left.size() && j < right.size()) {
		if (left[i] < right[j]) {
			vec[k++] = left[i++];
		}
		else {
			vec[k++] = right[j++];
		}
	}
	while (i < left.size()) {
		vec[k++] = left[i++];
	}
	while (j < right.size()) {
		vec[k++] = right[j++];
	}
}

2. 进程的分发

进程如何分发?看下面这张图:

在这里插入图片描述

这里就需要计算数组的划分次数,到底划分多少份?每个进程一份。

我起初实现了一个很简单的做法,就是由 0进程 来划分,然后分发给其它进程。

    if (myrank == 0) {
        // 计算每个进程分配完,多余出来的数量
        int remain = numberNums % processNum;
        // 主进程 排序 自己的 + 多余出来的
        subVec = vector<int>(vec.begin(), vec.begin() + n + remain);
        for (int i = 1; i < processNum; i++) {
            MPI_Send(vec.data() + i * n + remain, n,
                MPI_INT, i, 0, MPI_COMM_WORLD);
        }
    }
    else {
        // 接收数组
        MPI_Recv(subVec.data(), n, MPI_INT,
            0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
    }

其中的 numberNums 表示数组大小,processNum 表示进程数量。

值得注意的是,如果数组没有被进程刚好划分,也就是有余数(remain),我是这样处理的,让 0进程 来多排序一下,最终划分结果即:

  • 0进程 :排序 n+remain 个;
  • 其它进程 :排序 n 个。

3. 对接收到的子数组进行排序

这一步比较简单,对于每一个进程都是相同的,即:排序(干活)。

    // 对接收到的子数组进行排序    
    mergeSort(subVec);

4. 合并数组

数组的合并的主要思路如下:

在这里插入图片描述

就是两两进行合并,注意,这里不是一个进程合并所有其它的,那样的时间复杂度是 O ( n 2 ) O(n^2) O(n2),即合并操作是在一个进程中完成的。

我们需要让合并的操作再多个进程中完成,这样时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn):这里没有考虑进程通信的成本,也没有考虑的必要)

我们首先需要计算出合并的次数,合并的次数是由进程的数量决定的。

如果有 8 个进程,那么我们两两合并,需要合并 3 = l o g 2 ( 8 ) 3=log_2(8) 3=log2(8) 次。

那么如果有 7 个进程呢?合并过程如下图所示:

在这里插入图片描述

树的层数(合并的)是没有发生变化的。

如果有 6 个进程,合并过程如下图:

在这里插入图片描述

树的层数(合并次数)同样没有发生变化。

因此我们的合并次数应该为进程数对2的对数,然后向上取整就不难理解了。

m e r g e T i m e s = c e i l ( l o g 2 ( p r o c e s s N u m ) ) mergeTimes=ceil(log_2(processNum)) mergeTimes=ceil(log2(processNum))

具体代码如下:

    // 合并数组
    // 计算合并的次数
    int mergeTimes = ceil(log2(processNum));

    for (int i = 0; i < mergeTimes; i++) {
        // 判断当前进程 在 第 i 个轮次 是否需要接收数据
        if (myrank % int(pow(2, i + 1)) == 0) {
            // 计算当前进程的下一个进程
            int nextProcess = myrank + pow(2, i);
            // 如果下一个进程存在,那么就接收下一个进程的数据
            if (nextProcess < processNum) {
                // 接收数组的大小
                int vecNum;
                MPI_Recv(&vecNum, 1, MPI_INT, nextProcess, 0,
                    MPI_COMM_WORLD, MPI_STATUS_IGNORE);
                // 接收数组
                vector<int> recvVec(vecNum, 0);
                MPI_Recv(recvVec.data(), vecNum, MPI_INT,
                    nextProcess, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
                // 合并数组
                subVec = merge(subVec, recvVec);
            }
        }

		// 判断当前进程 在 第 i 个轮次 是否需要发送数据给前边的进程
        if ((myrank + int(pow(2,i))) % int(pow(2, i + 1)) == 0) {
            // 计算上一个进程
            int preProcess = myrank - pow(2, i);
            // 发送数组的大小
            int vecNum = subVec.size();
            MPI_Send(&vecNum, 1, MPI_INT, preProcess,
                0, MPI_COMM_WORLD);
            // 发送数组
            MPI_Send(subVec.data(), subVec.size(),
                MPI_INT, preProcess, 0, MPI_COMM_WORLD);
        }
    }

关于两个 if 条件判断的理解,主要是两两合并的思想。

1 2合并,3 4合并,5 6合并 7 8合并;
1 3 合并,5 7合并;
1 5合并。

这样合并 3 次就可以了,至于判断到底为什么这样写?主要利用了进程号合并轮次两个参数控制的。

读者需要自己想一下,然后再根据代码看一下,进行理解。

其中,merge函数部分的代码主要功能是,将两个有序数组合并成一个,代码如下:

// vec1 和 vec2 是两个有序数组,将其合并为一个有序数组
vector<int> merge(vector<int> vec1, vector<int> vec2) {
    int i = 0, j = 0;
    vector<int> res;
    while(i < vec1.size() && j < vec2.size()) {
        if (vec1[i] < vec2[j]) {
			res.push_back(vec1[i++]);
		}
        else {
			res.push_back(vec2[j++]);
		}
	}
    while (i < vec1.size()) {
        res.push_back(vec1[i++]);
    }
    while (j < vec2.size()) {
        res.push_back(vec2[j++]);
	}
    return res;
}

5.输出排序后的数组

最后输出排序后的数组,具体代码如下:

    // 输出排序后的数组
    if (myrank == 0) {
        // 计算排序时间
        cout << "time=" << (MPI_Wtime() - startTime) * 1000 << " ms" << endl;
        
        // 输出排序后的数组
        for (int i = 0; i < subVec.size(); i++) {
			cout << subVec[i] << " ";
		}
		cout << endl;
	}

6.进程分发部分的优化

我们是采用主进程进行数组分发的,一个进程进行数组的分发,时间复杂度为 O ( n ) O(n) O(n),相对较低。

另一种方法是,多个进程同时进行分发,时间复杂度为 O ( l o g n ) O(logn) O(logn)

主要思想如下:

在这里插入图片描述

编程的思路就是,首先接收数据。

然后计算需要划分的轮次。

根据轮次和进程号,得到每个进程需要发送消息的进程的进程号。

具体代码如下:

    // 计算划分次数
    int splitTimes = ceil(log2(processNum));

    if (myrank != 0) {
        // 计算当前进程的上一个进程
        int preProcess = getPreProcess(myrank);
        // 接收数组的大小
        int vecNum;
        MPI_Recv(&vecNum, 1, MPI_INT, preProcess,
            0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
        cout << "process=" << myrank << " receive " << 
            vecNum << " numbers from " << preProcess << endl;
        // 接收数组
        subVec = vector<int>(vecNum, 0);
        MPI_Recv(subVec.data(), vecNum, MPI_INT, preProcess,
            0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
    }

    for (int i = splitTimes; i > 0; i--) {
        // 初始化 0 号进程的子数组
        if (myrank == 0 && i == splitTimes)
            subVec = vector<int>(vec.begin(), vec.end());
        // 如果当前进程是 2 的 i 次方的倍数,那么就发送数据
        if (myrank % int(pow(2, i)) == 0) {
            // 计算当前进程的下一个进程
            int nextProcess = myrank + pow(2, i - 1);
            // 如果下一个进程存在,那么就发送数据给下一个进程
            if (nextProcess < processNum) {
                // 发送数组的大小
                int vecNum = subVec.size() - subVec.size() / 2;
                cout<<"process="<<myrank<<" send "<<vecNum<<" numbers to "<<nextProcess<<endl;
                MPI_Send(&vecNum, 1, MPI_INT, nextProcess,
                    0, MPI_COMM_WORLD);
                // 发送数组
                // subVec = vector<int>(subVec.begin() + subVec.size() / 2, subVec.end());
                MPI_Send(subVec.data() + subVec.size() / 2, vecNum,
                    MPI_INT, nextProcess, 0, MPI_COMM_WORLD);
                subVec = vector<int>(subVec.begin(), subVec.begin() + subVec.size() / 2);
            }
        }
    }

代码思路也有些绕,其中还涉及到一个 int getPreProcess(int curProcessNum) 函数,该函数主要用来求解给当前进程发送信息的进程的进程号,我们得到了之后好去接收它。

主要就是找比当前进程数小的,2的幂次方的累加和,但注意是要贪心去找,比如:

7->4+2
6->4
5->4
4->0
3->2
2->0
1->0

具体代码如下:

int getPreProcess(int curProcessNum) {
    int tmp;
    int sum = 0;
    while (sum < curProcessNum) {
        tmp = 2;
        bool flag = false;
        while (sum + tmp < curProcessNum) {
            tmp *= 2;
        }
        if (tmp == 2 || sum + tmp == curProcessNum) {
            return sum;
        }
        else {
            sum += tmp / 2;
        }
    }
}

说的还是很绕。

还是下面这张图,每个进程只接收一次消息,但是可能会发送多次消息。

在这里插入图片描述

因此,我们上面可以得到这个进程接收消息的进程号。

然后再去求它发送消息的进程号就可以了,具体还是看代码。

因为划分并不是该并行算法的性能瓶颈,最大的运算量还是在各个子进程的 mergeSort函数 以及 merge函数 上面。

所以划分这块性能看上去并没有过于明显的差别。下面是我按照两种划分方法,排序100万数据量的数组,所耗费的时间(单位:ms)对比:

在这里插入图片描述

可以看到单个进程划分效率看起来更高一些。具体的原因,我推测可能是 getPreProcess函数 的时间复杂度较大导致的。

7.完整代码

#include<vector>
#include<iostream>
#include<mpi.h>

using namespace std;
// 归并排序,输入一个vector的引用
void mergeSort(vector<int>& vec) {
	if (vec.size() <= 1) return;
	int mid = vec.size() / 2;
	vector<int> left(vec.begin(), vec.begin() + mid);
	vector<int> right(vec.begin() + mid, vec.end());
	mergeSort(left);
	mergeSort(right);
	int i = 0, j = 0, k = 0;
	while (i < left.size() && j < right.size()) {
		if (left[i] < right[j]) {
			vec[k++] = left[i++];
		}
		else {
			vec[k++] = right[j++];
		}
	}
	while (i < left.size()) {
		vec[k++] = left[i++];
	}
	while (j < right.size()) {
		vec[k++] = right[j++];
	}
}

// vec1 和 vec2 是两个有序数组,将其合并为一个有序数组
vector<int> merge(vector<int> vec1, vector<int> vec2) {
    int i = 0, j = 0;
    vector<int> res;
    while(i < vec1.size() && j < vec2.size()) {
        if (vec1[i] < vec2[j]) {
			res.push_back(vec1[i++]);
		}
        else {
			res.push_back(vec2[j++]);
		}
	}
    while (i < vec1.size()) {
        res.push_back(vec1[i++]);
    }
    while (j < vec2.size()) {
        res.push_back(vec2[j++]);
	}
    return res;
}

int getPreProcess(int curProcessNum) {
    int tmp;
    int sum = 0;
    while (sum < curProcessNum) {
        tmp = 2;
        bool flag = false;
        while (sum + tmp < curProcessNum) {
            tmp *= 2;
        }
        if (tmp == 2 || sum + tmp == curProcessNum) {
            return sum;
        }
        else {
            sum += tmp / 2;
        }
    }
}

double startTime;

int main(int argc, char* argv[]) {
    int myrank, processNum;
    char processor_name[MPI_MAX_PROCESSOR_NAME];
    int namelen;

    MPI_Init(&argc, &argv);
    // 当前进程的编号
    MPI_Comm_rank(MPI_COMM_WORLD, &myrank);
    // 进程总数
    MPI_Comm_size(MPI_COMM_WORLD, &processNum);

    // 数据的数量
    int numberNums = 10;
    vector<int> vec;
    // 初始化数据
    if (myrank == 0) {
        srand(time(NULL));
        // 随机生成10个数
        for (int i = 0; i < numberNums; i++) {
            vec.push_back(rand() % 100);
        }

        // 记录开始时间
        startTime = MPI_Wtime();
    }

    // 每个进程排序的数量
    int n = numberNums / processNum;
    // 分出来的子数字
    vector<int> subVec(n, 0);

    // 如果是主进程,那么就进行子数组的分发
    //if (myrank == 0) {
    //    // 计算每个进程分配完,多余出来的数量
    //    int remain = numberNums % processNum;
    //    // 主进程 排序 自己的 + 多余出来的
    //    subVec = vector<int>(vec.begin(), vec.begin() + n + remain);
    //    for (int i = 1; i < processNum; i++) {
    //        MPI_Send(vec.data() + i * n + remain, n,
    //            MPI_INT, i, 0, MPI_COMM_WORLD);
    //    }
    //}
    //else {
    //    // 接收数组
    //    MPI_Recv(subVec.data(), n, MPI_INT,
    //        0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
    //}
    
    // 计算划分次数
    int splitTimes = ceil(log2(processNum));

    if (myrank != 0) {
        // 计算当前进程的上一个进程
        int preProcess = getPreProcess(myrank);
        // 接收数组的大小
        int vecNum;
        MPI_Recv(&vecNum, 1, MPI_INT, preProcess,
            0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
        cout << "process=" << myrank << " receive " << 
            vecNum << " numbers from " << preProcess << endl;
        // 接收数组
        subVec = vector<int>(vecNum, 0);
        MPI_Recv(subVec.data(), vecNum, MPI_INT, preProcess,
            0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
    }

    for (int i = splitTimes; i > 0; i--) {
        // 初始化 0 号进程的子数组
        if (myrank == 0 && i == splitTimes)
            subVec = vector<int>(vec.begin(), vec.end());
        // 如果当前进程是 2 的 i 次方的倍数,那么就发送数据
        if (myrank % int(pow(2, i)) == 0) {
            // 计算当前进程的下一个进程
            int nextProcess = myrank + pow(2, i - 1);
            // 如果下一个进程存在,那么就发送数据给下一个进程
            if (nextProcess < processNum) {
                // 发送数组的大小
                int vecNum = subVec.size() - subVec.size() / 2;
                cout<<"process="<<myrank<<" send "<<vecNum<<" numbers to "<<nextProcess<<endl;
                MPI_Send(&vecNum, 1, MPI_INT, nextProcess,
                    0, MPI_COMM_WORLD);
                // 发送数组
                // subVec = vector<int>(subVec.begin() + subVec.size() / 2, subVec.end());
                MPI_Send(subVec.data() + subVec.size() / 2, vecNum,
                    MPI_INT, nextProcess, 0, MPI_COMM_WORLD);
                subVec = vector<int>(subVec.begin(), subVec.begin() + subVec.size() / 2);
            }
        }
    }

    // 对接收到的子数组进行排序    
    mergeSort(subVec);

    // 合并数组
    // 计算合并的次数
    int mergeTimes = ceil(log2(processNum));

    for (int i = 0; i < mergeTimes; i++) {
        // 判断当前进程 在 第 i 个轮次 是否需要接收数据
        if (myrank % int(pow(2, i + 1)) == 0) {
            // 计算当前进程的下一个进程
            int nextProcess = myrank + pow(2, i);
            // 如果下一个进程存在,那么就接收下一个进程的数据
            if (nextProcess < processNum) {
                // 接收数组的大小
                int vecNum;
                MPI_Recv(&vecNum, 1, MPI_INT, nextProcess, 0,
                    MPI_COMM_WORLD, MPI_STATUS_IGNORE);
                // 接收数组
                vector<int> recvVec(vecNum, 0);
                MPI_Recv(recvVec.data(), vecNum, MPI_INT,
                    nextProcess, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
                // 合并数组
                subVec = merge(subVec, recvVec);
            }
        }

		// 判断当前进程 在 第 i 个轮次 是否需要发送数据给前边的进程
        if ((myrank + int(pow(2,i))) % int(pow(2, i + 1)) == 0) {
            // 计算上一个进程
            int preProcess = myrank - pow(2, i);
            // 发送数组的大小
            int vecNum = subVec.size();
            MPI_Send(&vecNum, 1, MPI_INT, preProcess,
                0, MPI_COMM_WORLD);
            // 发送数组
            MPI_Send(subVec.data(), subVec.size(),
                MPI_INT, preProcess, 0, MPI_COMM_WORLD);
        }
    }

    // 输出排序后的数组
    if (myrank == 0) {
        // 计算排序时间
        cout << "time=" << (MPI_Wtime() - startTime) * 1000 << " ms" << endl;
        
        // 输出排序后的数组
        for (int i = 0; i < subVec.size(); i++) {
			cout << subVec[i] << " ";
		}
		cout << endl;
	}

    MPI_Finalize();

    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/891007.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

宝藏级画图工具-drawio

今天推荐一款非常好用的免费开源画图工具drawio. Drawio即可以下载安装到本地&#xff0c;也可以在线编辑&#xff0c;在线编辑网址为 https://app.diagrams.net/。 本地版下载地址为https://github.com/jgraph/drawio-desktop/releases 1、支持各类图形 Drawio可以非常便捷…

Docker服务编排Docker Compose介绍

1.服务编排概念 2.Docker Compose介绍 3.Docker Compose安装及使用

【【STM32----I2C通信协议】】

STM32----I2C通信协议 我们会发现I2C有两根通信线&#xff1a; SCL和SDA 同步 半双工 带数据应答 支持总线挂载多设备&#xff08;一主多从&#xff0c;多主多从&#xff09; 硬件电路 所有I2C设备的SCL连在一起&#xff0c;SDA连在一起 设备的SCL和SDA均要配置成开漏输出模式 …

MySQL— 基础语法大全及操作演示!!!(下)

MySQL—— 基础语法大全及操作演示&#xff08;下&#xff09;—— 持续更新 三、函数3.1 字符串函数3.2 数值函数3.3 日期函数3.4 流程函数 四、约束4.1 概述4.2 约束演示4.3 外键约束4.3.1 介绍4.3.2 语法4.3.3 删除/更新行为 五、多表查询5.1 多表关系5.1.1 一对多5.1.2 多对…

hdu8-Congruences(中国剩余定理)

Problem - 7363 (hdu.edu.cn) 参考&#xff1a;2023杭电暑假多校8 题解 3 5 7 10 | JorbanS_JorbanS的博客-CSDN博客 题解&#xff1a;&#xff08;中国剩余定理 增量法&#xff09; 注意验证和特判&#xff0c;此题中 pi 两两互质&#xff0c;可用CRT和增量法&#xff0c;当…

ipkvm之RK3568高温测试

1. 简介 KVM高温测试描述&#xff1a; 将KVM主板放入50℃的温箱中放置4个小时。四个小时后记录VGA小板的温度以及SOC温度和外壳温度。 测试仪器&#xff1a; 两块KVM主板&#xff0c;温度记录仪&#xff0c;两个串口&#xff0c;笔记本&#xff0c;电源 KVM主板上电和串口 …

【回溯】总结

1、 组合和子集问题 组合问题需要满足一定要求才算作一个答案&#xff0c;比如数量要求&#xff08;k个数&#xff09;&#xff0c;累加和要求&#xff08;target&#xff09;。 子集问题是只要构成一个新的子集就算作一个答案。 进阶&#xff1a;去重逻辑。 一般都是要对同…

星际争霸之小霸王之小蜜蜂(二)--类的使用

目录 前言 一、将设置内容写在一个类里 二、设置小蜜蜂的造型 三、设置猫蜜蜂的参数 四、绘制猫蜜蜂到窗口 总结 前言 昨天我们设置好了窗口&#xff0c;下面我们需要向窗口中添加元素了。 一、将设置内容写在一个类里 我个人理解书上的意思是要创建一个类&#xff0c;将所有需…

厘米级定位技术主要有以下几种

厘米级定位技术主要有以下几种&#xff1a; 1.UWB (Ultra-Wideband) 定位技术&#xff1a;UWB技术使用一组频带非常宽的无线电脉冲来实现高精度定位&#xff0c;可以实现亚米级的定位精度&#xff0c;甚至在理想条件下可以达到厘米级的定位精度。 2.RTK (Real-Time Kinematic…

python3 0基础学习----基本知识

0基础学习笔记之基础知识 &#x1f4da; 基础内容1. 条件语句 if - elif - else2. 错误铺捉try - except(一种保险策略&#xff09;3. 四种开发模式4. 函数&#xff1a;def用来定义函数的5. 最大值最小值函数&#xff0c;max &#xff0c;min6. is 严格的相等&#xff0c;is no…

Vault主题 - UiCore多用途Elementor WordPress主题

你可以使用Vault主题 – UiCore多用途Elementor WordPress主题构建什么&#xff1f; Vault主题拥有专业、像素级完美且干净的现代布局&#xff0c;几乎适合您需要的任何网站&#xff1a; 小型企业网站企业网站着陆页面权威博客销售和营销页面网上商店 自由职业者的最佳选择 …

VM——流程自动连续运行

1、需求&#xff1a;使用一个流程连续运行&#xff0c;充当独立线程&#xff0c;监控外部IO输入。 2、方法&#xff1a;某个流程连续运行&#xff0c;需要在界面设置控制按钮&#xff0c;这不符合要求。需要程序启动后自动连续运行。咨询海康技术人员、网上查资料后&#xff0…

JS逆向-某招聘平台token

前言 本文是该专栏的第56篇,后面会持续分享python爬虫干货知识,记得关注。 通常情况下,JS调试相对方便,只需要chrome或者一些抓包工具,扩展插件,就可以顺利完成逆向分析。目前加密参数的常用逆向方式大致可分为以下几种,一种是根据源码的生成逻辑还原加密代码,一种是补…

双向-->带头-->循环链表

目录 一、双向带头循环链表概述 1.什么是双向带头循环链表 2.双向带头循环链表的优势 3.双向带头循环链表简图 二、双向带头循环链表的增删查改图解及代码实现 1.双向带头循环链表的头插 2.双向带头循环链表的尾插 3.双向带头循环链表的头删 4.双向带头循环链表的尾删…

使用Pillow对图像进行变换

使用Pillow对图像进行变换 from PIL import Image, ImageEnhance# 原图 image Image.open("1.jpg") image.show()# 镜像 mirrored_image image.transpose(Image.FLIP_LEFT_RIGHT) mirrored_image.show() mirrored_image.save(mirror_image.jpg)# 旋转 rotated_imag…

day12 13-牛客67道剑指offer-JZ83、70、63、47、48、46、21、81

1. JZ83 剪绳子&#xff08;进阶版&#xff09; class Solution { public:int jumpFloorII(int number) {if(number < 1) return number;int temp 1;int res 0;/*2级台阶 23级台阶 44级台阶 65级台阶 16*/for(int i2; i<number; i){res 2 * temp;temp res;}return re…

ts与vue

ts与Vue 如果你已经学习了typeScript,但不知道如何在vue项目中使用&#xff0c;那么这篇文章将会很适合你。参考千峰教育 kerwin视频 1.会自动推导&#xff0c;隐士推导。提示 类型系统。 独立模块。 isolatedModules选项&#xff1a;是否配置为独立的模块。 减少报错 let …

innodb索引与算法

B树主键插入 B树在innodb的插入有三种模式page_last_insert, page_dirction, page_N_direction 而在bustub里面的B树就是page_N_direction,如果是自增主键的话&#xff0c;就是上面这样的插入法 FIC优化 (DDL) 选择性统计 覆盖索引 MMR ICP优化 自适应hash 全文索引

乖宝宠物上市,能否打破外资承包中国宠物口粮的现实

近日&#xff0c;乖宝宠物上市了&#xff0c;这是中国宠物行业成功挂牌的第三家公司。同时&#xff0c;昨日&#xff0c;宠物行业最大的盛事“亚洲宠物展”时隔3年&#xff0c;于昨日在上海成功回归。 这两件事情的叠加可谓是双喜临门&#xff0c;行业能够走到今天实属不易&…

09- DMA(DirectMemoryAccess直接存储器访问)

DMA 09 、DMA(DirectMemoryAccess直接存储器访问)DMA配置流程 09 、DMA(DirectMemoryAccess直接存储器访问) DMA配置流程 dma.c文件 main.c文件 详见《stm32中文参考手册》表57。