77、Cortex-M33处理器的DSP指令与优化策略

Cortex-M33处理器的DSP指令与优化策略

1. Cortex-M33的DSP指令集

Cortex-M33处理器拥有丰富的数字信号处理(DSP)指令集,涵盖了整数和浮点运算,以下是一些主要指令的介绍:
- 16位整数指令
- SADD16 :使用单指令多数据(SIMD)技术对两个16位值进行加法运算。若发生溢出,结果会回绕。该指令可在Cortex - M4、Cortex - M7和带有DSP扩展的Armv8 - M主线处理器上使用,执行时间为1个周期。

int32_t x, y, z;
z = __SADD16(x, y);
- **QADD16**:同样使用SIMD技术对两个16位值进行加法运算,但在溢出时会进行饱和处理。正数饱和到0x7FFF,负数饱和到0x8000。支持的处理器与SADD16相同,执行时间为1个周期。
int32_t x, y, z;
z = __QADD16(x, y);
- **SSAT16**:将两个有符号16位值饱和到指定的位位置B。结果值会被限制在 - 2^(B - 1) 到 2^(B - 1) - 1 的范围内。此指令只能通过内联函数在C代码中使用。
int32_t x, y;
y = __SSAT16(x, 12); // 饱和到第12位
- **SMLABB**:将两个寄存器的低16位相乘,并将结果累加到一个32位累加器中。若加法过程中发生溢出,结果会回绕。
int16_t x, y;
int32_t acc1, acc2;
acc2 = acc1 + (x * y);
- **SMLAD**:对两个有符号16位值进行两次乘法运算,并将结果累加到一个32位累加器中。即 (top * top) + (bottom * bottom)。若加法过程中发生溢出,结果会回绕。
sum = __SMLAD(x, y, z);
- **SMLALBB**:将两个寄存器的低16位相乘,并将结果累加到一个64位累加器中。
int16_t x, y;
int64_t acc1, acc2;
acc2 = acc1 + (x * y);
- **SMLALD**:执行两次16位乘法运算,并将结果累加到一个64位累加器中。若累加过程中发生溢出,结果会回绕。
// Input arguments each containing 2 packed 16-bit values
// x[31:16] x[15:0], y[31:15] y[15:0]
uint32_t x, y;
// 64-bit accumulator
uint64_t acc;
// Computes acc += x[31:15]*y[31:15] + x[15:0]*y[15:0]
acc = __SMLALD(x, y, acc);
  • 8位整数指令
    • SADD8 :使用SIMD技术对四个8位值进行加法运算。若发生溢出,结果会回绕。
// Input arguments contain 4 8-bit values each:
// x[31:24] x[23:16] x[15:8] x[7:0]
// y[31:24] y[23:16] y[15:8] y[7:0]
int32_t x, y;
// Result also contains 4 8-bit values:
// z[31:24] z[23:16] z[15:8] z[7:0]
int32_t z;
// Computes without saturation:
//
z[31:24] = x[31:24] + y[31:24]
//
z[25:16] = x[25:16] + y[25:16]
//
z[15:8] = x[15:8] + y[15:8]
//
z[7:0] = x[7:0] + y[7:0]
z = __SADD8(x, y);
- **QADD8**:使用SIMD技术对四个8位值进行加法运算,溢出时进行饱和处理。正数饱和到0x7F,负数饱和到0x80。
// Input arguments contain 4 8-bit values each:
// x[31:24] x[23:16] x[15:8] x[7:0]
// y[31:24] y[23:16] y[15:8] y[7:0]
int32_t x, y;
// Result also contains 4 8-bit values:
// z[31:24] z[23:16] z[15:8] z[7:0]
int32_t z;
// Computes with saturation:
//
z[31:24] = x[31:24] + y[31:24]
//
z[25:16] = x[25:16] + y[25:16]
//
z[15:8] = x[15:8] + y[15:8]
//
z[7:0] = x[7:0] + y[7:0]
z = __QADD8(x, y);
  • 浮点指令
    • VABS.F32 :计算浮点值的绝对值。
float x, y;
y = fabs(x);
- **VADD.F32**:对两个浮点值进行加法运算。
float x, y, z;
z = x + y;
- **VDIV.F32**:对两个浮点值进行除法运算,可能需要多个周期。
float x, y, z;
z = x / y;
- **VMUL.F32**:对两个浮点值进行乘法运算。
float x, y, z;
z = x * y;
- **VMLA.F32**:将两个浮点值相乘,并将结果累加到一个浮点累加器中。
float x, y, z, acc;
acc = z + (x * y);
- **VFMA.F32**:融合浮点乘法累加运算,比标准的浮点乘法累加(VMLA)更精确,因为它只进行一次舍入操作。
float x, y, acc;
acc = 0;
__fmaf(x, y, acc);
- **VNEG.F32**:将一个浮点值乘以 - 1。
float x, y;
y = -x;
- **VSQRT.F32**:计算浮点值的平方根,可能需要多个周期。
float x, y;
y = __sqrtf(x);
- **VSUB.F32**:对两个浮点值进行减法运算。
float x, y, z;
z = x - y;
2. Cortex-M33的优化策略

为了充分发挥Cortex-M33处理器的性能,可采用以下优化策略:
- 负载和存储指令调度 :Cortex-M33处理器的负载或存储指令需要1个周期,但如果下一条指令使用了加载的数据,由于其设计特性,流水线可能会停滞。为了最大化性能,应尝试在负载和数据处理指令之间调度其他指令。例如,在点积计算中,可将循环计数器更新操作放在负载和MAC(SMLAL指令)之间,以避免浪费时钟周期。此优化适用于整数和浮点运算。
- 检查中间汇编代码 :底层的DSP算法可能看起来简单易优化,但编译器在设置时可能会出现混淆。因此,需要仔细检查C编译器的中间汇编输出,确保使用了正确的汇编指令。同时,检查中间代码,查看寄存器是否被正确使用,或者中间结果是否被存储在栈上。如果发现问题,可重新检查编译器设置或参考编译器文档。若使用Keil MDK,可通过以下步骤启用中间汇编输出文件的生成:
1. 打开目标选项窗口的“Listings”选项卡。
2. 勾选“Assembly Listing”。
3. 重新构建项目。
- 启用优化 :编译器生成的代码在调试模式和优化模式下差异很大。调试模式下生成的代码是为了方便调试,而不是为了执行速度。编译器通常提供多种优化级别,以Arm Compiler 6为例,其优化级别如下表所示:
| 级别 | 描述 |
| ---- | ---- |
| -O0 | 关闭大部分优化。调试时生成的代码直接映射到源代码,可能会导致程序镜像显著增大,不推荐用于一般调试。 |
| -O1 | 受限优化。调试时在镜像大小、性能和调试视图质量之间取得较好的平衡,是当前推荐的源代码级调试级别。 |
| -O2 | 高速优化。由于循环展开和函数内联,代码大小会增加,调试视图可能不太理想,因为目标代码与源代码的映射关系可能不清晰。 |
| -O3 | 非常高的速度优化。此级别下调试可见性通常较差。 |
| -Ofast | 启用级别3的所有优化,包括使用快速数学模式(-ffp - mode = fast armclang选项)的优化。还会执行其他激进的优化,可能会违反严格的语言标准合规性。 |
| -Omax | 最大优化。专门针对性能优化,启用快速级别以上的所有优化以及其他激进优化。此级别会启用链接时优化(LTO),默认情况下在Keil MDK项目选项中不可用,若需要此级别优化,需手动在“Misc control”字段中添加该选项。 |
| -Os | 进行优化以减小代码大小,在代码大小和代码速度之间进行平衡。 |
| -Oz | 进行优化以最小化镜像大小。 |
一般来说,为了获得最佳性能,通常使用 -O3 或 -Ofast 等高优化级别。但如果代码是经过精心安排指令顺序编写的,使用这些高级别优化可能会导致指令重新排序,因此可能需要尝试不同的优化级别选项,以确定特定算法的最佳性能。
- MAC指令的性能考虑 :从概念上讲,MAC指令先进行乘法运算,然后立即进行加法运算。由于加法需要乘法的结果,因此MAC的整体操作可能需要多个周期。对于整数MAC操作,如果MAC的结果不用于下一次乘法阶段,连续的整数MAC操作是可行的。对于浮点MAC操作,MAC指令(VMLA.F32 / VFMA.F32)需要3个周期。通过将MAC拆分为单独的乘法(VMUL.F32)和加法(VADD.F32)操作,并合理安排指令顺序,可将浮点MAC操作的周期数减少到2个周期。在为Cortex-M33处理器进行速度优化时,建议避免使用浮点MAC指令,仅使用单独的算术运算。虽然这样会增加代码大小,并且无法利用融合MAC的更高精度,但代码大小的增加通常可以忽略不计,且在大多数情况下对结果没有影响。
- 循环展开 :Cortex-M33每次循环迭代有2到3个周期的开销(如果分支目标是未对齐的32位指令,则为3个周期)。将循环展开N倍可以有效地将每个迭代的循环开销降低到2/N或3/N个周期。这在内部循环只包含少量指令时可以节省大量时间。可以手动展开循环,也可以让编译器自动完成。Keil MDK编译器支持使用编译指示来指导编译器操作。例如,使用以下编译指示可以让编译器展开循环:

#pragma unroll
for(i = 0; i < L; i++)
{
    ...
}
在Arm Compiler 6中,此编译指示仅在优化级别为 -O2 或更高时有效。默认情况下,Arm Compiler 6(即armclang)会完全展开循环,而Arm Compiler 5(即armcc)会将循环展开4倍。此编译指示可用于“for”、“while”和“do - while”循环。指定 #pragma unroll(N) 可使循环展开N次。在展开循环时,要确保检查生成的代码,以确保循环展开有效。关键是要注意寄存器的使用情况,过度展开循环可能会导致寄存器不足,中间结果将被存储在栈上,从而导致性能下降。
  • 关注内循环 :许多DSP算法包含多个嵌套循环。内循环的处理频率最高,应作为优化工作的重点。内循环中的节省会被外循环次数放大。只有在内循环优化良好后,才应考虑优化外循环。许多工程师花费大量时间优化非关键代码,但性能提升甚微。
  • 内联函数 :每次函数调用都会有一定的开销。如果函数较小且频繁执行,可以考虑将函数代码内联,以消除函数调用开销。
  • 寄存器计数 :C编译器使用寄存器来保存中间结果。如果编译器的寄存器不足,结果会被存储在栈上,这就需要额外的负载和存储指令来访问栈上的数据,从而增加开销。在开发算法时,建议先使用伪代码计算所需的寄存器数量,包括指针、中间数值和循环计数器。通常,使用最少中间寄存器的实现是最佳的。在定点算法中,要特别注意寄存器的使用。Cortex-M33处理器只有13个通用寄存器可用于存储整数变量,而浮点单元会额外提供32个浮点寄存器。大量的浮点寄存器使得循环展开和负载存储指令分组更容易实现。
  • 使用合适的精度 :Cortex-M33处理器提供了几种对32位数字进行乘法累加的指令,有些指令提供64位结果(如SMLAL),有些提供32位结果(如SMMUL)。虽然它们都可以在单条指令中执行,但SMLAL需要两个寄存器来保存64位结果,执行速度可能较慢。这可能是由于值在处理器和内存之间频繁传输,或者寄存器被耗尽。一般来说,首选64位中间结果,但需要检查生成的代码,确保实现是高效的。
3. Cortex-M33指令的局限性

尽管Cortex-M33的DSP指令集相当全面,但仍存在一些局限性:
- 饱和定点算术 :饱和定点算术仅适用于加法和减法运算,不适用于MAC指令。出于性能考虑,通常会使用定点MAC指令,因此需要对中间操作进行缩放,以避免溢出。
- 8位SIMD MAC支持 :不支持8位值的SIMD MAC操作,建议使用16位SIMD MAC代替。

4. 编写优化的DSP代码示例 - Biquad滤波器

Biquad滤波器是一种二阶递归或无限脉冲响应(IIR)滤波器,广泛应用于音频处理中,如均衡、音调控制、响度补偿、图形均衡器和分频器等。高阶滤波器可以通过级联二阶Biquad部分来构建。在许多应用中,Biquad滤波器是处理链中计算量最大的部分。

Biquad滤波器有两种常见的实现形式:直接形式I和直接形式II。直接形式I有五个系数、四个状态变量,每个输出样本需要进行五次MAC操作。直接形式II有五个系数、两个状态变量,每个输出样本同样需要进行五次MAC操作。两种形式在数学上是等价的,但直接形式II所需的状态变量数量是直接形式I的一半。在定点实现中,由于直接形式I的状态变量与输入输出有更直接的关系,不易溢出,因此更受青睐;而在浮点实现中,由于直接形式II的状态变量较少,通常更受欢迎。

以下是使用直接形式II实现的Biquad滤波器的标准C代码:

// b0, b1, b2, a1, and a2 are the filter coefficients.
// a1 and a2 are negated.
// stateA, stateB, and stateC represent intermediate state variables.
for (sample = 0; sample < blockSize; sample++)
{
    stateA = *inPtr++ + a1*stateB + a2*stateC;
    *outPtr++ = b0*stateA + b1*stateB + b2*stateC;
    stateC = stateB;
    stateB = stateA;
}
// Persist state variables for the next call
state[0] = stateB;
state[1] = stateC;

将上述代码的内循环分解为单个Cortex-M33指令,每个样本的执行需要20 - 21个周期:

stateA = *inPtr++;  // 数据获取 [1周期]
stateA += a1*stateB; // MAC,结果用于下一条指令 [3周期]
stateA += a2*stateC; // MAC,结果用于下一条指令 [3周期]
out = b0*stateA;    // 乘法,结果用于下一条指令 [2周期]
out += b1*stateB;   // MAC,结果用于下一条指令 [3周期]
out += b2*stateC;   // MAC,结果用于下一条指令 [3周期]
*outPtr++ = out;    // 数据存储 [1周期]
stateC = stateB;    // 寄存器移动 [1周期]
stateB = stateA;    // 寄存器移动 [1周期]
// 循环开销 [2到3周期]

为了优化该函数,可采取以下步骤:
1. 拆分MAC指令 :将MAC指令拆分为单独的乘法和加法操作,并重新排序计算,以避免下一个周期需要浮点运算的结果。使用一些额外的变量来保存中间结果。

stateA = *inPtr++  // 数据获取 [1周期]
prod1 = a1*stateB; // 乘法 [1周期]
prod2 = a2*stateC; // 乘法 [1周期]
stateA += prod1;   // 加法 [1周期]
prod4 = b1*stateB; // 乘法 [1周期]
stateA += prod2;   // 加法 [1周期]
out = b2*stateC;   // 乘法 [1周期]
prod3 = b0*stateA  // 乘法 [1周期]
out += prod4;      // 加法 [1周期]
out += prod3;      // 加法 [1周期]
stateC = stateB;   // 寄存器移动 [1周期]
stateB = stateA;   // 寄存器移动 [1周期]
*outPtr++ = out;   // 数据存储 [1周期]
// 循环开销 [2到3周期]

通过这些更改,Biquad滤波器的内循环从20个周期减少到15个周期。

  1. 进一步优化
    • 消除寄存器移动 :通过巧妙使用中间变量,避免寄存器移动操作。状态变量的顺序可以按一定规律循环使用,以减少不必要的赋值操作。
    • 循环展开 :将循环展开3倍,以减少循环开销。循环开销将被分摊到三个样本上。
    • 指令调度 :合理安排指令顺序,避免流水线停滞。如果一条指令(如内存加载或MAC)在流水线的第三阶段产生结果,而下一条指令在流水线的第二阶段立即使用该结果,可能会导致流水线停滞。为了避免这种情况,应在加载/MAC指令和使用其结果的指令之间安排其他指令。
    • 负载和存储指令分组 :在上述代码中,负载和存储指令是孤立的,每个操作需要2个周期(对于Cortex - M3/M4处理器)。通过批量加载和存储多个结果,第二次及后续的内存访问只需1个周期。

以下是优化后的代码:

in1 = *inPtr++;    // 数据获取 [1周期]
in2 = *inPtr++;    // 数据获取 [1周期]
in3 = *inPtr++;     // 数据获取 [1周期]
prod1 = a1*stateB;  // 乘法 [1周期]
prod2 = a2*stateC;  // 乘法 [1周期]
stateA = in1+prod1; // 加法 [1周期]
prod4 = b1*stateB;  // 乘法 [1周期]
stateA += prod2;    // 加法 [1周期]
out1 = b2*stateC;   // 乘法 [1周期]
prod3 = b0*stateA;  // 乘法 [1周期]
out1 += prod4;      // 加法 [1周期]
out1 += prod3;      // 加法 [1周期]
prod1 = a1*stateA;  // 乘法 [1周期]
prod2 = a2*stateB;  // 乘法 [1周期]
stateC = in2+prod1; // 加法 [1周期]
prod4 = b1*stateA;  // 乘法 [1周期]
stateC += prod2;    // 加法 [1周期]
out2 = b2*stateB;   // 乘法 [1周期]
prod3 = b0*stateC;  // 乘法 [1周期]
out2 += prod4;      // 加法 [1周期]
out2 += prod3;      // 加法 [1周期]
prod1 = a1*stateC;  // 乘法 [1周期]
prod2 = a2*stateA;  // 乘法 [1周期]
stateB = in3+prod1; // 加法 [1周期]
prod4 = b1*stateC;  // 乘法 [1周期]
stateB += prod2;    // 加法 [1周期]
out3 = b2*stateA;   // 乘法 [1周期]
prod3 = b0*stateB;  // 乘法 [1周期]
out3 += prod4;      // 加法 [1周期]
out3 += prod3;      // 加法 [1周期]
outPtr++ = out1;    // 数据存储 [1周期]
outPtr++ = out2;    // 数据存储 [1周期]
outPtr++ = out3;    // 数据存储 [1周期]
// 循环开销 [2到3周期]

经过优化,计算三个输出样本需要35或36个周期,即每个样本约12个周期。此代码适用于长度为3的倍数的向量。如果样本总数不是3的倍数,代码需要额外的处理来处理剩余的1或2个样本。

通过以上对Cortex-M33处理器的DSP指令集和优化策略的介绍,以及Biquad滤波器的优化示例,我们可以看到合理使用指令和优化策略能够显著提高处理器的性能。在实际应用中,需要根据具体的算法和需求,灵活运用这些方法,以达到最佳的性能表现。

Cortex-M33处理器的DSP指令与优化策略

5. 优化策略的应用总结

为了更清晰地展示上述优化策略的应用,我们可以将其总结为以下流程图:

graph LR
    A[开始] --> B[负载和存储指令调度]
    B --> C[检查中间汇编代码]
    C --> D[启用优化]
    D --> E[MAC指令优化]
    E --> F[循环展开]
    F --> G[关注内循环]
    G --> H[内联函数]
    H --> I[寄存器计数]
    I --> J[使用合适的精度]
    J --> K[结束]

这个流程图展示了在优化Cortex - M33处理器代码时,可以按照的一系列步骤。从负载和存储指令的调度开始,逐步进行各项优化,最终达到提高性能的目的。

同时,我们可以将这些优化策略的要点总结如下表:
| 优化策略 | 操作要点 |
| ---- | ---- |
| 负载和存储指令调度 | 在负载和数据处理指令之间调度其他指令,避免流水线停滞。 |
| 检查中间汇编代码 | 检查编译器中间汇编输出,确保指令正确,寄存器使用合理。若使用Keil MDK,可在目标选项窗口“Listings”选项卡勾选“Assembly Listing”并重建项目生成中间汇编文件。 |
| 启用优化 | 根据需求选择合适的优化级别,如 -O3 或 -Ofast 以获得最佳性能,但可能需要根据代码情况调整。 |
| MAC指令优化 | 对于浮点MAC操作,拆分为单独的乘法和加法操作,合理安排指令顺序以减少周期数。 |
| 循环展开 | 使用编译指示让编译器展开循环,注意检查生成代码和寄存器使用情况。 |
| 关注内循环 | 优先优化内循环,因为内循环处理频率最高,节省效果会被外循环放大。 |
| 内联函数 | 对于小且频繁执行的函数,将代码内联以消除函数调用开销。 |
| 寄存器计数 | 开发算法时用伪代码计算所需寄存器数量,尽量使用最少中间寄存器。定点算法中注意通用寄存器数量。 |
| 使用合适的精度 | 选择合适的乘法累加指令,检查生成代码确保实现高效。 |

6. 其他可能的优化思路

除了上述提到的优化策略,还有一些其他的思路可以进一步提高Cortex - M33处理器的性能:
- 并行计算 :利用Cortex - M33的SIMD指令,尝试将一些可以并行处理的计算任务进行并行化。例如,对于多个数据的相同操作,可以使用SIMD指令同时处理多个数据,从而提高处理速度。
- 数据预取 :在进行数据处理之前,提前将可能需要的数据加载到缓存中,减少数据访问的延迟。这样可以避免在处理过程中等待数据从内存中读取,提高整体性能。
- 算法优化 :对具体的算法进行深入分析,寻找可以优化的地方。例如,某些算法可能存在冗余计算,可以通过去除冗余来减少计算量。或者,使用更高效的算法来替代原有的算法。

7. 总结与展望

通过对Cortex - M33处理器的DSP指令集、优化策略以及Biquad滤波器优化示例的介绍,我们了解到合理运用这些指令和策略能够显著提升处理器的性能。在实际应用中,我们需要根据具体的算法和需求,灵活选择和组合这些优化方法。

在未来的开发中,随着技术的不断发展,Cortex - M33处理器可能会有更多的优化空间。例如,随着编译器技术的进步,可能会有更智能的优化选项出现,能够自动识别代码中的性能瓶颈并进行优化。同时,新的指令集扩展也可能会进一步提高处理器的计算能力。

总之,对于使用Cortex - M33处理器进行DSP开发的开发者来说,掌握这些优化策略和指令集知识是非常重要的。通过不断地实践和探索,我们可以充分发挥处理器的性能,开发出更加高效的DSP应用程序。

以下是一个简单的总结列表,回顾本文的主要内容:
1. 介绍了Cortex - M33处理器的多种DSP指令,包括16位整数指令、8位整数指令和浮点指令。
2. 阐述了一系列优化策略,如负载和存储指令调度、检查中间汇编代码、启用优化等。
3. 以Biquad滤波器为例,展示了如何应用这些优化策略来提高代码性能。
4. 提出了其他可能的优化思路,如并行计算、数据预取和算法优化。

希望本文能够为开发者在Cortex - M33处理器的DSP开发中提供有益的参考和指导。在实际应用中,不断尝试和实践这些方法,结合具体的需求和场景,将能够实现更好的性能表现。

基于可靠性评估序贯蒙特卡洛模拟法的配电网可靠性评估研究(Matlab代码实现)内容概要:本文围绕“基于可靠性评估序贯蒙特卡洛模拟法的配电网可靠性评估研究”,介绍了利用Matlab代码实现配电网可靠性的仿真分析方法。重点采用序贯蒙特卡洛模拟法对配电网进行长时间段的状态抽样统计,通过模拟系统元件的故障修复过程,评估配电网的关键可靠性指标,如系统停电频率、停电持续时间、负荷点可靠性等。该方法能够有效处理复杂网络结构设备时序特性,提升评估精度,适用于含分布式电源、电动汽车等新型负荷接入的现代配电网。文中提供了完整的Matlab实现代码案例分析,便于复现和扩展应用。; 适合人群:具备电力系统基础知识和Matlab编程能力的高校研究生、科研人员及电力行业技术人员,尤其适合从事配电网规划、运行可靠性分析相关工作的人员; 使用场景及目标:①掌握序贯蒙特卡洛模拟法在电力系统可靠性评估中的基本原理实现流程;②学习如何通过Matlab构建配电网仿真模型并进行状态转移模拟;③应用于含新能源接入的复杂配电网可靠性定量评估优化设计; 阅读建议:建议结合文中提供的Matlab代码逐段调试运行,理解状态抽样、故障判断、修复逻辑及指标统计的具体实现方式,同时可扩展至不同网络结构或加入更多不确定性因素进行深化研究。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值