9

我有一些旨在处理 1-256 字节的函数,在嵌入式 C 平台上运行,其中传递一个字节比传递一个 int 更快、更紧凑(一条指令对三条指令),它的首选编码方式是什么:

  1. 接受一个 int,如果为零则提前退出,否则将计数值的 LSB 复制到一个无符号字符并在 do {} while(--count); 中使用它 循环(参数值 256 将被转换为 0,但会运行 256 次)
  2. 接受一个无符号字符,如果为零则提前退出,并有一个 256 字节的特殊版本的函数(这些情况将提前知道)。
  3. 接受一个无符号字符,如果为零则运行 256 次。
  4. 有一个像上面这样的函数,但是通过表现为 (0-255) 和 (256 only) 的包装函数调用它。
  5. 具有与上述类似的函数,但通过行为为 (0-255) 和 (256 only) 的包装宏调用它。

预计系统繁忙时,函数的内循环大概占处理器执行时间的15%-30%;它有时用于少量字节,有时用于大字节。该函数使用的内存芯片具有每个事务的开销,我更喜欢让我的内存访问函数在内部执行 start-transaction/do-stuff/end-transaction 序列。

最有效的代码是简单地接受一个 unsigned char 并将参数值 0 视为执行 256 字节的请求,依靠调用者来避免任何意外尝试读取 0 字节。不过,这似乎有点危险。其他人是否在嵌入式系统上处理过此类问题?他们是如何处理的?

编辑 平台是一个PIC18Fxx(128K代码空间;3.5K RAM),连接到一个SPI闪存芯片;当预期更少时读取 256 字节可能会超出 PIC 中的读取缓冲区。写入 256 字节而不是 0 会损坏闪存芯片中的数据。如果不检查忙状态,PIC 的 SPI 端口被限制为每 12 个指令次一个字节;如果这样做会慢一些。典型的写事务除了要接收的数据外,还需要发送 4 个字节;读取需要一个额外的字节用于“SPI 周转”(访问 SPI 端口的最快方法是在发送下一个字节之前读取最后一个字节)。

编译器是 HiTech PICC-18std。

我一般都喜欢 HiTech 的 PICC-16 编译器。HiTech 似乎已经将他们的精力从 PICC-18std 产品转移到了编译时间更慢的 PICC-18pro 系列,似乎需要使用 3 字节“const”指针而不是 2 字节指针,并且有关于内存分配的自己的想法。也许我应该多看看 PICC-18pro,但是当我尝试在 PICC-18pro 的 eval 版本上编译我的项目时,它不起作用,我也没有弄清楚为什么——也许是关于可变布局的一些不同意我的 asm 例程——我一直在使用 PICC-18std。

顺便说一句,我刚刚发现 PICC-18 特别喜欢 do {} while(--bytevar); 尤其不喜欢 do {} while(--intvar); 我想知道编译器在生成后者时的“思维”是什么?

  做
  {
    本地测试++;
    --lpw;
  } 而(lpw);

  第2533章
  第2534章
  2535 0144A8 2AD9 incf fsr2l,f,c
  第2536章
  2537 0144AA 0E00 movlw 低 ?_var_test
  2538 0144AC 6EE9 movwf fsr0l,c
  2539 0144AE 0E01 movlw 高 ?_var_test
  2540 0144B0 6EEA movwf fsr0h,c
  2541 0144B2 06EE decf postinc0,f,c
  2542 0144B4 0E00 移动 0
  2543 0144B6 5AED subwfb postdec0,f,c
  2544 0144B8 50EE movf postinc0,w,c
  2545 0144BA 10ED iorwf postdec0,w,c
  2546 0144BC E1F5 bnz l242

编译器加载一个指向变量的指针,甚至不使用 LFSR 指令(需要两个字),而是使用 MOVLW/MOVWF 的组合(需要四个字)。然后它使用这个指针进行递减和比较。虽然我承认 do{}while(--wordvar); 不能产生像 do{}while(wordvar--); 这样好的代码 该代码比后一种格式实际生成的要好。进行单独的递减和while-test(例如while (--lpw,lpw))会产生合理的代码,但看起来有点难看。后递减运算符可以为向下计数循环产生最佳代码:

  decf _lpw
  btfss _STATUS,0 ; 如果进位则跳过下一个 inst(即不为零)
   decf _lpw+1
  公元前循环; 只有当 lpw 为零时进位才会被清除

但它反而会生成比 --lpw 更糟糕的代码。最好的代码是向上计数循环:

  infsnz _lpw
   incfsz _lpw+1
   文胸环

但编译器不会生成它。

编辑 2 我可能使用的另一种方法:为字节数分配一个全局 16 位变量,并编写函数,使计数器在退出前始终归零。那么如果只需要一个 8 位的值,则只需要加载 8 位。我会使用宏来处理东西,以便可以调整它们以获得最佳效率。在 PIC 上,对已知为零的变量使用 |= 永远不会比使用 = 慢,而且有时更快。例如, intvar |= 15 或 intvar |= 0x300 将是两条指令(每种情况只需要处理结果的一个字节,而可以忽略另一个);intvar |= 4(或 2 的任何幂)是一条指令。显然在其他一些处理器上, intvar = 0x300 会比 intvar |= 0x300; 快 如果我使用宏,它可以适当地调整。

4

3 回答 3

2

你的内部函数应该复制count + 1字节,例如,

 do /* copy one byte */ while(count-- != 0);

如果后递减速度很慢,其他选择是:

 ... /* copy one byte */
 while (count != 0) { /* copy one byte */; count -= 1; }

或者

 for (;;) { /* copy one byte */; if (count == 0) break; count -= 1; }

调用者/包装者可以做:

if (count > 0 && count <= 256) inner((uint8_t)(count-1))

或者

if (((unsigned )(count - 1)) < 256u) inner((uint8_t)(count-1))

如果它在你的编译器中更快。

于 2010-08-19T16:28:32.637 回答
0

如果一个 int 参数需要 3 条指令,而 char 参数需要 1 条指令,那么您可以为缺少的额外 1 位传递一个额外的 char 参数。您的(可能是 16 位)int 需要的指令是 8 位字符的两倍多,这似乎很愚蠢。

于 2010-08-19T17:49:53.193 回答
0

FWIW,我会选择选项#1的一些变体。该函数的界面保持合理、直观,并且似乎不太可能被错误地调用(您可能需要考虑如果传入大于 256 的值要做什么 - 仅调试构建断言可能是合适的)。

我不认为使用 8 位计数器循环正确次数的次要“hack”/微优化真的是一个维护问题,而且您似乎已经做了相当多的分析来证明它的合理性。

如果有人喜欢它们,我不会反对包装器,但我个人会稍微倾向于选项 1。

但是,我反对让公共接口要求调用者传入比他们想要读取的值小一的值。

于 2010-08-19T18:20:13.597 回答