2020-10-22

it2025-12-21  7

对原先表格按照第三范式重新设计:

原先的表格很多很多都只满足第一范式,

例如成绩表,将学号、姓名、学院、老师等全部放在一个模式下

=》造成数据冗余,例如学院信息被重复记录

=》删除异常,当成绩表信息都删除后,教师信息也被删除了

 

第三范式:消除非主属性对码的传递函数依赖。

对相应表格进行了分区操作,数据从0几年以来,每年的3月和11月都要新的一批数据录入,而教师端经常作的操作是整批的数据录入和按照年份进行查询整理。

=》按照年份进行水平分区:水平分区是将所有记录按照一定规则存储到不同的数据页中。

 

但也发现了问题:

如果按照年份进行批量的查询导出时,速度很快。

但是一旦按照学院等其它条件批量查询导出数据时,速度反倒变慢。

查询结果如下:

 

如果一个查询条件恰好引用了分区条件,那么InnoDB引擎在查询时可以直接定位到相应的分区中,直接返回相应的分区记录即可

这样子速度很快。

但如果一个查询条件没有引用到分区时依据,那么InnoDB引擎需要在每一个分区中进行查询,找到那些符合要求的数据,而

局部分区索引:一个分区中既存放索引也存放数据 全局分区索引:各个分区只存放数据,而所有数据的索引单独存放在一个位置

Mysql只支持局部分区索引:即一个分区中既存放了索引也存放了数据

而在每个分区的查询就需要依据索引(B+树结构) 

未分区前,一个查询走B+树索引需要 2~3次磁盘IO

分区后,每个分区都要进行2~3次磁盘IO

 

分区的类别:

range:按照一个字段的区间范围进行划分,意味者如果没在任何一个范围内插入失败

list:离散的划分方式,当插入的记录字段属于某个分区字段集合,那么插入到对应分区

hash:根据自定以的表达式的返回值来划分:常见的基于mod函数,将自增主键字段作为依据划分到不同分区

key:依据Mysql提供的哈希函数进行分区

 

另外一个工作是:为那些经常查询和排序的列建立了辅助索引。

聚集索引:索引的逻辑顺序于在物理存储上的顺序一致,体现在数据结构上, 由B+树建立的聚集索引,其在叶子节点存放具体的数据行,而非叶子节点存放索引 叶子节点左右兄弟之间依靠双向链表相连,意味者很容易进行排序和顺序扫描 非聚集索引:即辅助索引,其建立起的B+树结构,节点只存放索引,而在叶子节点 则依据一个书签的实现。存放数据的聚集索引的主键,意味者需要定位到实际的数据 需要根据这个主键去回表:从聚集索引B+树结构中找到真正的数据。以一次B+树木查询为例: 往往需要2~3次磁盘IO 意味者需要5~6次磁盘IO

因此如果能很好的利用聚集索引,那么对于排序和范围查找将大大加速。

 

当存在大量的DDL操作,即插入删除操作时,索引不易建太多:

因为索引的维护本质上是对B+树的维护,其包含两个关键的操作:

B+树节点的分裂:当插入数据时,如果Leaf page 或者 index page满,那么需要拆分节点 B+树节点的合并:当一个Page小于填充因子:50% 那么会造成合并左右的兄弟节点

 

另外一个工作是:采取nodejs实现了一个Reactor模式的后端调用接口:

基于V8的JS解释器,底层调用libuv   

 

Reactor:

 

是一种非阻塞IO设计模式:主线程通过IO多路复用检测IO事件就绪。 一旦IO事件就绪,那么将相应的任务加入到任务队列中,任务队列依据线程池来完成具体的 IO操作,IO操作完成后,通知主线程。

关于阻塞IO和非阻塞IO

阻塞IO:调用线程发起一个IO操作后,被挂起阻塞,需要等待IO操作完成才继续执行。 非阻塞IO:调用线程发起一个IO操作后,立即返回继续执行,当IO操作完成时,可依据事件信号通知调用者

libuv库底层如何实现IO复用?

 

主要是通过Linux下三个IO多路复用系统调用,即 epoll

epoll:通过向内核注册需要监听的事件,但epoll调用返回时,返回就绪事件的epoll_struct

 

同步IO:IO操作的执行者是IO操作的发起者 阻塞同步IO:IO发起者发起IO操作=》IO发起者系统调用=》IO操作发起者被挂起=》系统调用返回=》继续执行 非阻塞同步IO:IO发起者发起IO操作=》不等待系统调用返回=》执行其它=》通过轮训去查看IO是否完成 异步IO:IO操作的发起者发出IO操作后,立即返回执行后续部分,内核完成相应的IO操作,在完成之后才通知发起者 关于IO复用模型: IO复用模型是一个多个网络连接公用一个IO线程:一个线程可以同时监视多个IO 每次轮训的去调用epoll 从IO观察者那里获取到就绪的事件。

 

 

光线跟踪主要解决问题:

 

实现了基于linux下 fork and execve 形式的多进程并发渲染。

数据共享?

对于模型、光照、材质等数据,由于起在各个元素渲染时都不变,直接通过fork出的子进程共享父进程的这一部分区域。

对于标记数组和渲染的二维图像,基于system V  方式下的 共享内存方式

它们需要在多个进程间互斥的读写访问,特别的标记数组需要互斥访问。

而二维图像由于每个进程渲染的图像像素不同,可以支持并发的写入。

 

而对于多个进程互斥的读写标记数组。采取的是信号量的方式。信号量大小为1

也就是限定一次只能有一个线程进行读写操作(而这个过程是很短的)

 

另外一个问题是如何确认所有进程对像素渲染都完成?

 

父进程调用 wait_pid函数等待所有子进程的结束。

 

 

 

 

实现了一个基于多线程的并行实现:

多个线程进行并行绘制主要解决线程间并发同步的问题:

多个线程共享进程的同一地址空间,因此可以很好的共享光线跟踪的模型、光照、光影等结构

 

通过一张map映射表:

0表示尚未渲染、1表示正在渲染、2表示渲染完成。

=》引出新的问题,读取该临界变量时,因为线程并行在不同的处理器CPU下:

因此需要解决:不同CPU下不同线程间互斥访问临界变量的问题:

一种思路是采取简单的互斥锁,但加锁解锁线程会被挂起阻塞,带来线程切换的开销:

后来采取自旋锁方式:

可以很好的使得并行的线程互斥的访问结构:每个处理器上线程在申请自旋锁不能成功的时候,会进入一个简短的忙等待

当一个线程发现所有像素均被渲染完成时,执行exit()退出

问题二?

如何确认所有像素渲染完毕,主线程能及时的继续执行。

采取 child_thread.join()  表示调用线程插入到child_thread之后,那么调用线程被挂起。

研究发现:

创建的线程数量在接近实际CPU核心数量时,渲染时间最短。因为更多的线程,无法真正的并行,反而会带来线程切换

线程切换开销: 线程的切换需要保存线程的栈顶指针,线程所使用的寄存器组,以及线程的PC程序计数器

另外,多个线程之间因为需要自旋锁同步,造成一定的性能下降。

 

后来,又将程序移植到GPU上,利用GPU在计算密集型任务上的优势:

GPU拥有更多的计算核心,但确实部分逻辑跳转的结构。

主要解决问题是: 主机数据=》设备内存  数据的拷贝

原先的光线跟踪程序基于一个C++多态实现:

运行时动态绑定:需要指针,同时对象头安插了vptr 也就是指向虚函数表指针

 

这些指针变量如果直接拷贝到GPU设备内存中,是无法直接使用的。

解决思路是:

将原先的面向对象部分,采取一个结构体,统一描述,替换掉其中面向对象的部分,而只将计算任务和计算任务需要的

模型、光照等数据移交到设备内存中。

 

首先是主机内存到设备内存的拷贝,地址指针将失效。

=》将光线、模型、角度重新封装为结构体,以块共享内存方式存放在主机内存中。

主要解决一个问题是:光线跟踪非递归算法的实现,GPU上不支持递归运算,因此自行设计了

一个地柜调用栈,以实现最终的光线跟踪,采取后续遍历,递归的结束条件是:

递归跟踪深度达到一定的深度

颜色值绝对值小于某个值,意味者深度足够深:

 

CUDA 中多个核心,GPU中的计算核心基于网格来划分:

每个网格又包含许多的线程。可以根据index 来定位每个核心的计算任务,也就是核函数。

做法是:

根据图像的二维矩阵大小,划分出对应的网格数量和每个网格的核函数。

每个核函数执行相应的光线跟踪渲染任务。

当所有像素的渲染完成=》核函数执行完毕,GPU的调用返回=》最终完成了像素渲染。

 

 

相较于其它算法?

 

早先的算法都是基于CPU上实现的较多,主要创新点是基于GPU进行了实现。同时分析了并行效率和实际核心数的关系。

 

 

 

 

最新回复(0)