嵌入式系统优化与并发管理全解析
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
元素的引用次数将达到
N³
。
为了改善这种情况,科学家设计了分块算法,以下是分块后的矩阵乘法代码,块大小参数为
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
的一个
B²
大小的块内。如果这个块能放入缓存,内层循环的第一次执行会将该块加载到缓存中,后续执行可以重用这些元素,从而将对主存中
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[最终高效代码];
总之,嵌入式系统的优化是一个复杂而持续的过程,需要开发者不断探索和应用新的技术,以满足不断增长的性能和资源需求。
超级会员免费看
2346

被折叠的 条评论
为什么被折叠?



