处理1-256字节的函数最佳实践

9
我有一些处理1-256字节的函数,运行在嵌入式C平台上,在这个平台上传递一个字节比传递一个int更快更紧凑(一个指令与三个指令),那么编码的首选方式是什么:
  1. 接受一个int,如果为零,则提前退出,否则将计数值的LSB复制到无符号字符中,并在do {} while (--count)循环中使用它(参数值为256将转换为0,但将运行256次)。
  2. 接受一个无符号字符,如果为零,则提前退出,并为256字节拥有特殊版本的函数(这些情况将事先知道)。
  3. 接受一个无符号字符,如果为零,则运行256次。
  4. 有一个类似上述函数的函数,但通过包装函数调用它,这些函数的行为为(0-255)和(仅256)。
  5. 有一个类似上述函数的函数,但通过包装器宏调用它,这些宏的行为为(0-255)和(仅256)。
当系统繁忙时,预计该函数的内部循环可能会占处理器执行时间的15%-30%; 它有时用于少量字节,有时用于大量字节。 该函数使用的存储芯片具有每个事务的开销,并且我更喜欢我的存储器访问函数在内部执行开始事务/执行工作/结束事务序列。
最有效的代码是简单地接受一个无符号字符,并将参数值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' 指针而不是两字节指针,并且有自己关于内存分配的想法。也许我应该更多地看看 PICC-18pro,但当我尝试在 PICC-18pro 的评估版本上编译我的项目时,它没有工作,我也没有弄清楚为什么——也许与变量布局不符合我的汇编程序有关——我只是继续使用 PICC-18std。
顺便说一句,我刚刚发现 PICC-18 特别喜欢 do {} while(--bytevar); 而特别不喜欢 do {} while(--intvar); 我想知道编译器在生成后者时正在思考什么?
编译器加载变量的指针,甚至不使用 LFSR 指令(需要两个字),而是使用 MOVLW/MOVWF 的组合(需要四个字)。然后它使用这个指针来执行递减和比较。虽然我承认 do{}while(--wordvar); 不能产生像 do{}while(wordvar--); 那样好的代码,但代码比后者实际生成的要好。单独执行递减和 while 测试(例如 while (--lpw,lpw))会产生合理的代码,但它看起来有些丑陋。后置递减运算符可以为倒计时循环产生最佳代码:
  decf _lpw
  btfss _STATUS,0 ; 如果进位标志位为0(即不是零),跳过下一条指令
   decf _lpw+1
  bc    loop  ; 只有当lpw为零时,进位标志位才会清除
但它生成的代码比--lpw还要糟糕。最好的代码应该是一个向上计数的循环:
infsnz _lpw incfsz _lpw+1 bra loop 但编译器没有生成这样的代码。 编辑2 我可能会采用另一种方法:为字节数分配一个全局的16位变量,并编写函数,使得在退出前计数器总是归零。然后,如果只需要8位值,只需要加载8位即可。我会使用宏来进行调整,以获得最佳效率。在PIC上,对于已知为零的变量使用| =永远不会比使用=慢,并且有时会更快。例如,intvar | = 15或intvar | = 0x300将是两个指令(每种情况只需处理一个字节的结果,可以忽略其他字节);intvar | = 4(或任何2的幂)是一条指令。显然,在某些其他处理器上,intvar = 0x300将比intvar | = 0x300更快;如果我使用宏,可以根据需要进行调整。

2
如果要读取的数据量存储在一个字节中,并且需要能够读取256个字节,那么将0解释为256是合理的。在这种情况下,“读取0个字节”实际上会读取256个字节,这些字节可能还没有准备好被读取,可能会严重破坏事情。 - cHao
1
@Dummy00001,PIC18是一种8位处理器;RAM通常只有几K字节,如果有的话。 - Doug Currie
一个无符号字符(unsigned char)仅能接受从0到255(0xFF)的值。你永远不可能在无符号字符中得到256的值! - Zardoz89
1
@Zardoz89:没错,但是一个 do {...} while(--byteVar); 循环(或者等效的汇编代码)如果传入零值将会循环256次。在20世纪80年代的计算机上,可能有很多程序都是写成当循环计数为零时请求256次循环,而不是跳过整个循环。认为0代表比最大可通过值“更大”的想法确实有点奇怪,但这与一些操作系统延迟例程并不矛盾,它们接受无符号超时值,但使用0作为“无限期等待事件”的特殊情况。 - supercat
在特定的情况下,硬件可以使用单个命令有用地编程1-256个字节。编程零是无用的,尝试编程超过256个字节将导致缓冲区溢出。 - supercat
显示剩余2条评论
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))

如果在您的编译器中更快,请使用第二种方法。


你可以将其包装在宏中,以便导出的接口是合理的。 - nmichaels

0

如果一个int参数需要3个指令,而一个char参数只需要1个指令,你可以传递一个额外的char参数来弥补你缺失的1个比特位。看起来很傻,因为你的(可能是16位)int比8位的char需要两倍多的指令。


1
编译器将允许一个char参数通过W寄存器传递;所有其他参数都有RAM位置分配给它们。在许多情况下,如果编译器可以在W中传递一个字参数的LSB,那将是有利的,但它没有这样做。实际上,如果我设计一个编译器,我可能会指定每个不接受char但需要word的函数都有两个入口点;一个入口点将W存储到LSB,然后跳转到另一个入口点。其他例程可以调用最有利的那个。 - supercat
1
真是疯狂。我以为PIC应该是RISCy的。哦,算了。 - nmichaels

0

就我个人而言,我会选择选项1的某个变体。该函数的接口保持合理、直观,并且似乎不太可能被错误调用(如果传入一个大于256的值,您可能需要考虑要做什么——只在调试构建中使用断言可能是适当的)。

我认为使用8位计数器循环正确次数的微小“hack”/微优化实际上不会成为维护问题,而且您似乎已经进行了相当多的分析来证明它的合理性。

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

然而,我会反对公共接口要求调用者传入比他们想读取的少一个值的情况。


@Micnael Burr:提供实际要处理的字节数,无论是多少,都“感觉”正确,尽管我讨厌浪费代码空间。当然,与传递n-1相比,它有更好的“感觉”。处理任意数量的字节可能并不是不合理的,即使我不希望使用大于256字节的缓冲区。一些例程(如空白检查范围)可能会用于大于256字节的计数,如果内部循环仅使用8位变量,则接受更大变量的成本不会太高。我将在上面发布一些进一步的想法。 - supercat

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