0%

SLIC 超像素分割算法的并行计算实现

在图像处理领域,超像素分割作为一种重要的预处理技术,能够将图像划分为具有语义意义的像素簇,为后续的目标检测、图像分割等任务奠定基础。

SLIC 算法由 Radhakrishna Achanta 等人于 2012 年提出,其核心思想是通过线性迭代聚类将图像分割为具有相似颜色和空间位置的超像素。与传统算法相比,SLIC 具有计算效率高、参数少(近乎零参数)的特点,尤其适合实时性要求高的场景。

算法的主要步骤包括:

  1. 颜色空间转换:将 RGB 图像转换到 LAB 颜色空间,LAB 空间的欧氏距离更符合人眼感知
  2. 种子点初始化:在图像中均匀采样种子点,并基于边缘强度调整位置
  3. 迭代聚类:以种子点为中心,计算邻域像素的颜色距离和空间距离,更新像素所属聚类
  4. 连通性增强:消除孤立小区域,保证超像素的连通性

算法优化策略与实现细节

编译优化:开启 O3 选项

编译优化是最直接的性能提升手段。通过 GCC 的-O3选项,编译器会执行一系列优化操作,包括指令调度、循环展开、常量折叠等。

1
g++ -std=c++11 -O3 -fopenmp -mavx -mavx2 -march=native bestgai.cpp -o slictest

AVX 向量化

AVX(Advanced Vector Extensions)指令集支持单指令多数据操作,能同时处理多个数据元素。在 SLIC 算法中,颜色空间转换部分是向量化优化的重点:

1
2
3
4
5
6
7
8
9
10
11
12
// RGB到LAB转换的AVX向量化实现
void SLIC::RGB2XYZ(const int& sR, const int& sG, const int& sB, double& X, double& Y, double& Z) {
// 使用AVX指令同时处理4个数据元素
__m256d rgb = _mm256_set_pd(0.0, sB, sG, sR);
__m256d mmm = _mm256_set1_pd(255.0);
__m256d result = _mm256_div_pd(rgb, mmm);
double resultArray[4];
_mm256_storeu_pd(resultArray, result);
double R = resultArray[0], G = resultArray[1], B = resultArray[2];

// 后续计算...
}

向量化的核心在于将标量操作转换为向量操作,充分利用 CPU 的 SIMD 单元,减少指令周期数。

OpenMP 并行:多线程分工加速迭代计算

SLIC 算法中的迭代聚类过程具有天然的并行性,通过 OpenMP 实现多线程并行计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 并行化RGB到LAB转换
void SLIC::DoRGBtoLABConversion(const unsigned int*& ubuff, double*& lvec, double*& avec, double*& bvec) {
int sz = m_width * m_height;
lvec = new double[sz];
avec = new double[sz];
bvec = new double[sz];

// 使用parallel for指令并行化循环
#pragma omp parallel for
for (int j = 0; j < sz; j++) {
int r = (ubuff[j] >> 16) & 0xFF;
int g = (ubuff[j] >> 8) & 0xFF;
int b = (ubuff[j]) & 0xFF;
RGB2LAB(r, g, b, lvec[j], avec[j], bvec[j]);
}
}

更复杂的并行优化体现在迭代聚类的分块处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void SLIC::PerformSuperpixelSegmentation_VariableSandM(...) {
int xiancheng = omp_get_max_threads();
int group = sz / xiancheng;

// 分块处理图像行
#pragma omp parallel
{
int qq = omp_get_thread_num();
int start = qq * group;
int endle = start + group;
if (qq == xiancheng - 1) endle = m_height;

#pragma omp parallel for
for (int y = start; y < endle; y++) {
for (int n = 0; n < numk; n++) {
// 计算当前种子点的影响区域
int wq1 = (int)(kseedsy[n] - offset);
int wq2 = (int)(kseedsy[n] + offset);
if (y >= wq1 && y < wq2) {
int x1 = max(0, (int)(kseedsx[n] - offset));
int x2 = min(m_width, (int)(kseedsx[n] + offset));

for (int x = x1; x < x2; x++) {
// 计算颜色距离和空间距离
int i = y * m_width + x;
double l = m_lvec[i], a = m_avec[i], b = m_bvec[i];
double distlab = (l - kseedsl[n])*(l - kseedsl[n]) +
(a - kseedsa[n])*(a - kseedsa[n]) +
(b - kseedsb[n])*(b - kseedsb[n]);
double distxy = (x - kseedsx[n])*(x - kseedsx[n]) +
(y - kseedsy[n])*(y - kseedsy[n]);
double dist = distlab / maxlab[n] + distxy * invxywt;

if (dist < distvec[i]) {
distvec[i] = dist;
klabels[i] = n;
}
}
}
}
}
}
}

循环调整

调整循环顺序、提取公共计算

1
2
3
4
5
6
7
8
9
10
11
for (column = 0; column < 100; column++) {
for (row = 0; row < 5; row++) {
sum += table[row][column];
}
}

// 优化后:按列遍历,提升缓存命中率
for (row = 0; row < 5; row++) {
for (column = 0; column < 100; column++) {
sum += table[row][column];
}

将图像遍历从按行改为按列,并结合分块并行,进一步提升了数据局部性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int group1 = m_height / xiancheng;
#pragma omp parallel
{
int qq = omp_get_thread_num();
int start = qq * group1;
int endle = start + group1;
if (qq == xiancheng - 1) endle = m_height;

#pragma omp parallel for
for (int y = start; y < endle; y++) {
for (int n = 0; n < numk; n++) {
// 种子点邻域计算...
}
}
}

按需计算

将使用频率低的数组改为按需计算,避免全程存储:

1
2
3
4
5
6
7
8
9
10
11
vector<double> distxy(sz, DBL_MAX);
vector<double> distlab(sz, DBL_MAX);
vector<double> distvec(sz, DBL_MAX);

// 优化后:按需计算,仅存储必要结果
double distlab = (l - kseedsl[n])*(l - kseedsl[n]) +
(a - kseedsa[n])*(a - kseedsa[n]) +
(b - kseedsb[n])*(b - kseedsb[n]);
double distxy = (x - kseedsx[n])*(x - kseedsx[n]) +
(y - kseedsy[n])*(y - kseedsy[n]);
double dist = distlab / maxlab[n] + distxy * invxywt;

性能优化结果

以下是各优化步骤的性能对比:

优化策略 计算时间(ms) 加速比(%)
基准版本 25120 0.00
编译优化 - O3 8600 293.49
AVX 向量化 8539 294.71
OpenMP 并行 1560 1610.25
按需计算 1436 1749.30
循环层次调整 388 6474.22
最终优化版本 350 -

循环优化

循环优化整体策略为循环展开循环合并循环顺序的交换

循环展开

通过减少循环迭代次数来降低循环控制开销,将多个迭代步骤合并执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 优化前
while(i < count){
a[i] = i;
i++;
}

// 优化后
while(i < count - 1){
a[i] = i;
a[i+1] = i+1;
i += 2;
}
if(i == count-1){
a[count-1] = count-1;
}

循环合并

对于计数器相同的多个循环,将其合并为一个循环,避免多次轮询:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 优化前
for(i = 0; i < index; i++){
do_type_a_work(i);
}
for(i = 0; i < index; i++){
do_type_b_work(i);
}

// 优化后
for(i = 0; i < index; i++){
do_type_a_work(i);
do_type_b_work(i);
}

循环顺序交换

计算外提

将循环内不变的计算提取到循环外部,减少无效计算:

1
2
3
4
5
6
// 优化前
for(int i = 0; i < get_max_index(); i++){}

// 优化后
int max_index = get_max_index();
for(int i = 0; i < max_index; i++){}

多级寻址外提

将循环内的多级指针寻址提取到外部,避免反复寻址跳转:

1
2
3
4
5
6
7
8
9
10
11
12
// 优化前
for(int i = 0; i < max_index; i++){
ainfo->bconfig.cset[i].index = index;
ainfo->bconfig.cset[i].flag = flag;
}

// 优化后
set = ainfo->bconfig.cset;
for(int i = 0; i < max_index; i++){
set[i].index = index;
set[i].flag = flag;
}

判断外提

将循环内固定不变的条件判断提取到外部,减少比较次数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 优化前
for(i = 0; i < index; i++) {
if (type == TYPE_A) {
do_type_a_work(i);
} else {
do_type_b_work(i);
}
}

// 优化后
if (type == TYPE_A) {
for(i = 0; i < index; i++) {
do_type_a_work(i);
}
} else {
for(i = 0; i < index; i++) {
do_type_b_work(i);
}
}

循环嵌套顺序调整

将最繁忙的循环放在最内层,提高缓存利用率:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 优化前
for (column = 0; column < 100; column++) {
for (row = 0; row < 5; row++) {
sum += table[row][column];
}
}

// 优化后
for (row = 0; row < 5; row++) {
for (column = 0; column < 100; column++) {
sum += table[row][column];
}
}
  • 充分分解小的循环:要充分利用CPU的指令缓存,就要充分分解小的循环。特别是当循环体本身很小的时候,分解循环可以提高性能。注意:很多编译器并不能自动分解循环。
1
2
3
4
5
6
7
8
9
10
11
12
13
// 优化前(3D矢量与4x4矩阵相乘)
for (i = 0; i < 4; i++) {
r[i] = 0
for (j = 0; j < 4; j++) {
r[i] += M[j][i] * V[j];
}
}

// 优化后
r[0] = M[0][0]*V[0] + M[1][0]*V[1] + M[2][0]*V[2] + M[3][0]*V[3];
r[1] = M[0][1]*V[0] + M[1][1]*V[1] + M[2][1]*V[2] + M[3][1]*V[3];
r[2] = M[0][2]*V[0] + M[1][2]*V[1] + M[2][2]*V[2] + M[3][2]*V[3];
r[3] = M[0][3]*V[0] + M[1][3]*V[1] + M[2][3]*V[2] + M[3][3]*v[3];
  • 提取公共部分:对于一些不需要循环变量参加运算的任务可以把它们放到循环外面,这里的任务包括表达式、函数的调用、指针运算、数组访问等,应该将没有必要执行多次的操作全部集合在一起,放到一个init的初始化程序中进行。

OpenMP 并行优化策略

  1. for 循环之前直接加:#pragma omp parallel for 即可自动并行;
  2. 没有 for 循环,还想让代码并行一起干活就用 sections、section:
1
2
3
4
5
6
7
8
#pragma omp parallel sections{ 
#pragma omp parallel section
//第一个人干点啥
#pragma omp parallel section
//第二个人干点啥
...
//可以无限套娃
}

但是这样做有个缺点:如果想让每个人干的活差不多,我们得学怎么分任务,太麻烦也不好实现,很容易划分不佳;并且还有一个问题:同一个 sections 内部,不同的 section 之间变量会不会相互干预?我们一定能找到变量相互干预的情况,也一定能找到变量相互不干预的情况。为了区别不同并行区域之间变量是自己使用,还是给大家共享使用,我们定义不同的类型:

  • 私有不干预:private,将变量声明为私有,并行完成后消失,不干预并行区域外的同名变量。
  • 继承不干预:firstprivate,并行之前就有这个变量存在,并行完成后区域内消失,外面的变量保持原值。
  • 私有干预:lastprivate,并行区域内生成,并行完成后保存在并行外部。
  • 复制不干预:threadprivate,共享版 private。

private 子句

1
2
3
4
5
6
int k = 100;
#pragma omp parallel for private(k)
for ( k=0; k < 10; k++){
printf("k=%d\n", k);
}
printf("last k=%d\n", k);

输出结果是:首先是 0-9 的乱序输出(并行没有顺序),其次就是 last k=100,内外不干预,内部 k 虽然不断变化,但外面的 k 仍然保持 100。

firstprivate 子句

private 虽好,但是它不能继承来自外面的同名变量的值,也就是内外不相联系。firstprivate 可以继承外面变量值用以内部使用,但是对外面变量不会有影响:

1
2
3
4
5
6
7
int k = 100;
#pragma omp parallel for firstprivate(k)
for ( i=0; i < 4; i++){
k+=i;
printf("k=%d\n",k);
}
printf("last k=%d\n", k);

打印结果是:k=100-103 之间乱序输出,last k=100。

lastprivate 子句

firstprivate 虽然能从外面继承,但是不能干预外部,只能自娱自乐。lastprivate 能干预外部。

1
2
3
4
5
6
7
int k = 100;
#pragma omp parallel for firstprivate(k),lastprivate(k)
for ( i=0; i < 4; i++){
k+=i;
printf("k=%d\n",k);
}
printf("last k=%d\n", k);

打印结果是:k=100-103 之间乱序输出,last k=103。

threadprivate 子句

threadprivate 子句用来指定全局的对象被各个线程各自复制了一个私有的拷贝,即各个线程具有各自私有的全局对象,通常用来做计数器,

1
2
3
4
5
6
int counter = 0; 
#pragma omp threadprivate(counter)
int increment_counter() {
counter++;
return(counter);
}

threadprivate 和 private 的区别在于:前者声明的变量通常是全局范围内有效,而 private 只在自己的并行区域内有效。threadprivate 对应只能用于 copyin、copyprivate、schedule、num_threads 和 if 子句中。

default/shared 子句

如果我们想让不同的 section 共同编辑一个变量,就要用 default(shared) 声明, 如果是 default(none),则线程中用到的变量必须指明是私有的还是共享的,毕竟如果不说明参数,很容易造成误会。

reduction 规约子句

这个就是我们在 MPI 中用到的规约子句:

1
2
3
4
5
6
int i, sum = 100;
#pragma omp parallel for reduction(+: sum) {
for ( i = 0; i < 1000; i++ ){
sum += i;
}
printf( "sum = %ld\n", sum);

上面的程序为每个线程执行规约加法操作,其中 + 为规约操作类型,sum 为规约变量(reduction 操作类型与 reduction 变量)。

1. 什么是求解器?

求解器是用于求解数学方程组(如线性方程组、非线性方程组、偏微分方程等)的算法工具或程序模块。它以输入的系数矩阵和向量为基础,输出解的数值结果。

2. 求解器的求解流程

  1. 初始化: 输入方程系数矩阵 AAA 和右端向量 bbb。
  2. 预处理(可选): 针对矩阵的结构或性质,选用适当的预处理方法。
  3. 求解过程:
    • 使用直接法:构造分解(如 LU),求解方程组。
    • 使用迭代法:初始化解向量,迭代更新直到满足收敛准则。
  4. 验证结果: 检查解的精度和残差。

3. 求解器的求解方法有哪些?

  1. 直接法求解器:

    • 高斯消元
    • LU分解、Cholesky分解
    • QR分解
  2. 迭代法求解器:

    • Jacobi法、Gauss-Seidel法
    • 共轭梯度法(CG)
    • GMRES、BiCGStab
    • 多重网格法
  3. 混合方法:

    • 直接法结合迭代法(如预处理直接求粗解,然后用迭代法优化)。

    1. 直接法和迭代法是什么?

直接法
直接法是求解线性方程组(如 Ax=bAx = bAx=b)的算法,直接通过有限步操作精确地找到解。常用的方法包括:

  • 高斯消元法
  • LU分解
  • Cholesky分解(适用于正定矩阵)

迭代法
迭代法是通过构造初始解并逐步逼近精确解的数值方法,适用于稀疏矩阵或规模较大的问题。每次迭代基于上一次的结果更新解,直到满足收敛准则。常见方法包括:

  • Jacobi迭代法
  • Gauss-Seidel迭代法
  • 共轭梯度法(CG)
  • 广义最小二乘残差法(GMRES)

2. 直接法和迭代法的区别

方面 直接法 迭代法
精确性 精确解(仅受计算误差影响) 近似解(逐步逼近精确解)
适用问题 小规模问题,稠密矩阵 大规模问题,稀疏矩阵
计算成本 高,通常为 O(n3)O(n^3)O(n3) 较低,单次迭代通常为 O(n)O(n)O(n) 或 O(m)O(m)O(m)
存储需求 需要存储整个矩阵,可能导致内存问题 通常只需存储稀疏矩阵,适合大规模问题
可扩展性 不易扩展到超大规模问题 容易扩展到超大规模问题,适合并行计算
预处理需求 通常不需要预处理 通常需要预处理以加速收敛

3. 迭代法前面的预处理方式有哪些,如何根据需要选择合适的预处理方法?

常见预处理方法:

  1. 对角预处理:使用矩阵的对角元素构造简单预条件器,计算成本低,适用于对角元素占优的矩阵。
  2. 不完全LU分解(ILU):通过对原矩阵进行近似LU分解构造预条件器,适合稀疏矩阵。
  3. 不完全Cholesky分解(ICC):适用于对称正定矩阵。
  4. 代数多重网格法(AMG):一种高级预处理方法,适用于复杂的大规模稀疏矩阵问题。
  5. 稀疏逆近似(SAI):构造稀疏的逆矩阵近似作为预条件器,计算成本低,适用于特殊结构的矩阵。

选择依据:

  • 矩阵性质(稠密/稀疏、对称性、正定性):如正定矩阵优先考虑 ICC。
  • 计算资源:简单方法如对角预处理计算开销小,但可能效果有限。
  • 收敛速度需求:较难收敛的问题通常需要复杂的预处理(如 ILU 或 AMG)。

4. 迭代法求解过程中,如何选择合理的迭代方法?

  • 共轭梯度法(CG):适用于对称正定矩阵,具有快速收敛性。
  • GMRES(广义最小残差法):适用于非对称或非正定矩阵,计算成本较高,但稳定性较好。
  • BiCGStab(双共轭梯度稳定法):适用于非对称矩阵,改进了共轭梯度法。
  • 最小二乘法(LSQR):适用于稀疏或病态问题。

选择依据:

  • 矩阵类型(对称性、正定性、稀疏性)
  • 计算资源和时间成本
  • 预处理器的兼容性

Teco异构并行计算平台

1757593943368
Teco异构并行模型采用主从异构的物理架构,即Host-Device运行模式。
1757593952590

SPMD于SDAA C

SPMD(Single Program Multiple Data,单程序多数据),是一种用于任务并行的编程范式。其本质是将一个问题分解成若干个子问题,然后对其并行求解。

SDAA C编程模型遵循SPMD编程范式,同一计算核心阵列SPA内所有计算核心SPE运行同一份应用程序,每个SPE可以独立完成对子问题的并行求解。其核心在于对复杂计算任务的切分,并合理地将计算任务分配到不同的SPE内进行计算。

1757593963537

SDAA C编程模型提供了threadIdxthreadDim为每个SPE定制计算任务。其中:

  • threadIdx:当前计算核心SPE的ID号,可以通过该关键字为目标SPE分配计算任务。

  • threadDim:获取同一计算核心阵列SPA内计算核心SPE的总数。

编程示例

以数组计算为例,将计算任务分配到同一SPA的所有SPE内并行执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
using namespace sdaa;
#define SIZE 67

__device__ int g_data[SIZE];
__global__ void func()
{
// 初始化原始数据
for (int i = 0; i < SIZE; i++) {
g_data[i] = i;
}

// SPE同步,确保所有的SPE都完成了数据的初始化
sync_threads();

// 可用的计算资源超过计算量时,每个SPE承担一次计算
if (threadDim >= SIZE) {
if (threadIdx < SIZE) {
g_data[threadIdx]++;
}
} else {
// 可用计算资源少于计算量时,每个SPE需承担多次计算
// 例如当前计算量为67,假如共有32个SPE参与计算。则每个SPE需要至少承担两次计算任务
int cal_time = SIZE / threadDim;
for (int i = 0; i < cal_time; i++) {
g_data[i * threadDim + threadIdx]++;
}

// 对于剩余的计算量使用部分SPE承担
// 例如当前的计算量为67,共有32个SPE参与计算,每个SPE计算两次后,还剩余3次。则使用0, 1, 2三个SPE完成剩余的计算任务
int remain_time = SIZE % threadDim;
if ((threadIdx + 1) <= remain_time) {
g_data[cal_time * threadDim + threadIdx]++;
}
}

// SPE同步,确保所有的SPE都完成了子问题的求解
sync_threads();

// 打印输出计算结果
for (int i = 0; i < SIZE; i++) {
printf("SPE ID = %lu, g_data[%d] = %d\n", threadIdx, i, g_data[i]);
}
}

上述应用程序中就通过SPMD编程范式完成对复杂任务的求解。

  • SP(Single Program,单程序):所有参与计算的SPE都执行同一份代码,在本例中为func函数。

  • MD(Multiple Data,多数据):通过threadIdx将数组进行切分,每个SPE仅需完成少量运算。

    在本例中:SIZE为67,假如可用计算资源为32个SPE,计算资源少于计算量,每个SPE需要承担多次计算,如:SPE0需要计算数据g_data[0]、g_data[32]和g_data[64]、SPE1需要计算数据g_data[1]、g_data[33]和g_data[65]等。

1757593977134

图2 使用SPMD编程范式完成数组计算任务

具体计算任务的代码片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 可用计算资源少于计算量时,每个SPE需承担多次计算
// 例如当前计算量为67,共有32个SPE参与计算。则每个SPE需要至少承担两次计算任务
int cal_time = SIZE / threadDim;
for (int i = 0; i < cal_time; i++) {
g_data[i * threadDim + threadIdx]++;
}

// 对于剩余的计算量使用部分SPE承担
// 例如当前的计算量为67,共有32个SPE参与计算,每个SPE计算两次后,还剩余3次。则使用0, 1, 2三个SPE完成剩余的计算任务
int remain_time = SIZE % threadDim;
if ((threadIdx + 1) <= remain_time) {
g_data[cal_time * threadDim + threadIdx]++;
}

代码中涉及部分关键字含义如下:

  • __device__:运行空间标识符,修饰Device端的函数或变量,在Device端调用、运行,可参考device

  • __global__:函数标识符,修饰Kernel函数,可参考global

  • sync_threads:对同一计算核心阵列SPA内的所有或部分计算核心SPE进行同步操作,可参考sync_threads

  • printf:打印接口,可参考printf

张量

在计算机中,张量通常被表示为多维数组。一维张量类似于向量,二维张量类似于矩阵,而高阶张量则可以有任意数量的维度。例如,一个二维张量可以被表示为一个二维数组,其中的每个元素都有两个索引,分别用来表示行和列。

winograd算法

例子
1757593132456
需要如下计算
1757593143285
r0 = d0g0 + d1g1 + d2g2 # 3次乘法+2次加法
r1 = d1
g0 + d2g1 + d3g2 # 3次乘法+2次加法
以上的计算过程总共需要 6 次乘法和 4 次加法
1757593156991
推导:
上面已经说过,如果我们直接进行矩阵乘,会得到 r0 = d0 x g0 + d1 x g1 + d2 x g2,r1 = d1 x g0 + d2 x g1 + d3 x g2 ,所以我们可以令:m1 + m2 + m3 = r0,m2 - m3 - m4 = r1。像这样:

1757593182894

先观察,两个等式中 m1、m4 是没有重复出现的,我先令 m1 = d0 x g0,m4 = -d3 x g2,这样可以约掉 m1 和 m4,所以左边只剩两个变量,两个等式两个变量即可求出 m3、m4,所以这个时候的 m1、m2、m3、m4 是这样的:

1757593197088

观察 m2 中包含了 d1、d2、g0、g1、g2,将其转换为两个多项式乘积形式,拆成 d 和 g 分开的形式,如下:

1757593212837

同理,对 m3 也进行如上转换,完了之后现在的 m1、m2、m3、m4 是这样的:

1757593233407

这个时候让我们回到最开始的等价关系,进行观察,要是我在 m2、m3 上同时加上一个值,对于式 (b) 来说是不变的(所以 m4 不用动),对于式 (a) 来说需要给 m1 减去两倍的这个值。

1757593245589

观察现在的 m1、m2、m3、m4,当这个值是 (d2g0) / 2 时可以简化表达式,所以这样给上面等式进行等价变换后得到的 m1、m2、m3、m4 如下:

1757593260927

继续如上操作,如果给 m2 加上一个值,同时给 m3 减去这个值,那么对于式 (a) 来说是不变的 (所以 m1 不用动),对于式 (b) 来说需要给 m4 减去两倍的这个值才能等价。同样观察现在的 m1、m2、m3、m4,当这个值为 (d1g2) / 2 时可以进一步简化表达式,接着作这样的变换后得到最终的 m1、m2、m3、m4,如下:

1757593280494

稀疏矩阵乘

压缩

压缩稀疏行
1757593301059
V = [ 10 20 30 40 50 60 70 80 ]
COL_INDEX = [ 0 1 1 3 2 3 4 5 ]
ROW_INDEX = [ 0 2 4 7 8 ]
压缩稀疏列
对角线压缩
对角线压缩利用了稀疏矩阵中对角线上大量为零的特点。它只存储对角线上的非零元素及其对应的行列索引,并将其他位置的元素设为零。这种方法适用于矩阵对角线上非零元素占比较大的情况。
块压缩
块压缩将稀疏矩阵分成多个块,只存储每个块中的非零元素及其对应的行列索引。这种方法适用于稀疏矩阵中非零元素分布比较均匀的情况,可以进一步减少存储空间

BCSR
BCSR是对CSR的一般化和改进,它和CSR的区别在于把原矩阵分成了大小相同的block,block中的空元素则用0填上,于是每一个block都是稠密的,所以val数组会变大一些(需要填充一些0),但是索引却简化了,这里的精髓在于每一个块内部可以直接进行稠密的矩阵向量乘法
1757593311771