为8位处理器优化一行C代码

3

我正在开发一个8位处理器,并使用C编译器编写代码,现在超过140行的代码只占用了1200字节,但是这一行代码却占用了超过200字节的ROM空间。 eeprom_read() 是一个函数,其中1000、100和10的乘法可能存在问题。

romAddr = eeprom_read(146)*1000 + eeprom_read(147)*100 +
          eeprom_read(148)*10 + eeprom_read(149);

处理器是8位,romAddr的数据类型是int。有没有更优化的方法来写这行代码?

5
你用什么来进行编译?你的编译器可能支持优化空间的优化,这可以防止该语句中 eeprom_read 函数调用的(可能的)内联扩展。 - Joe
这是哪个芯片?你的编译器能生成汇编代码让你阅读吗?你能向我们展示一下吗? - User.1
最简单的方法是 /Os。否则,sh1 的方式也很不错。 - phuclv
4个回答

1

这非常取决于编译器,但我建议您至少以这种方式简化乘法:

romAddr = ((eeprom_read(146)*10 + eeprom_read(147))*10 +
          eeprom_read(148))*10 + eeprom_read(149);

您可以将这个放在一个循环中:
uint8_t i = 146;
romAddr = eeprom_read(i);
for (i = 147; i < 150; i++)
    romAddr = romAddr * 10 + eeprom_read(i);

希望编译器能够认识到将16位值乘以10比分别实现1000和100的乘法要简单得多。但我不完全放心依赖编译器有效地处理循环。也许:
uint8_t hi, lo;
hi = (uint8_t)eeprom_read(146) * (uint8_t)10 + (uint8_t)eeprom_read(147);
lo = (uint8_t)eeprom_read(148) * (uint8_t)10 + (uint8_t)eeprom_read(149);
romAddr = hi * (uint8_t)100 + lo;

所有这些都没有经过测试。

这样只需要乘以10,就可以大大减少代码量。但是在(uint8_t)10中的强制类型转换是不必要的,因为在表达式中,比int小的类型总是会被提升为int。 - phuclv
@LưuVĩnhPhúc 我曾经见过一些需要这个提示才能生成高效代码的8位编译器,但那是几年前的事了。我不知道它们现在是否仍然如此。 - sh1

1
有时乘法可以编译成一系列加法,是的。您可以通过使用左移运算符来优化它。
A*1000 = A*512 + A*256 + A*128 + A*64 + A*32 + A*8

或者说同样的事情:
A<<9 + A<<8 + A<<7 + A<<6 + A<<5 + A<<3

这仍然比单个的“乘法”指令要长得多,但是你的处理器显然也没有它,所以这可能是下一个最好的选择。


1
或者这样写:(A << 10) - (A << 5) + (A << 3) - harold
确实是的。您的表述更好。 - akalenuk
1
这也可以用:((x << 7) - (x + (x << 1))) << 3 ,实际上,你帖子里的那个可能比我给出的两个更好,因为 OP 没有说他使用哪种 CPU,而大多数 8 位 CPU 的移位速度都不快于 1。 - harold
@harold 只有在(A << 10)没有溢出时,(A << 10) - …才能工作。程序员知道使用此表达式在其环境中生成更好的代码是可以接受的,但编译器只能在推断出A在此范围内可行的情况下使用它来替换A*1000 - Pascal Cuoq
1
@PascalCuoq 只要不陷入陷阱,即使溢出也可以正常工作。减法也会溢出(修复问题),或者不会(那么乘法也会溢出)。 - harold
注意:在C++语言中,+ 和 - 的运算符优先级较高,大于 << 和 >> 的运算符。因此,A<<9 + A<<8 + A<<7 + A<<6 + A<<5 + A<<3 的含义是 A<<(9 + A)<<(8 + A)<<(7 + A)<<(6 + A)<<(5 + A)<<3,这不是你的本意。 - phuclv

1
您关心的是空间,而不是时间,对吗?您有四个函数调用,每个函数都传递一个整数参数,然后乘以一个常量,再加上。作为第一次猜测,可能是这样的:
  1. 将整数常量加载到寄存器中(6字节)
  2. 推寄存器(2字节)
  3. 调用eeprom_read(6字节)
  4. 调整堆栈(4字节)
  5. 将整数乘数加载到寄存器中(6字节)
  6. 同时推送两个寄存器(4字节)
  7. 调用乘法程序(6字节)
  8. 调整堆栈(4字节)
  9. 将临时总和加载到寄存器中(6字节)
  10. 将乘法结果添加到该寄存器中(2字节)
  11. 将结果存回到临时总和中(6字节)。
让我们看看,6+2+6+4+6+4+6+4+6+2+6=每次调用eeprom_read约52个字节。最后一个调用会更短,因为它不进行乘法运算。
我建议您尝试使用 (unsigned char)146 而不是像 146 这样的参数来调用 eeprom_read,并且将乘数从 1000 改为 (unsigned short)1000。 这样做可能会使编译器使用更短的指令,可能会使用乘法指令而不是乘法函数调用。 此外,对 eeprom_read 的调用可能被宏定义为直接内存获取,节省了参数推送、函数调用和堆栈调整。
另一个技巧是将四个乘积中的每一个都存储在本地变量中,并在最后将它们全部加起来。这可能会生成更少的代码。 所有这些可能性也会使程序更快,同时更小,虽然您可能不需要关心这一点。
另一种节省空间的可能性是使用循环,如下所示:
static unsigned short powerOf10[] = {1000, 100, 10, 1};
unsigned short i;
romAddr = 0;
for (i = 146; i < 150; i++){
  romAddr += powerOf10[i-146] * eeprom_read(i);
}

这样做可以通过仅一次调用和乘法以及循环指令来节省空间,而不是四份拷贝。

无论如何,请熟悉编译器生成的汇编语言。


1
可能占用最多空间的是乘法的使用。如果您的处理器缺少执行乘法的指令,编译器被迫使用软件逐步执行乘法,这可能需要相当多的代码。
很难说,因为您没有指定目标处理器(或使用的编译器)的任何信息。
一种方法可能是以某种方式尝试减少内联,以便可以重复使用乘以10的代码(在所有四个术语中都使用)。
要知道是否存在这种情况,必须检查机器代码。顺便说一句,对于地址计算使用十进制常量真的很奇怪。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接