CMake 简单用法

编译:

编译时,编译器需要的是语法的正确,函数与变量的声明的正确。对于后者,通常是你需要告诉编译器头文件的所在位置(头文件中应该只是声明,而定义应该放在C/C++文件中),只要所有的语法正确,编译器就可以编译出中间目标文件。一般来说,每个源文件都应该对应于一个中间目标文件( .o 文件或 .obj 文件)。

链接:

链接时,主要是链接函数和全局变量。所以,我们可以使用这些中间目标文件( .o 文件或 .obj 文件)来链接我们的应用程序。链接器并不管函数所在的源文件,只管函数的中间目标文件(Object File),在大多数时候,由于源文件太多,编译生成的中间目标文件太多,而在链接时需要明显地指出中间目标文件名,这对于编译很不方便。所以,我们要给中间目标文件打个包,在Windows下这种包叫“库文件”(Library File),也就是 .lib 文件,在UNIX下,是Archive File,也就是 .a 文件。

CMake 定位

其他Make 工具 CMake
GNU Make ,QT 的 qmake ,微软的 MS nmake,BSD Make(pmake),Makepp CMake
对应着不同的Makefile 采用平台统一的CMakeLists.txt 生成自适应的Makefile
make Makefile 编译 :不同产商的make各不相同,也有不同的语法,但其本质都是在 “文件依赖性”上做文章 make Makefile 编译

CMake用法

Makefile

工程编译规则,描述了整个工程的编译和链接等规则。其中包含了哪些文件需要编译,哪些文件不需要编译,哪些需要先编译,哪些需要重建等。

在生成makefile这步时 会进行语法检查么 会进行.cpp 到.o 么 ?不会。makefile只是编译规则 还没开始编译。makefile命令中就包含了调用gcc(也可以是别的编译器)去编译某个源文件的命令。

Make

make工具就根据makefile中的命令进行编译和链接的。生成.o 这类文件,再进一步链接生成.exe这类文件。

参考

Makefile 简介 - Makefile基本概念介绍 - Makefile 简明教程 | 宅学部落

概述 — 跟我一起写Makefile 1.0 文档

Linux Makefile:使用CMake生成Makefile文件_cmake写makefile-CSDN博客

常见操作的耗时

在c release层面

分辨率 H*W 1280 720 耗时(MS) 包含基本操作
3*3 avg filter 普通写法 1.02 读写 +: 8  / :1
5*5 avg filter 普通写法 2.54 读写 + 24 /:1
3*3 winsum avg filter 0.93 读写 +: 2 /: 1 other prepare for window
5*5 winsum avg filter 0.94 读写 +: 2 /: 1 other prepare for window
2 to 1 down sample 0.23 读写
1/4 H*W upsample (video filter) 1.52 读写 +:4  / :5
1/16 H*W upsample(video filter) 1.51 读写 +:4  / :5
1/4 H*W upsample (specialRatio_bilinear up) 0.60 读写 +:4  / :1
1/16 H*W upsample (specialRatio_bilinear up) 0.44 读写 +:4  / :1
3*3 avg filter 1/4 HxW 普通写法 0.26
5*5 avg filter 1/4 HxW winsum 0.24

手写单指令多数据 armv8 加速效果

分辨率 H*W 1280 720 耗时(MS) 包含基本操作
3*3 avg filter 普通写法
5*5 avg filter 普通写法
3*3 winsum avg filter 0.13
5*5 winsum avg filter 0.14
2 to 1 down sample
1/4 H*W upsample (video filter)
1/16 H*W upsample(video filter)
1/4 H*W upsample (specialRatio_bilinear up)
1/16 H*W upsample (specialRatio_bilinear up)
3*3 avg filter 256x144 winsum 0.01
5*5 avg filter  256x144 winsum 0.01

单指令多数据常用操作 intrinsic

mla 乘加或者乘减比分开做要快

循环费时

arm intrinsic 指令查询技巧

查询指令的网址

ARM Neon Intrinsics各函数介绍_arm neon intrinsic 函数_June_Hou的博客-CSDN博客

https://developer.arm.com/architectures/instruction-sets/intrinsics/#q=vmovl_high_u16

总的来说

正常指令 操作64位 指令后加q了的话128位

长指令 指令后面加l ,结果比两个操作数要长

宽指令 指令后面加w 两种数据类型不一样相加减等 生成的数据类型取较宽的那个

窄指令 指令后面加hn 对应的长指令 加减之后 结果比原本两个一样类型的输入要窄

饱和指令 指令后面加q 自动限制在数据范围内

基本运算

乘法 mul

乘加 mla

加法 add

减法 sub

位移 shl shr 

取大小 min max

绝对值 vabs 

取反 vneg

和查表有关的:tb 但只支持表格很小?unit8x8这种table + index

比较

vceq (vector compare equal)

vcge a>=b? (Greater equal

vcle   a<=b? 

Vcgt a>b greater than

Vclt a<b

Vcage |a| >= |b| (vector compare absolute greater equal

r = |a - b| vabd vector absolute difference 

R = a + |b -c| vaba absolute difference and accumulate

位运算

r = a|(~b) vorn_s8   or not

R = ~a & b vbic_s8 bitwise clear

R = a ^ b veor_s8 exclusive or

R = a | b vorr_s8 or

R = a & b vand_s8 and

R = ~a vmvn_s8

赋值or读写

vdup_n_s8 duplicate a scalar into every element of vector ??? Dup mov???

Dup是把一个数据复制多份的关键词

Ld 则是正常load 的关键词

Mov是 拿到一些高位 或者地位 就是操作数和结果位宽不同?(好像也不一定 偶尔和dup等价?

Get是 把unit816 拆成uint88 x2 这种 位宽相同 并行个数不同 vget_high vget_low

cvt convert数据类型

combine 把8个*2 合并成16个

int8x16_t vcombine_s(int8x8_t a,int8x8_t b)

create 从uint64 create uint8x8

banding artifact

banding 现象

单帧

图像平坦渐变区域,出现条带状变化

多帧

随着时间推进,条带状变化移动

banding 造成原因和解决办法

量化

色彩空间转化时,中间空间的量化: RGB 2 YUV 2 RGB

中间运算中的量化: Y‘ = kY + mU + nV

尽量避免中间运算的量化,尤其避免中间运算的多步量化

拉伸

拉伸会导致原来+1 的数据+2 Y‘ = kY k>1

压缩会导致原来不规则(-1 -2 随机)变化的数据 规则的-1 Y‘ = kY k<1

不合理的拉伸公式:UV = f(y), 导致在RGB域无法连续变化

避免映射曲线的斜率过大或者过小,注意多步变化相当于曲线相乘,进一步增大斜率

过于规则

平坦区域如果变化过于规则,视觉也能看到banding

加噪音

多帧

随着时间的推移,输入在渐变,但是经过上述的异常引入,表现出banding 移动

测试阶段

  1. C++ 多次运行,测试结果的一致性

  2. 开启这些诊断

  3. 长时间开启,测试内存

  4. 汇编和C++测试一致性

  5. 视频任务,测试各种分辨率(不同视频不同分辨率,同一个视频中间切换分辨率)

  6. 视频任务,测试其中某些帧内存申请失败,会不会导致整个线程挂掉

  7. 测试同其他feature 一同运行结果是否正常

  8. 测试不同机型下运行时间

DDPM_DDIM_SDE_CompVis

DDPM

Denoising Diffusion Probabilistic Models

https://github.com/betterlmy/ddpm_abarankab

公式原理

正向一步步加噪音:

beta是一个设定好的和t有关的超参数,噪音就是随机高斯噪音

正向一步得到加噪结果

网络训练去噪器
估计出当前噪音,得到Xt-1

总的来说

代码实现

训练过程

推理过程

DDIM

DENOISING DIFFUSION IMPLICIT MODELS

https://github.com/ermongroup/ddim

论文原理

DDPM在采样的时候,需要利用到xt-1 扩散到xt的知识,导致sample速度过慢,而DDIM可以只利用predicted X0 Xt,去求Xt-1,这样就不要求一步步采样了。

reverse过程

代码实现

训练过程,和DDPM一样

推理过程

SDE

论文原理

SDE时间上连续 ,正向和逆向推导过程

涵盖了之前的score base 和ddpm

score base VE

ddpm VP

结合上述两种扩散方式 提出sub-VP SDE 目的是which perform particularly well on

likelihoods

PC采样:

用SDE数值求解预测出结果,再用分数估计修正结果。这部分也有一些实验性结论,比如correct一步还是n步。结合了两种采样方式 *unifies and improves over existing sampling methods for score-based models.

PC的原因

纠正离散步长不准确?

ODE

就是一种和SDE有同样边缘概率分布的采样方式 likelihood fast adaptive sampling via black-box ODE solvers, flexible data manipulation via latent codes, a uniquely identifiable encoding, and notably, exact likelihood computation.

controllable generation

对于score模型 只要知道了 每个时刻condition 的score 就可以根据condition生成图片了,总之就是额外或者利用domain信息,得到一个跟condition有关的score,放到采样过程中就行。主要应用有,分类图片生成,图片补全,图片上色。

这里这些condition都是sample时候加进去的,不涉及估计score的那个训练阶段。

代码实现

怎么训练分数网络

score 本质上还是和noise z相关的的一个估计

怎么采样

PC采样的框架

不同数值求解器的predictor

不同数值求解器的correcter,只截取了部分 还有很多

https://www.bilibili.com/video/BV1Dd4y1A7oz/?spm_id_from=333.999.0.0&vd_source=857061537338948659f6d0f1f3868d83

https://www.bilibili.com/video/BV19M411z7hS/?spm_id_from=333.999.0.0&vd_source=857061537338948659f6d0f1f3868d83

https://www.bilibili.com/video/BV1jg411Q7bQ/?spm_id_from=333.337.search-card.all.click&vd_source=857061537338948659f6d0f1f3868d83

CompVis

论文思想

High-Resolution Image Synthesis with Latent Diffusion Models

利用预先训练好的encoder decoder,将图像映射到latent space上再做DM,这样节省计算资源,因此可以训练高清图片。

在去噪器的结构里加入cross-attention layers 实现更丰富的控制生成。

https://github.com/CompVis/latent-diffusion

c的优化

step1 对c++ 代码的优化

  1. 从算法逻辑上优化,整体调用逻辑,何时准备什么类型的数据。还有算法层面:比如滑动窗口和等。

  2. 对数据精度进行优化,例如double能变成float 的,float能拉伸成整数的。必要时标明数据的实际范围(0~10bit)这种。

  3. 对除法能被乘法代替的,乘法能用加法替代的,乘法能用位移替代的运算进行优化。

  4. 对kn ,(k+1)n这类的运算也可以优化。

  5. 减少if else分支语句内的内容,能合并的合并

step2 c写汇编

  1. 首先原则是 利用函数指针,当运行c的时候就指向c,当运行汇编时就指向汇编

  2. 汇编加速的原理是利用单指令多数据的方式进行加速

  3. 确定输入输出的尺寸,是固定还是任意,是否是偶数,8对齐 16对齐等

  4. 先把c转化成利用flag标识if else的语句。

  5. 确定每一个变量在其生命周期中,数据范围,可并行范围,能并行运算并行,不能时再拆开

  6. 根据运算的并行度,将常数都定义好 是u8 u16 还是u32 还是有符号的s。

  7. 如果大部分操作都可以在int16x8上操作 那就一次读8个 如果大部分操作都能在uint8上操作 那就一次读16个 ,以此类推。

step3 汇编注意事项

  1. 有些运算涉及负数,需要用有符号运算s

  2. 有些数据必须用uint 才能装下

  3. 作为if 条件标识的flag 再需要扩占位数时,需要用s有符号,才能补全高位

ARM Neon Intrinsics 指令相关文档:

https://blog.csdn.net/u012385733/article/details/121086734

https://developer.arm.com/architectures/instruction-sets/intrinsics/https://developer.arm.com/architectures/instruction-sets/intrinsics/

函数与汇编

X86 X64 arm64

arm64 精简指令集

X86 复杂指令集 32位

X64 复杂指令集 64位

寄存器

向下兼容

应用程序地址空间

操作系统通过虚拟内存的方式为所有应用程序提供了统一的内存映射地址。如图3所示,从上到下分别是用户栈、共享库内存、运行时堆和代码段。当然这个是一个大概的分段,实际分段比这个可能稍微复杂一些,但整个格局没有大变化。

函数调用及汇编指令

函数调用主要是call和ret。汇编语言的call指令相当于执行了2步操作,分别是,1)将当前的IP或CS和IP压入栈中; 2)跳转,类似与jmp指令。同样,ret指令也分2步,分别是,1)将栈中的地址弹出到IP寄存器;2)跳转执行后续指令。这个基本上就是函数调用的原理。

除了在代码间的跳动外,函数的调用往往还需要传递一个参数,而处理完成后还可能有返回值。这些数据的传递都是通过寄存器进行的。在函数调用之前通过上文介绍的寄存器存储参数,函数返回之前通过RAX寄存器(32位系统为EAX)存储返回结果。

函数的参数是从右往左传递的。

另外一个比较重要的知识点是函数调用过程中与堆栈相关的寄存器RSP和RBP

RSP(32位叫ESP):栈指针寄存器(reextended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。

RBP(32位叫EBP):基址指针寄存器(reextended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。

可以看出函数内,局部变量的地址是越来越低的。rsp栈顶

到运行到call Blur时,先将main函数的栈底(高地址)push rbp。

并且将main函数的栈顶rsp作为新调用函数blur的栈底 mov rbp  <- rsp。

然后把传进来的参数放在新的栈顶(rbp - 112) mov DWORD PTR [rbp-112], r9d

把函数内的局部变量放在栈底并且向上生长(rbp-8)int* pA1 = pSrc - strideS*2; mov QWORD PTR [rbp-8], rax

函数语句以及汇编指令

基本指令
指令 指令 耗时latency Rthroughput
sub eax, 5 寄存器 - 常数 1 0.25
mov eax, dword ptr [rax] 读 32位 5 0.5
mov rax, qword ptr [rbp - 8] 读64位 5 0.5
mov qword ptr [rbp - 24], rax 写64位 1 1
jmp .L3 1 0.5
add     rax, rdx 寄存器相加 1 0.25
neg rax 取反 1 0.25
shl rax, 2 左移 1 0.5
cdqe 它用于将一个32位有符号整数(存储在 EAX 寄存器中)扩展为一个64位有符号整数(扩展到 RAX 寄存器中) 1 0.25
lea   rdx, [4*rax] “Load Effective Address”  lea destination, source 1 0.5
C++ 语句对应指令
int f = pA1[w]; mov eax, DWORD PTR [rbp-48] 读w的地址 5
cdqe 32eax 64位rax 1
lea rdx, [0+rax*4] 读w地址对应的数据w 1
mov rax, QWORD PTR [rbp-8] 读pA1的地址 5
add rax, rdx 地址偏移w 1
mov eax, DWORD PTR [rax] 读pA1[w] 地址对应的数据 5
mov DWORD PTR [rbp-68], eax 写到f里 1
int c =pA1[w] + pA1[w - 1]; mov eax, DWORD PTR [rbp-48] 读w的地址 5
cdqe 32eax 64位rax 1
lea rdx, [0+rax*4] 读w地址对应的数据w 1
mov rax, QWORD PTR [rbp-8] 读pA1的地址 5
add rax, rdx 地址偏移w 1
mov edx, DWORD PTR [rax] 读pA1[w] 地址对应的数据 5
mov eax, DWORD PTR [rbp-48] 读w的地址 5
cdqe 32eax 64位rax 1
add rax, 1 地址w+1 1
lea rcx, [0+rax*4] 读w+1地址对应的数据 1
mov rax, QWORD PTR [rbp-8] 读pA1的地址 5
add rax, rcx 地址偏移w 1
mov eax, DWORD PTR [rax] 读pA1[w] 地址对应的数据 5
add eax, edx add两个数字 1
mov DWORD PTR [rbp-56], eax 1
sum =
(sum * 327)>>13;
mov eax, DWORD PTR [rbp-92]
imul eax, eax, 327
sar eax, 13
mov DWORD PTR [rbp-92], eax
sum = sum / 25; mov eax, DWORD PTR [rbp-92]
(在sum最大范围数据类型最大精度下优化算法) movsx rdx, eax
imul rdx, rdx, 1374389535
shr rdx, 32
mov ecx, edx
sar ecx, 3
cdq
mov eax, ecx
sub eax, edx
mov DWORD PTR [rbp-92], eax
pBlur[w] = static_cast(sum); mov eax, DWORD PTR [rbp-48]
cdqe
lea rdx, [0+rax*4]
mov rax, QWORD PTR [rbp-104]
add rdx, rax
mov eax, DWORD PTR [rbp-92]
mov DWORD PTR [rdx], eax
for (int h = 0; h < height; h++) mov DWORD PTR [rbp-44], 0
jmp .L2
L2:
mov eax, DWORD PTR [rbp-44]
cmp eax, DWORD PTR [rbp-128]
jl .L5

https://godbolt.org/

积分图和滑动窗口

d*d 模糊 filter

对原图做r*r 的均值模糊 以循环方式书写运算次数为 (d * d) * (HW),优化目标:运算次数和半径无关

积分图

step 0 该padding还都得padding,d = 2r + 1。那原图需要padding 成 (h+2r)* (w+2r)
step1 计算积分图 积分图在padding 的结果上还需要再加一行一列 (h+2r+1)* (w+2r+1),因为积分图每一点xy表示的是x之前 y之前那个矩形的和。如下图 绿色面积表示padding后的图像,蓝色为积分图增加的内存,xy点的值就是蓝绿色面积内的和
step2 filter 对一个点(m,n) 进行3*3的boxfilter为例,需要在积分图上进行 1 + 4 - 2 - 3操作

滑动窗口

step 0 该padding还都得padding,d = 2r + 1。那原图需要padding 成 (h+2r)* (w+2r)
step1 计算d行和,申请1*w+2r 大小的空间,存储当前位置d行和。

step2 由行和先计算第一个窗口和,代表00点的窗口和, 随后每次减去最左边的d行和 加上最右边的d行和
step3 继续向下滑动,先通过减第一行加下面一行的原图更新d行和,再重复计算窗口和步骤