This article is part ofTechXchange:开发高质量的软件
What you'll learn:
- RISC-V支持的标准扩展。
- Hints to help the compiler make better decisions.
- 为什么避免编写“聪明的代码”的原因。
RV32i是可以在图1, 如:
- M(整数乘法)
- A (Atomic Instructions)
- F(单浮点)
- D (Double Floating Point)
- C(压缩说明)
- B(位操纵)
- 等等…。
Most extensions(图。1)被批准或冻结,但目前正在研究新的。可以在各种核心中看到的一个示例图2。
如果我们采用了通用设备RV32,我们可以认识到它支持M,F,D和C。C(压缩说明)通过为操作添加简短的16位指令来减少静态和动态代码大小,从而平均25% -30%的代码尺寸减少,并导致降低功耗和内存使用。此外,RV32E基本指令集(嵌入式)旨在为带有16个寄存器的嵌入式微控制器提供更小的基础核心。
Designers are free to implement their own extensions for specific needs, e.g., machine learning, low-power application, or optimized SoC for metering and motor control. The purpose of the standard extensions or custom extension is to achieve faster response time from calculations and processing performed in hardware that require mostly one or just a few cycles.
Why Professional Tools for RISC-V?
With the growth of RISC-V, the need arises for professional tools that can take full advantage of the core features and extensions. A well-designed and optimized SoC also should run the best optimized code so that companies can innovate fast, have outstanding products, and derive the best cost benefit out of it.
当涉及代码密度时,可以保存计数的每个字节。专业工具有助于优化应用程序,以最适合所需的需求。通过优化应用程序,客户将能够通过使用具有较小内存或汇总值的设备通过将功能添加到现有平台来节省资金(图3)。
与其他工具相比,RISC-V的专业编译器平均可以生成7%-10%的代码。
Writing Compiler-Friendly Code for Better Optimizations
优化编译器试图通过以最佳执行顺序选择正确的说明来生成既小和快速的代码。它通过反复将许多转换应用于源程序来做到这一点。大多数优化遵循基于合理理论基础的数学或逻辑规则。其他转换是基于启发式方法,在这种情况下,经验表明,某些转换通常会导致良好的代码或为进一步优化的机会打开机会。
因此,编写源代码的方式可以确定是否可以将优化应用于您的程序。有时,源代码的小变化可能会显着影响编译器生成的代码的效率。
尝试在尽可能少的行上编写您的代码,使用?: - 表达式,填充和逗号表达式以在单个表达式中挤压许多副作用,不会使编译器生成更有效的代码。最好的提示是以易于阅读的样式编写代码。
开发人员可以通过注意源代码中的以下提示来帮助编译器做出更好的决策:
1.仅做一次函数调用。编译器通常很难研究常见的子表达,因为子表达可能会产生副作用,如果必要时,编译器可能不知道这些编译器可能不知道。因此,在指示这样做时,编译器将对同一功能进行多个调用,这会浪费代码空间和执行开销。最好将功能分配到变量(很可能会存储在寄存器中)并在易于访问的寄存器中执行操作(图4)。
2. Pass by reference rather than by copy. When you pass a pointer to a primitive rather than the primitive itself, you save the compiler the overhead of copying that primitive somewhere in RAM or in a register. For a large array, this can save quite a bit of execution time. Passing by copy will force the compiler to insert code to copy the contents of the primitive.
3.使用正确的数据大小。有些MCUS(例如8051或AVR)是8位micros;有些像MSP430是16位;有些喜欢手臂和RISC-V为32位。当对您的核心使用“不自然”的大小时,编译器必须创建额外的开销来解释其中包含的数据,例如,32位MCU需要进行换档,掩盖和签名扩展操作才能达到值需要执行其操作。因此,最好将MCU的自然大小用于您的数据类型,除非有令人信服的理由不这样做,即。您还需要精确数量的位,或更大的类型(例如字符阵列)会占用过多的内存。
4. Using signedness appropriately. The signedness of a variable can affect the code that’s generated by the compiler. For example, division by a negative number is treated differently (by the rules of the C language) than that for a positive number. Ergo, if you use a signed number that will never be negative in your application, you can incur an extra test-and-jump condition in your code that wastes both code space and execution time. In addition, if the purpose of a variable is to do bit-manipulation, it should be unsigned or you could have unintended consequences when doing shifting and masking.
5.避免成为抛弃的。C通常会执行隐式铸件(例如,在浮子和整数之间以及ints和long longs之间),并且这些不是自由的。从较小的类型到较大类型的铸造将使用符号扩展操作,往返浮子的铸造将引入对浮点库的需求(这可以大大增加代码的大小)。自然,您应该避免尽可能多地制作明确的演员阵容,以避开这个额外的开销。当习惯使用INT和功能指针的桌面程序员互换时,可以轻松地看到此问题(图5)。
6.使用功能原型。如果不存在原型,则C语言规则规定必须将所有参数推广到整数,并且(如先前讨论)可以在运行时库中链接到不必要的开销中。
7.将全局变量读为临时变量。如果您在函数中多次访问全局变量,则可能需要将其读取为本地临时变量。否则,每次访问此变量时,都需要从内存中读取它。通过将其放入本地临时变量中,编译器可能会将寄存器分配给该值,以便可以更有效地对其进行操作(Fig. 6)。
8.避免内部组件。使用内联装配对优化器具有非常有害的影响。由于优化器对代码块一无所知,因此无法优化它。此外,由于它不知道代码在做什么,因此无法对手写块进行指令计划(这可能对DSP尤其损害)。最重要的是,开发人员必须每次检查手写代码,以确保其正确插入优化的C代码,以免产生意外的副作用。内联汇编器的可移植性非常差,因此,如果您决定将其移至新的体系结构,则需要重写(及其后果)。如果您必须内联汇编器,则应将其分为自己的汇编文件,并将其与源分开。
9. Don’t write clever code. Some developers erroneously believe that writing fewer source lines and making clever use of C constructions will make the code smaller or faster (i.e., they’re doing the compiler’s job for it). The result is code that’s difficult to read, impossible to understand for anyone but the person who originally wrote it and harder to compile. Writing it in a clear and straightforward manner improves the readability of your code and helps the compiler to make more informed decisions about how best to optimize your code.
For example, assume that we want to set the lowest bit of a variable b if the lowest 21 bits of another variable are set. The clever code uses the ! operator in C, which returns zero if the argument is non-zero (“true” in C is any value except zero), and one if the argument is zero. The straightforward solution is easy to compile into a conditional followed by a set bit instruction, since the bit-setting operation is obvious and the masking is likely to be more efficient than the shift. Ideally, the two solutions should generate the same code. The clever code, however, may result in more code since it performs two ! operations, each of which may be compiled into a conditional(Fig. 7)。
另一个例子包括使用条件lues in calculations. The “clever” code will result in larger machine code since the generated code will contain the same test as the straightforward code and adds a temporary variable to hold the one or zero to add to str. The straightforward code can use a simple increment rather than a full addition and doesn’t require the generation of intermediate results(Fig. 8)。
10.按顺序访问结构。如果您订购结构,从结构的一个元素到下一个元素,而不是在结构中跳来跳去,则编译器可以利用增量操作来访问结构的下一个元素,而不是试图计算其偏移量结构指针。在静态分配的结构中,由于地址是先验计算的,因此无法保存代码。但是,在大多数应用中,这些都是动态完成的。
结论
在过去的30年中,嵌入式编译器的发展巨大,尤其是与其优化功能有关。现代编译器采用许多不同的技术来生成非常紧密和高效的代码,因此您可以专注于以清晰,合乎逻辑和简洁的方式编写来源。每个开发人员都致力于实现其软件的最佳效率。编译器是非常复杂的软件,它们能够进行高度优化,但是通过遵循这些简单的提示,您可以帮助它达到更高的效率。
阅读更多文章TechXchange:开发高质量的软件