35、嵌入式系统优化与并发管理全解析

嵌入式系统优化与并发管理全解析

1. 高级优化策略

在代码优化领域,有多种高级优化策略能显著提升代码性能和资源利用率。

1.1 循环优化示例

首先来看一个循环优化的例子,有以下两种代码版本:

// 版本一
for (j=0; j<n; j++)
    p[j]= ... ;
for (j=0; j<n; j++)
    p[j]=p[j]+ ... ;

// 版本二
for (j=0; j<n; j++)
{
    p[j]= ... ;
    p[j]=p[j]+ ... ;
}

如果目标处理器提供仅适用于小循环的零开销循环指令,左边版本可能更具优势,而且简单的循环结构使其成为循环展开的良好候选。右边版本由于对数组 p 的引用局部性得到改善,可能会带来更好的缓存性能,同时也增加了循环体内并行计算的潜力。但很难判断哪种转换能生成最优代码。

1.2 循环分块(Loop Tiling/Blocking)

由于小内存比大内存速度快,使用内存层次结构可能会带来好处,如缓存和暂存器内存。不过,需要这些内存中的信息有显著的重用因子,否则无法有效利用内存层次结构。

以矩阵乘法为例,原始的矩阵乘法代码如下:

for (i=0; i<N; i++)
    for(j=0; j<N; j++)
    {
        r=0;
        for (k=0; j<N; k++)
            r+=X[i][k]*Y[k][j];
        Z[i][j]=r;
    }

在这个代码中,对数组 X 的访问具有空间局部性,而对数组 Y 的访问则没有。如果缓存不够大,每次访问 Y 都会导致缓存缺失,主存中对 Y 元素的引用次数将达到

为了改善这种情况,科学家设计了分块算法,以下是分块后的矩阵乘法代码,块大小参数为 B

for (ii=0; kk<N; ii+=B)
    for (jj=0; jj<N; jj+=B)
        for (kk=0; kk<N; kk+=B)
            for (i=ii; i<min(ii+B-1,N); ii++)
                for (j=jj; j<min(jj+B-1,N); jj++) {
                    r=0;
                    for (k=kk; k<min(kk+B-1,N); k++)
                        r+= X[i][k]*Y[k][j];
                    Z[i][j]=r;
                }

通过分块,内层两个循环被限制在数组 Y 的一个 大小的块内。如果这个块能放入缓存,内层循环的第一次执行会将该块加载到缓存中,后续执行可以重用这些元素,从而将对主存中 Y 元素的访问次数减少到 N³/(B - 1) 。研究表明,分块可以使矩阵乘法的性能提高 3 到 4.3 倍,并且随着处理器和内存速度差距的增大,性能提升更明显,还能降低内存系统的能耗。

1.3 循环拆分(Loop Splitting)

许多图像处理算法在处理图像边缘像素时需要特殊处理,这可能导致在算法的最内层循环中进行大量的边界检查。为了提高效率,可以将循环拆分为处理常规情况和异常情况的两部分。

例如,在 MPEG - 4 标准的运动估计算法中有如下循环嵌套代码:

for (z=0; z<20; z++)
    for (x=0; x<36; x++) {
        x1=4*x;
        for (y=0; y<49; y++) {
            y1=4*y;
            for (k=0; k<9; k++)
            {
                x2=x1+k-4;
                for (l=0; l<9; l++)
                {
                    y2=y1+l-4;
                    for (i=0; i<4; i++)
                    {
                        x3=x1+i; x4=x2+i;
                        for (j=0; j<4; j++)
                        {
                            y3=y1+j; y4=y2+j;
                            if (x3<0 || 35<x3 || y3<0 || 48<y3)
                                then_block_1; else else_block_1;
                            if (x4<0 || 35<x4 || y4<0 || 48<y4)
                                then_block_2; else else_block_2;
                        }
                    }
                }
            }
        }
    }

Falk 等人的算法可以自动检测到某些条件(如 x3<0 y3<0 )永远不会为真,并将循环嵌套转换为以下代码:

for (z=0; z<20; z++)
    for (x=0; x<36; x++) {
        x1=4*x;
        for (y=0; y<49; y++)
            if (x>=10 || y>=14)
                for (; y<49; y++)
                    for (k=0; k<9; k++)
                        for (l=0; l<9; l++ )
                            for (i=0; i<4; i++)
                                for (j=0; j<4; j++) {
                                    then_block_1; then_block_2;
                                }
            else {
                y1=4*y;
                for (k=0; k<9; k++) {
                    x2=x1+k-4;
                    for (l=0; l<9; l++) {
                        y2=y1+l-4;
                        for (i=0; i<4; i++) {
                            x3=x1+i; x4=x2+i;
                            for (j=0; j<4; j++) {
                                y3=y1+j; y4=y2+j;
                                if ( 0 || 35 <x3 || 0 || 48 < y3)
                                    then_block_1; else else_block_1;
                                if (x4 < 0 || 35 < x4 || y4 < 0 || 48 < y4)
                                    then_block_2; else else_block_2;
                            }
                        }
                    }
                }
            }
    }

循环拆分可以减少各种应用和架构的运行时间,对于运动估计算法,循环次数最多可减少约 75%。

1.4 数组折叠(Array Folding)

在嵌入式应用中,特别是多媒体领域,常常包含大型数组。由于嵌入式系统的内存空间有限,需要探索减少数组存储需求的方法。

有两种数组折叠方式:
- 数组间折叠(Inter - array Folding) :在不同时间不需要的数组可以共享相同的内存空间。
- 数组内折叠(Intra - array Folding) :利用数组内所需组件的有限集合,以更复杂的地址计算为代价节省存储空间。这两种折叠方式也可以结合使用。

1.5 浮点到定点转换(Floating - Point to Fixed - Point Conversion)

许多信号处理标准(如 MPEG - 2 或 MPEG - 4)使用浮点数据类型的 C 程序进行规范,而将浮点数据转换为定点数据是一种常用的优化技术。

例如,对于 MPEG - 2 视频压缩算法,将浮点转换为定点可以使循环次数减少 75%,能耗降低 76%。但通常会损失一些精度,需要在实现成本和算法质量之间进行权衡。

最初这种转换是手动完成的,过程繁琐且容易出错。现在有一些工具可以支持这种转换,如 FRIDGE(定点编程设计环境),其功能已作为 Synopsys System Studio 工具套件的一部分商业化。同时,SystemC 可用于模拟定点数据类型。

2. 任务级并发管理

任务图的粒度是其重要属性之一,即使对于分层任务图,改变节点的粒度也可能是有用的。在规范阶段,关注点的清晰分离和干净的软件模型比实现效率更重要。因此,任务在规范和实现中不一定一一对应,可能需要对任务进行重新分组,即任务的合并和拆分。

2.1 任务合并

当一个任务 τi 是另一个任务 τj 的直接前驱,且 τj 没有其他直接前驱时,可以进行任务图的合并。这种转换可以减少软件实现中上下文切换的开销,并增加整体优化的潜力。

2.2 任务拆分

任务拆分也可能是有利的。任务在等待输入时可能持有大量资源(如大量内存),为了最大化资源利用率,最好将资源的使用限制在实际需要的时间间隔内。

例如,假设任务 τ2 在其代码中某个地方需要输入,最初只有在输入可用时才能开始执行。将该任务拆分为 τ ∗₂ τ ∗∗₂ ,使得输入仅在 τ ∗∗₂ 的执行中需要,这样 τ ∗₂ 可以提前开始,增加了调度的自由度,可能提高资源利用率,甚至有助于满足某些截止日期要求,还可能影响数据存储所需的内存。

2.3 Petri 网技术进行任务转换

Cortadella 等人提出的基于 Petri 网的技术可以对规范进行相当复杂的转换。该技术从用 FlowC 语言描述的一组任务规范开始,FlowC 是在 C 语言基础上扩展了进程头和任务间通信(通过读写函数调用指定)的语言。

例如,有一个使用 FlowC 的输入规范,包含输入端口 IN COEF 以及输出端口 OUT ,进程间通过单向缓冲通道 DATA 进行通信。 GetData 任务从环境中读取数据并发送到通道 DATA ,每发送 N 个样本后,还会发送它们的平均值。 Filter 任务从通道读取 N 个值(忽略它们),然后读取平均值并乘以 c c 可以从端口 COEF 读取),最后将结果写入端口 OUT

该示例满足任务拆分的条件,通过 Cortadella 等人的技术,将 FlowC 程序先转换为(扩展)Petri 网,然后将每个任务的 Petri 网合并为一个单一的 Petri 网,再利用 Petri 网理论生成新的任务结构。

生成的任务 tau_in 代码如下:

tau_in () {
    READ(IN,sample,1);
    sum+=sample;
    i++;
    DATA=sample;
    d=DATA;
    /* merging of DATA & d feasible */
    L0: if (i<N) return;
    DATA=sum/N; d=DATA;
    d=d*c; WRITE(OUT,d,1);
    sum=0; i=0;
    return;
}

这个代码还可以通过任务内优化进一步优化,例如减少变量数量和简化条件判断。

通过以上这些优化策略和任务管理方法,可以有效提升嵌入式系统的性能和资源利用率。

3. 嵌入式系统编译器
3.1 嵌入式系统编译器的必要性

虽然标准编译器在很多情况下可用于嵌入式系统,因为它们通常价格便宜甚至免费,但为嵌入式系统设计特殊的优化和编译器有以下几个原因:
- 嵌入式系统的处理器架构具有特殊功能,编译器应利用这些功能来生成高效代码。

3.2 嵌入式系统编译器的优化示例

以 Cortadella 等人提出的任务结构优化为例,生成的 tau_in 任务代码可以进一步优化。原始的 tau_in 代码如下:

tau_in () {
    READ(IN,sample,1);
    sum+=sample;
    i++;
    DATA=sample;
    d=DATA;
    /* merging of DATA & d feasible */
    L0: if (i<N) return;
    DATA=sum/N; d=DATA;
    d=d*c; WRITE(OUT,d,1);
    sum=0; i=0;
    return;
}

在这个代码中,第一个 if 语句的测试总是为假(因为 j 等于 i - 1 ,且 i j 每当 i 等于 N 时会重置为 0),第三个 if 语句的测试总是为真(因为只有当 i 等于 N 且到达标签 L0 i 等于 j ),并且可以减少变量的数量。优化后的代码如下:

tau_in () {
    READ(IN,sample,1);
    sum+=sample;
    i++;
    DATA=sample;
    if (i<N) return;
    DATA=sum/N;
    DATA=DATA*c;
    WRITE(OUT,DATA,1);
    sum=0; i=0;
    return;
}

虽然目前很少有编译器能生成这样的优化版本,但这个例子展示了生成“好”的任务结构所需的转换类型。

4. 总结与展望

通过对高级优化策略和任务级并发管理的介绍,我们可以看到在嵌入式系统开发中,有多种方法可以提高系统的性能和资源利用率。以下是对各种优化技术的总结表格:
| 优化技术 | 描述 | 优点 | 缺点 |
| — | — | — | — |
| 循环优化 | 包括不同循环结构的选择和循环分块、拆分等 | 提高缓存性能、减少运行时间 | 难以判断最优转换 |
| 数组折叠 | 数组间和数组内折叠 | 节省内存空间 | 增加地址计算复杂度 |
| 浮点到定点转换 | 将浮点数据转换为定点数据 | 减少循环次数、降低能耗 | 损失一定精度 |
| 任务合并 | 合并任务图 | 减少上下文切换开销 | 可能影响任务独立性 |
| 任务拆分 | 拆分任务 | 提高调度自由度和资源利用率 | 增加任务管理复杂度 |

未来,随着嵌入式系统应用的不断扩展和处理器架构的不断发展,我们可以期待更多高效的优化技术和编译器的出现。例如,随着人工智能和机器学习在嵌入式系统中的应用增加,可能需要专门针对这些算法的优化技术。同时,编译器也需要不断改进,以更好地利用处理器的特殊功能,实现更高效的代码生成。

以下是一个简单的 mermaid 流程图,展示了嵌入式系统优化的整体流程:

graph TD;
    A[高级优化策略] --> B[循环优化];
    A --> C[数组折叠];
    A --> D[浮点到定点转换];
    E[任务级并发管理] --> F[任务合并];
    E --> G[任务拆分];
    B --> H[代码性能提升];
    C --> H;
    D --> H;
    F --> H;
    G --> H;
    H --> I[嵌入式系统编译器优化];
    I --> J[最终高效代码];

总之,嵌入式系统的优化是一个复杂而持续的过程,需要开发者不断探索和应用新的技术,以满足不断增长的性能和资源需求。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值