为什么模运算会消耗更多功率?

电器工程 stm32 C 低电量
2022-01-18 06:54:26

我主要使用 Cortex-M4F 或 Cortex-M0/0+ 设备,例如:

  • STM32L4,G4
  • STM32L0, G0

我有时会看到类似这样的博客说“避免模数”以降低功耗。

比较这两个程序,这是避免模运算的正确方法吗?

假设 MCU 电源电压可以是 1.8 V 或 3.3 V。

while(1) { // CODE X, I thought removing conditional statements could perform better
    my_index++;
    if (my_index >= 8) my_index = 0;
}

VS

while(1) { // CODE Y
    my_index = (my_index + 1) % 8;
}

另外,为什么模运算消耗更多?

2个回答

那套规则是有道理的。但它们是有限的。

具体来说,“不要使用模数”这一笼统规则有些误导,应该真正意味着“避免使用导致除法运算的代码”。

(我的一套规则是了解硬件是如何工作的,编译成汇编并检查结果,分析你的代码,以及基准测试、基准测试、基准测试)。

如果您有一行代码表示a = b % c;then(假设 a、b 和 c 是整数),那么您正在向编译器指定c可以是任何整数值它必须在除法操作中编译。除法运算占用大量时间或逻辑区域;在任何一种情况下,这都转化为执行除法所消耗的能量。

在您的特定情况下,您所说my_index = (my_index + 1) % 8;的编译器(即使将优化设置为最低级别)也可能会将其转换为等效于my_index = (my_index + 1) & 0x0007;. 换句话说,它不会分裂(非常昂贵),它甚至不会分支(更便宜),但它会屏蔽(在当今大多数处理器上最便宜)。但这只有在模数是 2 的幂时才会发生。

您可以通过仅使用 , 来确保这一点my_index = (my_index + 1) & 0x0007;,但代价是代码理解和代码可维护性。如果你走那条路,请好好评论。

因此,在您的特定情况下,只要% 8不改变,或者只改变% NwhereN总是\$2^n\$并且编译器知道,速度就不会改变。但是,如果您或其他人稍后出现并将其更改为my_index = (my_index + 1) % 17;(或任何其他非 2 的幂),那么您的代码将突然进行除法运算,并且会更加耗电。在这种情况下,使用条件语句更便宜。

(在 C/C++ 中,您可以确保编译器通过使用# define语句或(取决于优化器)声明它const unsigned int或(更强大,如果它是 C++)声明它来提前知道常量的值constexpr int。其他编译语言 (即Rust)有他们自己的方式来实现这一点。)

注意:如果一个好的优化编译器不会将“if”构造转换为掩码,我不会感到惊讶——但如果没有,我不会感到惊讶。同上,一个非常好的优化编译器可能会看到my_index = (my_index + 1) % 17;并推断条件构造。如果不查看程序集,我认为我不会指望它,而且我认为我不会 100% 信任它——我可能会使用它,但我会在代码中添加一条关于跨越我的代码的评论手指,希望编译器能玩得很好。

除非您绝对支持功耗,否则您还应该考虑代码的可读性和脆弱性。 稍后会有人出现并且需要理解该代码,并且如果它不是一个充满机会的雷区,将会很感激它搞砸它。有人可能是未来的你,所以要友善!

首先,以防不明显:更长的执行时间意味着更多的功耗。尽管如果您最感兴趣的是降低功耗,请先查看系统时钟。

为什么模运算会消耗更多功率?

它不在现代编译器上。对于大多数内核来说,除法和取模是繁重的 CPU 操作,但生成实际divetc 指令的 C 代码大部分发生在大约 15 到 20 年前。当您启用优化时,现代编译器会选择最好的代码,并在可以避免的情况下避免除法。此外,在 8 和 16 苦味等低端 MCU 的性能方面,除法也是一个更大的问题。

但是,应该提到的是,在嵌入式系统中,在禁用所有优化的情况下运行是一种常见的做法。主要是因为在 90 年代和 2000 年代初期,各种质量平庸的嵌入式编译器的编译器优化器正确地建立了一个令人讨厌的错误声誉。

如果您在禁用优化的情况下运行,那么您当然只能靠自己,并且必须手动执行所有优化 - 除非您对 C 和目标 CPU 都有深入的了解,否则绝对不建议这样做。


让我们在 gcc-arm-none-eabi 中反汇编您的特定代码示例,使用-O3. 我做了这些独立的例子:

void func1 (void)
{
  static unsigned int my_index;
  while(1) 
  { 
    my_index++;
    if (my_index >= 8) my_index = 0;

    volatile unsigned int out = my_index;
  }
}

void func2 (void)
{
  static unsigned int my_index;
  while(1) 
  { 
    my_index = (my_index + 1) % 8;
    volatile unsigned int out = my_index;
  }
}

需要 volatile 作为副作用来阻止优化器完全删除代码。现在,使用 Godbolt https://godbolt.org/z/bM5M5v38h反汇编它,我们得到几乎相同的机器代码。看不到分裂。cmp由于指令(分支),添加的版本实际上表现得稍微差一点。


我认为删除条件语句可以表现得更好

通常是的,在你的情况下它确实是这样,尽管它是一个微优化。Cortex M 通常没有高级分支预测,也没有高速缓存。在 M0 上,甚至不值得考虑头痛。我相信一些 STM32x4 对简单形式的分支预测有一些硬件支持。高端 M7 等将具有缓存,然后避免分支更重要。


一般来说:

您应该努力编写尽可能可读的代码。然后在代码中存在实际性能瓶颈时进行优化。手动优化是高素质的工作,需要大量经验。

在这种特殊情况下,我会说加法/计数器版本更具可读性,因此无论多少 CPU 滴答声,我都会使用它。


至于你链接的博客,作者不是一个完整的菜鸟,提出了一些好的观点,但提到了一些奇怪甚至误导的东西。让我评论一下您从以下获得模数评论的项目符号列表:

  • 尽可能使用“静态常量”类来防止运行时复制消耗功率的数组、结构等。

    我不知道“静态常量”类应该是什么意思。C 尤其区分大小写,并且没有 class 关键字。我假设作者不知道正确的 C 术语,实际上的意思是说:static尽可能使用存储类说明符和 const 正确性。如果这就是他们的意思,那是一般的好建议。

  • 使用指针。对于初学者来说,它们可能是 C 语言中最难理解的部分,但它们最适合有效地访问结构和联合。

    这有点像告诉建筑工人使用混凝土......这是强制性的,而不是一种选择。指针是 C 语言的基本组成部分。

  • 避免模​​数!

    正如上面所证明的,这不是很好的建议。

  • 在可能的情况下,局部变量优于全局变量。局部变量包含在 CPU 中,而全局变量存储在 RAM 中,CPU 访问局部变量的速度更快。

    尽管文件范围变量(“全局变量”)也可能临时存储在寄存器中,但通常是正确的。避免真正的全局(外部链接)变量的主要原因是程序设计,而不是性能。

    此外,在大多数 MCU 上,寄存器和 RAM 访问之间的差异并不大,此评论主要适用于 x86、Cortex A、Power PC 等高端 CPU。当手动优化 Cortex M 等中端 MCU 的内存访问时,您应该而是考虑闪存与 RAM,因为闪存通常具有等待状态。

    然而,减少变量的范围对于可读性来说总是一件好事,可以最大限度地减少错误并减少命名空间的混乱。

  • 在可能的情况下,无符号数据类型是您最好的朋友。

    是的,但不是因为性能,而是因为在按位运算中使用时有符号/负操作数的隐式转换和定义不明确的行为。

  • 尽可能为循环采用“倒计时”。

    好吧,这比模一更糟糕的恐龙建议。这样做的基本原理是众所周知的,与零比较比与一个值比较更快。但是编译器已经能够进行这种优化很长时间了!不要写下计数循环,这只是一无所获的混淆。这是 1993 年左右的有效建议,而不是 2022 年。

  • 使用位掩码代替无符号整数的位字段。

    很好的建议,但也与性能无关,但与可移植性和定义不明确的行为有关。

总体而言,博客文章的质量是多种多样的:非常好的建议与普通的坏建议混合在一起。我会停止阅读那个博客。请注意,在这个答案中,我在评论它时必须多久回到 20-30 年前。