【问题标题】:Is it better to do multi-step calculations in one line?在一行中进行多步计算会更好吗?
【发布时间】:2021-05-24 14:27:43
【问题描述】:

这个问题专门针对嵌入式系统的 C 编译器,但我确信它可以应用于一般编程。

在一行中编写多步数学表达式而不是将它们分解为等效的单个操作是否有任何好处,反之亦然?

例如是:

int convert(int in){
  return (int)(((long)in * 727) / 12) + 9;
}

优于或差于:

int convert(int in){
  long out = (long)in;
  out *= 727;
  out /= 12;
  out += 9;
  return (int)out;
}

一般来说?

我的直觉是单行表达式可能更容易让编译器优化,但我不编写编译器,所以我不确定。

【问题讨论】:

  • 只要看一下两者的机器码,你就会发现有什么不同。
  • 浮点代码存在差异。 C 标准允许实现在评估表达式时使用额外的精度,但不允许在赋值或强制转换中使用。

标签: c embedded


【解决方案1】:

编译器如今的表现如此出色,以至于为这种微优化而烦恼几乎毫无意义。应该优化算法,而不是代码排列。做对他人更容易阅读和理解的事情。

用 GCC 编译这两个代码(第一级优化)给出了相同的汇编输出,所以它们是严格等价的: https://godbolt.org/z/91rrc4E69

【讨论】:

  • godbolt 链接非常有帮助,但我确实注意到该程序集在 -O0 优化级别并不严格等效,而且根据我的经验,并非每个编译器都像 gcc 一样聪明
  • 您在问题中说“专门针对嵌入式系统的 C 编译器”,期望 gcc 或 clang 您会使用什么? -O2 无论如何都会用于任何严重的事情
  • @ShellCode 有很多编译器,几乎适用于每个主板/cpu 供应商,例如用于 ARC CPU 的 Synopsis 编译器等...
  • 我使用的编译器和代码库太老了,它们必须在视频游戏模拟器中运行。我不希望这样的东西比 gcc -O0 更聪明
  • 好吧,我不知道。我更习惯于提供自己的 gcc 工具链的供应商,但没有什么太疯狂
【解决方案2】:

在一行中编写多步数学表达式而不是将它们分解为等效的单个操作是否有任何好处,反之亦然?

是的。一是,如果您使用字符或short 类型编写操作,它们将使用int 算术自始至终执行,从而允许更大的中间结果。相反,如果将每个中间结果分配给一个字符或short 变量,则每个中间结果都将简化为目标类型(以C 标准为无符号类型和有符号类型的实现定义的方式)。 1 在中间值中拥有更多“净空”通常是一种好处,但分配或强制转换可用于故意减少。

第二个是 C 标准允许以比标称类型更高的精度执行浮点运算。例如,可以将两个float 操作数相乘,然后再将另一个float 操作数相加,就好像这些操作数是double 一样,或者甚至就好像在乘法中存在无限精度一样。相反,赋值(和强制转换)需要将值转换为名义类型。这可能是一个好处(因为它倾向于减少舍入误差)或一个坏处(在为精确的 IEEE-754 语义进行编程时)。

第三个是单个表达式通常给编译器更多的灵活性来组织操作。对多个语句的中断操作会在它们之间引入序列点,从而限制编译器的优化。与其他人一样,使用单个表达式来允许编译器优化或使用多个语句来引入必要的排序可能会有好处。

从 cmets 和其他答案来看,这些都没有体现在对您的示例代码的检查中,但它们确实存在。

请注意,所有这些都会影响语义:好处或坏处是由于有效地告诉编译器它是否可以自始至终使用更宽的类型,是否可以使用额外的精度,以及是否必须排序确定的事情。就优化而言,使用一个语句或单独的语句并不是帮助现代编译器优化的有用方法。优秀的现代编译器以相当全面的方式分析程序语义,因此这种简单的表达式重组对它们几乎没有帮助。

脚注

1 此答案仅讨论intcharshort 更宽的常见情况。虽然这在现代 C 实现中很常见,但 C 标准并不要求这样做。

【讨论】:

    【解决方案3】:

    对于编译器来说,这两种方式没有区别。

    不过,对于试图理解代码的读者或调试代码的开发人员来说,具有重要中间值的公式很有帮助。

    【讨论】:

    • 轻微修正。对于没有任何优化级别的编译器,是有区别的。在第二种情况下,它必须在堆栈上分配和存储中间值。但是,一旦应用了优化级别,生成的二进制文件的结果应该是相同的:godbolt.org/z/WGPjad3W9
    • 在这种情况下,第一个可读性更好(对我来说)。
    • 双方同意。由于 OP 要求提供一般性答案,我认为他使用现代编译器的假设是可以的,不会手动降低默认优化级别,只是想用这段代码来说明他的观点。他的中间值在这里确实没有意义。
    • @AlexLop。 “什么更有效”这个问题你不启用优化是没有意义的。
    • @0___________ 为什么不呢? OP 没有说明使用了哪个编译器和什么优化级别。有时它是有道理的……例如,在开发过程中,必须调试代码,但在某些领域仍然需要高效的代码。 OP 专门用“嵌入式”标记了这个问题
    【解决方案4】:

    如果您在启用优化的情况下进行编译,则生成的代码没有区别。我更喜欢循序渐进,因为当您在几个月后回到项目时,它更不容易出错并且更容易理解。

    int convert0(int in){
      return (int)(((long)in * 727) / 12) + 9;
    }
    
    int convert1(int in){
      long out = (long)in;
      out *= 727;
      out /= 12;
      out += 9;
      return (int)out;
    }
    
    int convert2(int in){
      long out1 = (long)in;
      long out2 = out1 * 727;
      long out3 = out2 / 12;
      long out4 = out3 + 9;
      return (int)out4;
    }
    

    最好的方法是自己检查。只需查看发出的汇编代码:

    https://godbolt.org/z/3cTzaWsGe

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2022-06-14
      • 1970-01-01
      • 2015-02-11
      • 2016-04-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多