24、代码级时序优化全解析

代码级时序优化全解析

在代码开发过程中,时序优化是提升程序性能的关键环节。以下将详细探讨代码级时序优化的多个方面,包括函数优化、处理器特定指令的运用以及编译器优化等内容。

1. 函数sqrt的优化

在数学函数的使用中,结果的准确性与运行时间往往需要权衡。以平方根函数 sqrt 为例,通常编译器提供的库函数在整个取值范围内尽可能保证结果的准确性,但在运行时间方面效率较低。通过适当降低精度,可以显著减少运行时间。

代码示例如下:

unsigned short sqrtFast(unsigned short x)
{
    unsigned short a,b;
    b = x;
    a = x = 0x3f;
    x = b/x;
    a = x = (x+a)>>1;
    x = b/x;
    a = x = (x+a)>>1;
    x = b/x;
    x = (x+a)>>1;
    return x;
}

该函数通过一些加法、二进制移位操作和除法来近似计算一个数的平方根。与编译器库中的 sqrt 函数相比,计算结果的精度有所降低。对于小输入值,误差较大;对于大输入值,结果往往偏大。不过,其执行速度更快。

下面是 sqrt sqrtFast 函数核心执行时间(CET)的对比表:
| 函数 | CETmin | CETmax | CETø |
| ---- | ---- | ---- | ---- |
| sqrt | 114 | 646 | 569 |
| sqrtFast | 464 | 476 | 472(约快17%) |

需要注意的是,该计算包含对输入参数 x 的除法操作。如果 x 在运行时的值为零,某些处理器会触发异常或陷阱。因此,对于这类处理器,代码需要进行补充以正确处理 x = 0 的异常情况。

此外,考虑将平方根计算实现为内联函数,这样可以节省函数调用和返回的开销,同时更有效地利用缓存。

在进行函数优化时,有两个关键方面必须考虑:
- 确保优化后的变体的运行时间确实更低。许多所谓的优化可能会减少源代码行数,但生成的机器代码效率不高。因此,需要进行测量或代码模拟。
- 检查优化后的代码是否仍然提供原始功能,以及是否完全准确或存在可接受的偏差。在某些情况下,分析这些偏差所需的工作量可能很大,以至于不得不放弃代码优化过程。

2. AURIX的线性核心ID

第二代英飞凌AURIX处理器最多可配备六个CPU。使用地址为 FE1C 的特殊功能寄存器可以在运行时查询核心ID,但这些ID不是连续的。对于CPU 0到4,该寄存器返回值0到4,但CPU 5的核心ID为6。

对于大多数应用程序,软件需要为CPU提供连续的编号。通常,这个线性ID用作访问数组的索引,不连续的编号会造成阻碍。因此,需要进行转换,当输入值为0到4时返回输入值,当输入值为6时返回5。

以下是一个简单的实现代码:

#if defined __GNUC__
#include <machine/intrinsics.h>
#endif

inline unsigned char GetLinearCoreId(void)
{
#if defined __TASKING__
    unsigned char id = __mfcr(0xFE1C);
#elif defined __GNUC__
    unsigned char id = __MFCR(0xFE1C);
#else
#error "compiler not supported"
#endif
    if (id > 5) {
        return 5;
    } else {
        return id;
    }
}

int main(void){
    return (int)GetLinearCoreId();
}

在不启用优化的情况下,HighTec GCC编译器生成的汇编代码较多。而启用优化(使用 -O3 选项)后,生成的汇编代码仅需四条指令,利用了TriCore架构提供的 min 机器指令。

实际上,屏蔽高24位并不是必需的。AURIX处理器手册显示,地址为 0FE1C 的特殊功能寄存器是32位寄存器,但只有低三位表示核心ID,高29位是保留位。 min 机器指令确保 GetLinearCoreId 函数永远不会返回大于5的值。

进一步优化后的代码可以实现为宏:

#if defined __TASKING__
#define GetLinearCoreId( ) __min( __mfcr(0xFE1C), 5 )
#elif defined __GNUC__
#define GetLinearCoreId( ) ({ unsigned int coreId_;
    __asm( "mfcr %0, 0xFE1C\n\tmin.u %0, %0, 5" :
    "=d"(coreId_) ); coreId_; })
#else
#error "compiler not supported"
#endif

int main(void){
    return (int)GetLinearCoreId();
}

这个例子不仅展示了AURIX提供的特殊机器指令可以高效实现任务,还提供了代码级运行时优化的思路和方法。

3. 饱和计算

在许多计算中,要求结果进入饱和状态而不是溢出,即结果取其所能表示的最大值。同样,在发生下溢时,结果应取其所能表示的最小值。

以下是一个对两个无符号16位数字进行饱和加法的函数:

unsigned short sadd16(unsigned short a, unsigned short b)
{
    return (a > 0xFFFF - b) ? 0xFFFF : a + b;
}

编译器为AURIX生成的汇编代码使用了减法、加法、条件跳转等六条指令。

而AURIX提供了用于饱和计算的特殊机器代码指令,使用 sath 指令的实现如下:

unsigned short sadd16(unsigned short a, unsigned short b)
{
    return __sathu(a + b);
}

生成的汇编代码仅需三条机器代码指令即可实现相同的功能。这表明,尽管编译器进行了最高级别的优化,但仍可能无法找到最优实现,程序员对处理器特定指令的了解可以实现显著的优化。

4. 处理器特定指令概述

大多数微处理器的部分指令在其他微处理器中也能找到相同或相似的形式,如内存读写、寄存器内容复制、加减运算、跳转指令、子程序调用等。然而,许多处理器还提供一些不太常见的指令,如英飞凌TriCore架构中的 min sat.hu 指令。

当需要在C代码中插入特定的机器代码指令时,可以使用内联汇编、编译器内置函数或编译器内置宏。例如,在获取AURIX核心ID的代码中,使用了TASKING编译器的内置函数 __mfcr 或HighTec GCC编译器的内置宏 __MFCR

由于大多数编译器支持多种处理器架构,特殊指令并不总是得到最优支持。在这种情况下,需要明确指示编译器使用特殊指令。为了充分利用处理器指令集的潜力,程序员必须熟悉所使用处理器的指令集,并具备选择和使用最优机器指令的能力。

5. 编译器优化

将源代码编译为机器代码有多种映射方式,编译器优化的目标是找到最有效的映射。通常,效率可以从“占用内存少”或“运行时间短”的角度来理解,在编译器手册中分别描述为“优化大小”和“优化速度”。

这两个目标并不总是相互矛盾的。例如,16位饱和加法的优化变体在大小和速度上都优于初始版本。

“Leaf-Call Optimization”是一种既能减少内存需求又能减少运行时间的优化方法。在函数末尾的函数调用被替换为跳转命令,避免了调用和返回的组合,编译器直接生成跳转指令。

以下是一个简单的示例代码:

void Function_D(void)
{
    // Do something here...
}

void Function_C(void)
{
    // Do something here...
    Function_D();
}

void Function_B(void)
{
    // Do something here...
    Function_C();
}

void Function_A(void)
{
    // Do something here...
    Function_B();
}

int main(void)
{
    Function_A();
    return 0;
}

TASKING编译器在使用 ctc.exe -O2 -o main.src main.c 调用时生成的汇编代码展示了这种优化。

在选择编译器优化时,可以通过以下几种方式:
- 编译器提供了现成的优化组合,用户只需指定大致目标(“优化大小”或“优化速度”)或优化程度。常用的优化程度开关有 -O0 (禁用所有优化)和 -O1 -O3 (逐渐增强的优化)。如果不选择任何优化选项,编译器默认使用 -O0 。优化选项在命令行调用编译器时传递,如果构建环境支持,还可以为每个源代码文件调整优化设置。
- 对于编译器提供的每个优化,都有一个单独的编译器开关,可以在命令行调用编译器时传递。在进行代码级运行时优化之前,应仔细研究编译器手册中描述的可用优化。
- 可以在源代码的函数级别甚至函数内部直接激活或停用个别优化。不同编译器的实现机制不同,例如TASKING编译器使用 #pragma 语句,HighTec GCC编译器可以使用 __attribute__ 来为特定函数启用优化。

需要注意的是,调试经过优化编译的软件比调试未优化的软件困难得多。随着优化数量的增加,机器代码指令与相应源代码之间的关系变得更加复杂。例如,“代码重排序”优化会重新排列机器代码指令的顺序,虽然提高了运行效率,但在调试时会造成困扰。

综上所述,代码级时序优化是一个复杂而重要的过程,需要综合考虑函数优化、处理器特定指令的使用以及编译器优化等多个方面,以实现程序性能的提升。通过合理运用这些优化方法,可以在准确性和运行时间之间找到平衡,满足不同应用场景的需求。

代码级时序优化全解析

6. 优化方法总结与流程

为了更清晰地展示代码级时序优化的过程,我们可以总结出一个优化流程:

graph LR
    A[确定优化目标] --> B[分析代码性能瓶颈]
    B --> C{是否可函数优化}
    C -- 是 --> D[进行函数优化(如sqrt、核心ID转换等)]
    C -- 否 --> E{是否可利用处理器特定指令}
    E -- 是 --> F[使用内联汇编、内置函数或宏插入特定指令]
    E -- 否 --> G{是否可编译器优化}
    G -- 是 --> H[选择合适的编译器优化选项]
    G -- 否 --> I[重新分析性能瓶颈]
    D --> J[测试优化效果]
    F --> J
    H --> J
    J --> K{效果是否达标}
    K -- 是 --> L[完成优化]
    K -- 否 --> I

在这个流程中,首先要明确优化的目标,是追求运行时间的减少还是内存使用的降低,或者两者兼顾。然后对代码进行性能分析,找出可能存在的瓶颈。根据分析结果,依次考虑函数优化、处理器特定指令的利用和编译器优化。每一步优化后都要进行测试,检查效果是否达到预期,如果未达标则需要重新分析瓶颈。

7. 不同优化场景下的选择

在实际开发中,不同的应用场景可能需要不同的优化策略。以下是一些常见场景及对应的优化建议:
| 应用场景 | 优化重点 | 具体方法 |
| ---- | ---- | ---- |
| 实时系统 | 运行时间 | 优先考虑处理器特定指令和编译器的速度优化选项,如使用AURIX的特殊指令进行饱和计算,开启编译器的 -O3 优化 |
| 资源受限系统 | 内存使用 | 选择编译器的大小优化选项,如 -Os ,同时考虑函数的内联优化以减少函数调用开销 |
| 对精度要求不高的计算 | 运行速度 | 采用近似算法,如使用 sqrtFast 替代 sqrt 函数 |

8. 优化过程中的注意事项

在进行代码级时序优化时,还需要注意以下几点:
- 兼容性 :使用处理器特定指令和编译器特定的优化选项时,要确保代码在不同的处理器和编译器版本上具有兼容性。例如,不同编译器对内置函数和宏的支持可能不同,需要进行相应的调整。
- 调试难度 :如前文所述,优化后的代码调试难度会增加。在优化过程中,建议先进行未优化代码的调试,确保功能正确后再进行优化。同时,可以保留未优化版本的代码作为参考。
- 性能测试 :优化前后都要进行严格的性能测试,以确保优化确实带来了预期的效果。测试环境应尽可能接近实际运行环境,避免因测试环境差异导致结果不准确。

9. 常见问题及解决方案

在优化过程中,可能会遇到一些常见问题,以下是对应的解决方案:
| 问题 | 原因 | 解决方案 |
| ---- | ---- | ---- |
| 优化后性能未提升 | 优化方法选择不当或存在其他性能瓶颈 | 重新分析代码,检查是否有其他未发现的瓶颈,尝试不同的优化方法 |
| 编译器不支持特定指令或优化选项 | 编译器版本过低或不支持该处理器架构 | 更新编译器版本,或者选择支持该处理器的编译器 |
| 代码兼容性问题 | 使用了特定编译器或处理器的特性 | 对代码进行修改,使其具有更广泛的兼容性,或者提供不同版本的代码以适应不同环境 |

10. 总结与展望

代码级时序优化是提升程序性能的重要手段,通过函数优化、处理器特定指令的利用和编译器优化等方法,可以在准确性和运行时间之间找到平衡。在实际应用中,要根据具体的场景和需求选择合适的优化策略,并注意优化过程中的兼容性、调试难度和性能测试等问题。

随着处理器技术的不断发展,未来可能会有更多的特殊指令和优化方法出现。开发者需要不断学习和掌握新的技术,以更好地进行代码优化,满足日益增长的性能需求。同时,自动化优化工具也可能会得到进一步发展,帮助开发者更高效地完成优化任务。

通过深入理解和运用代码级时序优化的方法,开发者可以编写出更高效、更稳定的程序,为各种应用场景提供更好的支持。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值