【问题标题】:Optimize Case Statement优化案例陈述
【发布时间】:2017-03-10 00:20:06
【问题描述】:

我们有一种将命令数据写入设备的方法。该方法首先将数据转换为设备接受的形式,然后将数据写入串口。数据转换是通过下面给出的 case 语句完成的。对于 10 个命令,我们需要转换数据。对于其他命令,我们不必转换数据(大约 10 个命令)。

客户抱怨代码未优化。一些不需要数据转换的命令是经常使用的。

if else 语句会直接优化代码吗?
在这种情况下是否有任何其他选项可以优化代码?

switch (cmd_no)
{
case CMD_WR_ACC:
    converted_command_data = (INT32)((((DOUBLE)cmd_data * CMD_WR_ACC_PARA1) / CMD_WR_ACC_PARA2) + 0.5);
    break;
case CMD_WR_BIAS:
    converted_command_data = (INT32)((((DOUBLE)cmd_data * CMD_WR_BIAS_PARA1) / CMD_WR_BIAS_PARA2) + 0.5);
    break;
case CMD_WR_SUP:
    converted_command_data = (INT32)((((DOUBLE)cmd_data * CMD_WR_SUP_PARA1) / CMD_WR_SUP_PARA2) + 0.5);
    break;
case CMD_WR_FIL:
    converted_command_data = (INT32)((((DOUBLE)cmd_data * CMD_WR_FIL_PARA1) / CMD_WR_FIL_PARA2) + 0.5);
    break;
    .
    .
default:
    converted_command_data = cmd_data;
    break;
}

【问题讨论】:

  • 有必要使用double吗?
  • 首先:测量它。其次:if/else ifs 的构造实际上是切换的最坏情况。
  • “客户抱怨代码未优化。”您如何定义优化?为什么上面的代码没有被认为是优化的?你对问题的定义还不够。
  • 使用static inline 或许还有一个早期的 if 语句来处理最频繁的命令(如果确实有的话),应该可以解决问题。
  • 如果cmd_data 和所有*_PARA1*_PARA2 都是整数,那么您可以重写呈现的转换以仅使用整数算术。这可能会使那些速度更快。

标签: c optimization


【解决方案1】:

如果您的客户所说的“未优化”意味着重复,看起来您可以将大量此类逻辑放在一个表格中。

假设您的命令类型是按顺序编号的,即

#define CMD_WR_ACC 0
#define CMD_WR_BIAS 1
#define CMD_WR_SUP 2
...

您可以这样定义表格:

struct params {
    int convert;       // whether or not to convert
    double param1;
    double param2;
} params_table[] = {
    { 1, CMD_WR_ACC_PARA1, CMD_WR_ACC_PARA2 },
    { 1, CMD_WR_BIAS_PARA1, CMD_WR_BIAS_PARA2 },
    { 1, CMD_WR_SUP_PARA1, CMD_WR_SUP_PARA2 },
    ...
    { 0, 0, 0}
    ...
};

那么你的代码如下所示:

if (params_table[cmd_no].convert) {
    converted_command_data = (INT32)((((DOUBLE)cmd_data * 
         params_table[cmd_no].param1) / params_table[cmd_no].param2) + 0.5);
} else {
    converted_command_data = cmd_data;
}

如果您的命令的起始索引不是 0,您需要从 cmd_no 中减去最小的命令编号才能将索引放入表中。

【讨论】:

  • 不错的重构+1。它可能并不比 switch 更快或明显更小。
  • 客户抱怨代码审查时的优化。我必须检查是什么困扰着他。重构不会像其他命令那样起作用,公式不同。由于 itron 系统尚未准备好,因此无法进行分析。
  • 因为结果被截断为 32 位,而 double 有 52 位尾数,您可以安全地将乘数和除数组合成一个乘法双常数。大多数 C 编译器无法推断出这一点,因此它甚至可能产生运行时加速。另外,你的 if 子句应该是if (params_table[cmd_no].convert)
  • @NominalAnimal 谢谢。已编辑。
  • 编译器可能足够聪明,可以优化开关。查看生成的程序集以获得一个想法。但是这种解决方案的优点是使代码更清晰。您可以有一个执行转换的函数表,但这感觉有点矫枉过正。
【解决方案2】:

if else 语句会直接优化代码吗?

不,if - else if 的列表与在机器代码级别使用 switch 完全相同,因为 if - else if 具有性质

if(integer == 1) 
{ ... }  
else if (integer == 2) 
{ ... }

其中 1 和 2 是任何类型的编译时整数常量。在这种情况下,它将产生 100% 等效的机器代码

switch(integer)
{
  case 1: ... break;
  case 2: ... break;
}

在这种情况下还有其他优化代码的选项吗?

一些事情:

  • 如果开关使用的常量是相邻的,最好是从 0 到 n,整个开关可以用函数指针跳转表代替。现代编译器应该在幕后做到这一点,但较旧的编译器可能会遇到困难。
  • 这里需要浮点数并不明显,因为您将结果转换为 int。用整数计算替换浮点计算可能会大大提高性能,尤其是在缺乏 FPU 的微控制器系统等上。如果浮点数的唯一目的是四舍五入,那么考虑一下

    // probably slow
    int32_t a = (int32_t)((((double)cmd_data * CMD_WR_ACC_PARA1) / CMD_WR_ACC_PARA2) + 0.5);
    

    等价于

    // probably faster
    int32_t b = ( (cmd_data * CMD_WR_ACC_PARA1) + CMD_WR_ACC_PARA2/2 ) / CMD_WR_ACC_PARA2;
    

    (见Rounding integer division (instead of truncating)

【讨论】:

  • 事实上,if / else / if 不一定编译为与 switch 相同的东西。与使用 if / else / if 块相比,编译器更有可能编译 switch 以使用跳转表。由于替代方法是依次测试每个条件,switch 有时可能会产生不同且更高效的代码。
  • @JohnBollinger 我澄清了编辑的意思。
【解决方案3】:

这甚至不需要 case 语句,因为它很容易向量化。最简单的方法之一是使用 X 宏。

由于没有提供任何值,我只是使用顺序整数作为占位符

#define XMACRO(X,...) \
  /*Label, Parameter1, Parameter2,...*/ \
  X(ACC, 1.0, 2.0, __VA_ARGS__) \
  X(BIAS, 3.0, 4.0, __VA_ARGS__) \
  X(SUP, 5.0, 6.0, __VA_ARGS__) \
  X(FILL, 7,0, 8.0, __VA_ARGS__) \
  //more entries here

#define AS_ENUM(label,...) CMD_WR_##label,
enum commands { XMACRO(AS_ENUM) CMD_WR_COUNT };

#define AS_PREMULTIPLIED(label, p1, p2,...) ((p1)/(p2)),
const double convert_multipliers[CMD_WR_COUNT] = { XMACRO(AS_PREMULTIPLIED) };

int convert_data(int cmd_data, unsigned cmd_no){
return (cmd_no >= CMD_WR_COUNT) 
  ? cmd_data
  : (int) (cmd_data * convert_multipliers[cmd_no] + 0.5);
}

编辑: 实际上,如果经常调用代码,switch 语句可能会更优化,但不是在紧密循环中,因为大多数编译器都可以生成跳转表。如果启用了正确的选项(例如 -Ofast 和 gcc),其他参数甚至可以编译为单个常量。幸运的是,X 宏也可以处理您的案例陈述:

#define AS_CASE(label, p1, p2,...) case CMD_WR_##label : \
  return (int) (cmd_data * ((p1)/(p2)) + 0.5);

switch(cmd_no){
  XMACRO(AS_CASE)
  default: return cmd_data;
}

确定其性能是否已优化的唯一方法是测量,但 X-macros 将有助于消除重复代码,并允许您将数据以易于阅读和修改的表格格式保存在一起(并且至少看起来已优化) .当你必须在路上添加另一个参数时,你只需要添加一行而不是遍历每个函数。

【讨论】:

    【解决方案4】:

    将代码从 switch 重写为 if/then/else 不会有太大的改进。首先要尝试的是将优化选项设置为最高进行编译。让我假设的编译器 gcc 为您优化它。这就是通常容易实现的目标。

    这有什么帮助? 默认情况下,GCC 平衡了代码速度优化与代码大小和调试便利性。默认情况下,gcc 生成的代码很容易调试,因为代码流更接近源代码,并且存储了变量(这很慢),但这使得使用 gdb 和断点更容易调试,因为变量在堆栈上很容易看到.

    当我们打开优化时,会进行更多优化,包括仅将一些局部变量存储在处理器寄存器中(速度很快),以及通过重新排序代码来优化通过 cpu 执行管道的代码流。缺点是单步执行机器代码时更难遵循。编译也需要稍长一些的时间。 但这是值得的!

    要将优化标志设置为最高进行编译,请尝试使用 -O3,它会针对代码大小和执行时间设置更多优化:

    -O3
    

    这里有一个关于优化标志的很好的讨论: https://stackoverflow.com/a/32941238/6693299

    使用优化标志集编译后,查看使用昂贵浮点运算的数学。你需要它们吗?双重乘法和除法很昂贵,只是为了将它们转换为int32s?您确定需要这样做吗?您可以将数学转换为仅使用整数运算吗?

    你能分享列出的常量的值吗?您也许可以将部分或全部数学转换为查找表。

    【讨论】:

    • 我想说重写使用if/then/else 而不是switch 将无济于事根本。没有理由认为编译器会以这种方式生成更小或更快的代码,并且生成的源代码的可读性会降低(对我而言)。
    • 我同意@JohnBollinger。我说了同样的话。让编译器进行优化。这就是我的建议。那,看看是否可以使用整数数学或查找表来消除双浮点。
    【解决方案5】:

    您可以将公共代码重新组合在一起,并在编译或初始化时而不是在执行时进行划分。

    我假设 CMD_WR_* 是增量(枚举的元素)。

    这里你先做除法:

    double coef[] = {CMD_WR_ACC_PARA1/CMD_WR_ACC_PARA2, CMD_WR_BIAS_PARA1/CMD_WR_BIAS_PARA2, CMD_WR_SUP_PARA1/CMD_WR_SUP_PARA2, CMD_WR_FIL_PARA1/CMD_WR_FIL_PARA2, etc};
    

    然后你用 if/else 进行计算(如果很少的话):

    if (cmd_no >= CMD_WR_ACC && cmd_no <= CMD_WR_FIL) {
       converted_command_data = (INT32)((DOUBLE)cmd_data * coef[cmd_no-CMD_WR_ACC]) + 0.5;
    } else {
        converted_command_data = cmd_data;
    }
    

    如果它们很多,则使用开关:

    switch (cmd_no) {
      case CMD_WR_ACC:
      case CMD_WR_BIAS:
      case CMD_WR_SUP:
      case CMD_WR_FIL:
         converted_command_data = (INT32)((DOUBLE)cmd_data * coef[cmd_no-CMD_WR_ACC]) + 0.5;
         break;
      [...]
      default:
        converted_command_data = cmd_data;
        break;
    }
    

    (我认为这在性能方面没有显着差异)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2023-04-10
      • 2010-10-30
      • 2016-05-13
      • 2016-02-21
      • 1970-01-01
      • 2012-09-08
      • 2013-10-14
      • 2015-05-17
      相关资源
      最近更新 更多