作业二
计组原理:
1.cache分类和特点替换机制
cache按照不同标准分类可以分为若干类:
按照数据类型划分:I-Cache与D-Cache。其中I-Cache负责放置指令,D-Cache负责方式数据。两者最大的不同是D-Cache里的数据可以写回,I-Cache是只读的。
按照大小划分:分为small cache和large cache。没路组(后文组相连介绍)<4KB叫small cache,多用于L1 Cache,>4KB的叫large cache。多用于L2及其他cache
按照位置划分:inner Cache和Outer Cache。一般独属于CPU微架构的叫inner cache,例如上图的L1 L2 Cache。不属于CPU微架构的叫outer cache
按照数据关系划分:inclusive/exclusive Cache:下级Cache包含上级的数据叫inclusive cache。不包含叫exclusive cache。L3 Cache里有L2 Cache的数据,则L2 Cache叫inclusive cache。
cache的特点替换机制:
**随机替换。**如果发生cache miss里随机替换掉一块。
**Least recently used. LRU。**最近使用的块最后替换。
**First in, first out (FIFO)。**先进先出。
第一个不经常使用
Cache 在什么时候数据会被替换内?也有几种策略。
- 不在本 Cache 替换。如果Cache miss了,直接转发访问地址到主存,取到的数据不会写到Cache.
- 在读MISS时替换。如果读的时候Cache里没有该数据,则从主存读取该数据后写入Cache。
- 在写MISS时替换。如果写的时候Cache里没有该数据,则将本数据调入Cache再写。
2.寄存器和cache哪一个更快,为什么
寄存器更快, 寄存器本身的结构决定了它能比构成Cache的结构工作在更高的时钟频率,所以才用在速度需求最快的地方(靠近ALU)。
cache和寄存器的基本组成单元都是D触发器构成的
SRAM使用了更加复杂的电路设计,包括了多路选择器、反相器、传输门等,这些电路的设计使得SRAM具有了较大的存储容量和更好的性能,但是也导致了SRAM的结构更为复杂。
而寄存器的触发器相对简单,只需要一个D触发器就可以实现一个寄存器的功能,因此它们的结构更为简单。此外,寄存器的触发器一般被直接放置在处理器的芯片中,因此它们的时钟速度也更快。
3.主存实现方式
-
全相连(Fully associative)。可以放在cache的任何位置。
-
直接映射(Direct mapped)。只允许放在cache的某一行。比如12 mod 8
-
组相连(set associative)。可以放在cache的某几行。例如2路组相连,一共有4组,所以可以放在0,1位置中的一个。
4.cache一致性
主要是多核系统中,假如core 0读了主存储的数据,写了数据。core 1也读了主从的数据。这个时候core 1并不知道数据已经被改动了,也就是说,core 1 cache中的数据过时了,会产生错误。
Cache一致性的保证就是让多核访问不出错。
Cache一致性主要有两种策略。
策略一:基于监听的一致性策略
这种策略是所有cache均监听各cache的写操作,如果一个cache中的数据被写了,有两种处理办法。
**写更新协议:**某个cache发生写了,就索性把所有cache都给更新了。
**写失效协议:**某个cache发生写了,就把其他cache中的该数据块置为无效。
策略1由于监听起来成本比较大,所以只应用于极简单的系统中。
策略二:基于目录的一致性策略
这种策略是在主存处维护一张表。记录各数据块都被写到了哪些cache, 从而更新相应的状态。一般来讲这种策略采用的比较多。又分为下面几个常用的策略。
- SI: 对于一个数据块来讲,有share和invalid两种状态。如果是share状态,直接通知其他cache, 将对应的块置为无效。
- **MSI:**对于一个数据块来讲,有share和invalid,modified三种状态。其中modified状态表表示该数据只属于这个cache, 被修改过了。当这个数据被逐出cache时更新主存。这么做的好处是避免了大量的主从写入。同时,如果是invalid时写该数据,就要保证其他所有cache里该数据的标志位不为M,负责要先写回主存储。
- **MESI:**对于一个数据来讲,有4个状态。modified, invalid, shared, exclusive。其中exclusive状态用于标识该数据与其他cache不依赖。要写的时候直接将该cache状态改成M即可。
我们着重讲讲MESI。图中黑线:CPU的访问。红线:总线的访问,其他cache的访问。
当前状态时I状态时,如果发生处理器读操作prrd
- 如果其他cache里有这份数据,如果其他cache里是M态,先 把M态写回主存再读。否则直接读。最终状态变为S。
- 其他cache里没这个数据,直接变到E状态。
当前状态为S态
- 如果发生了处理器读操作,仍然在S态。
- 如果发生了处理器写操作,则跳转到M状态。
- 如果其他cache发生了写操作,跳到I态。
当前状态E态
- 发生了处理器读操作还是E。
- 发生了处理器写操作变成M。
- 如果其他cache发生了读操作,变到S状态。
当前状态M态
-
发生了读操作依旧是M态。
-
发生了写操作依旧是M态。
-
如果其他cache发生了读操作,则将数据写回主存储,变换到S态。
5.cache miss以及cache 命中详细流程
Cache Miss(缓存未命中):当查找的数据不在高速缓存中或在但是不可用(有效位为0)
6.TLB快表命中,不命中流程
TLB又称页表缓存,为了加速页表查询的 。快表是单独的寄存器,页表是存在于主存。 当CPU执行机构收到应用程序发来的虚拟地址后,首先到TLB中查找相应的页表数据,如果TLB中正好存放着所需的页表,则称为TLB命中(TLB Hit),接下来CPU再依次看TLB中页表所对应的物理内存地址中的数据是不是已经在一级、二级缓存里了,若没有则到内存中取相应地址所存放的数据。
**TLB是一个虚拟高速缓存。**硬件存在TLB后,虚拟地址到物理地址的转换过程发生了变化。虚拟地址首先发往TLB确认是否命中cache,如果cache hit直接可以得到物理地址。否则,一级一级查找页表获取物理地址。并将虚拟地址和物理地址的映射关系缓存到TLB中。
7.虚拟内存是啥
虚拟内存 使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。与没有使用虚拟内存技术的系统相比,使用这种技术的系统使得大型程序的编写变得更容易,对真正的物理内存(例如RAM)的使用也更有效率。目前,大多数操作系统都使用了虚拟内存,如Windows家族的“虚拟内存”;Linux的“交换空间”等。
8.指令流水线,经典五段式
每一个周期作为一个流水段;
在各段之间加上锁存器(流水寄存器)。
一条指令的执行过程分为以下5个周期:
取指令周期(IF)
以程序计数器PC中的内容作为地址,从存储器中取出指令并放入指令寄存器IR;
同时PC值加4(假设每条指令占4个字节),指向顺序的下一条指令。
指令译码/读寄存器周期(ID)
对指令进行译码,并用IR中的寄存器地址去访问通用寄存器组,读出所需的操作数。
执行/有效地址计算周期(EX)
不同指令所进行的操作不同:
load和store指令:ALU把指令中所指定的寄存器的内容与偏移量相加,形成访存有效地址。
寄存器-寄存器ALU指令:ALU按照操作码指定的操作对从通用寄存器组中读出的数据进行运算。
寄存器-立即数ALU指令:ALU按照操作码指定的操作对从通用寄存器组中读出的操作数和指令中给出的立即数进行运算。
分支指令:ALU把指令中给出的偏移量与PC值相加,形成转移目标的地址。同时,对在前一个周期读出的操作数进行判断,确定分支是否成功。
存储器访问/分支完成周期(MEM)
该周期处理的指令只有load、store和分支指令。
其它类型的指令在此周期不做任何操作。
load和store指令
load指令:用上一个周期计算出的有效地址从存储器中读出相应的数据;
store指令:把指定的数据写入这个有效地址所指出的存储器单元。
分支指令
分支“成功”,就把转移目标地址送入PC。
分支指令执行完成。
写回周期(WB)
ALU运算指令和load指令在这个周期把结果数据写入通用寄存器组。
ALU运算指令:结果数据来自ALU。
load指令:结果数据来自存储器。
在这个实现方案中:
分支指令需要4个时钟周期(如果把分支指令的执行 提前到ID周期,则只需要2个周期);
store指令需要4个周期;
其它指令需要5个周期才能完成。
9.在阅读完比较底层的内容后,思考下从哪里去着手优化(提示:问题1-8有提示作用),以及你自己对性能优化的理解
一:考虑内存访问问题,从以下几个方面入手:第一尽可能使重复多并且是有效的内存存在与cache中,第二考虑cache的排列,使cache命中率尽可能的高,第三在cache的一致性方面,进行基于目录的一致性策略,第四使常用的页表项存储于tlb中节省地址翻译时间(提高tlb命中率)(从虚拟地址到物理地址的翻译是一个漫长的过程),第五思考怎么优化寄存器。
并行导论:
1.OpenMP原理
创建多个线程,操作系统把这几个线程分到几个核上面同时执行,从而可以达到快速执行代码的目的。
2.OpenMP有什么特点?
- 易于编写:OpenMP使用一组简单易用的指令来实现并行化,程序员无需了解复杂的并行编程技术即可进行并行化编程。
- 可移植性:OpenMP是一个开放的标准,支持多种编译器和计算机平台,可以实现跨平台的并行化编程。
- 可扩展性:OpenMP支持从单机多核到大型并行计算机的并行化,可以根据问题规模和计算资源的不同进行灵活的扩展。
- 透明性:OpenMP实现了共享内存的并行化,使得程序员无需手动管理线程之间的通信和同步,降低了编程复杂度。
- 高效性:OpenMP采用轻量级的线程模型,能够实现低开销、高效率的并行化执行,提高程序的性能。
3.OpenMP如何开启并行区?
在OpenMP中,使用#pragma omp parallel
指令可以开启一个并行区,即使得多个线程同时执行一段代码块。下面是一个简单的示例:
#include <stdio.h>
#include <omp.h>
int main()
{
#pragma omp parallel
{
int tid = omp_get_thread_num();
printf("Hello, world! from thread %d\n", tid);
}
return 0;
}
4.Openmp中的single master critical atomic的区别
single
指令:指定只有一个线程执行单独的代码块。在该代码块中,任何其他线程都将被阻塞,直到该线程完成该代码块并释放锁。使用single
指令时,不需要任何同步机制来避免竞争条件,因为只有一个线程会访问该代码块。master
指令:指定线程池中的一个线程执行一个代码块。这个线程在指令之前被指定为主线程。在该代码块中,任何其他线程都将被阻塞,直到主线程完成该代码块并释放锁。与single
不同的是,使用master
指令时,必须确保其他线程不会同时访问与该代码块相关的数据结构。critical
指令:指定只有一个线程可以访问一个代码块。任何其他线程都将被阻塞,直到该线程完成该代码块并释放锁。使用critical
指令时,可以确保同一时间只有一个线程访问共享的变量或数据结构,从而避免竞争条件。atomic
指令:用于对共享变量执行原子操作。原子操作是指不会被其他线程中断的操作。使用atomic
指令时,可以确保同一时间只有一个线程访问共享变量,从而避免竞争条件。
以下是一个使用这些指令的示例程序:
#include <stdio.h>
#include <omp.h>
int main() {
int sum = 0;
// 只有一个线程执行该代码块
#pragma omp single
{
printf("Single thread executing this code block.\n");
}
// 在4个线程中指定主线程执行该代码块
#pragma omp parallel num_threads(4)
{
#pragma omp master
{
printf("Master thread executing this code block.\n");
}
}
// 保证只有一个线程同时访问sum
#pragma omp parallel for num_threads(4)
for (int i = 0; i < 10; i++) {
#pragma omp critical
{
sum += i;
}
}
printf("sum: %d\n", sum);
// 保证原子更新sum
sum = 0;
#pragma omp parallel for num_threads(4)
for (int i = 0; i < 10; i++) {
#pragma omp atomic
{
sum += i;
}
}
printf("sum: %d\n", sum);
return 0;
}
5.Openmp函数原型介绍自己选几个介绍下
-
omp_get_num_threads(): 用于获取当前并行区域中的线程数。
#include <omp.h> #include <stdio.h> int main() { #pragma omp parallel num_threads(4) { int thread_num = omp_get_thread_num(); printf("Hello from thread %d\n", thread_num); } return 0; }
-
omp_get_thread_num(): 用于获取当前线程的编号。
#include <omp.h> #include <stdio.h> int main() { #pragma omp parallel num_threads(4) { int thread_num = omp_get_thread_num(); int num_threads = omp_get_num_threads(); printf("Hello from thread %d out of %d threads\n", thread_num, num_threads); } return 0; }
-
omp_set_num_threads(int n): 用于设置下一次并行区域中的线程数,n为线程数。
#include <omp.h> #include <stdio.h> int main() { omp_set_num_threads(2); #pragma omp parallel { int thread_num = omp_get_thread_num(); printf("Hello from thread %d\n", thread_num); } return 0; }
-
omp_barrier(): 用于在并行区域内所有线程达到同步点之前,等待其他线程执行完成。
#include <omp.h> #include <stdio.h> int main() { #pragma omp parallel num_threads(4) { int thread_num = omp_get_thread_num(); printf("Before barrier, thread %d\n", thread_num); #pragma omp barrier printf("After barrier, thread %d\n", thread_num); } return 0; }
-
omp_critical(): 用于实现对共享变量的互斥访问,保证同一时间只有一个线程可以访问共享变量。
-
omp_atmic(): 用于实现对共享变量的原子操作,保证同一时间只有一个线程可以修改共享变量。
-
omp_parallel_for(): 用于并行执行for循环,将循环迭代分配给多个线程。
6.自己的对于Openmp的理解
OpenMP是一种用于多线程编程的API,它允许程序员使用一组简单的指令来指示程序在并行计算机系统中执行。它是一种共享内存并行模型,即所有线程共享同一个内存地址空间,每个线程可以访问该内存中的所有变量和数据结构。