3

问题

我正在寻找一些关于如何优化将整数的数字打印uint32_t num = 1234567890;到带有 Arduino UNO 的字符显示的输入。要考虑的主要指标是内存使用量和编译大小。显示速度太慢了,没有任何提高速度的意义,最小代码长度虽然不错,但不是必需的。

目前,我正在使用提取最低有效数字num%10,然后删除该数字num/10,依此类推,直到num提取所有数字。使用递归,我可以颠倒打印的顺序,因此只需要很少的操作(作为明确的代码行)就可以按正确的顺序打印数字。使用for循环我需要找到用于写入数字的字符数,然后存储它们,然后才能以正确的顺序打印它们,需要一个数组和 3 个for循环。

根据 Arduino IDE,当打印各种有符号和无符号整数时,递归使用2010/33字节的存储/内存,而使用扩展类的库时,迭代使用2200/33字节和 2474/52字节。Adafruit_CharacterOLEDPrint

有没有办法比我在下面使用递归和迭代编写的函数更好地实现这一点?如果不是,你更喜欢哪一个,为什么? 我觉得可能有更好的方法可以用更少的资源做到这一点——但也许我是堂吉诃德与风车搏斗,代码已经足够好了。

背景

我正在使用 NHD-0420DZW 字符 OLED 显示器,并使用 Newhaven 数据表和 LiquidCrystal 库作为编写我自己的库的指南,并且显示器运行良好。然而,为了尽量减少代码膨胀,我选择不让我的显示库成为 的子类Print,它是 Arduino 核心库的一部分。在此过程中,已经实现了存储空间(~400 字节)和内存(~19 字节)的显着节省(ATmega328P 具有 32k 存储空间和 2k RAM,因此资源稀缺)。


递归

如果我使用递归,打印方法相当优雅。该数字除以 10,直到达到基本情况为零。然后打印最小数字的最低有效位(num的MSD),下一个最小数字的LSD(num的第二个MSD)等等,导致最终的打印顺序颠倒。%10这更正了使用和/10操作的数字提取的相反顺序。

// print integer type literals to display (base-10 representation) 
void NewhavenDZW::print(int8_t num) {print(static_cast<int32_t>(num));}
void NewhavenDZW::print(uint8_t num) {print(static_cast<uint32_t>(num));}
void NewhavenDZW::print(int16_t num) {print(static_cast<int32_t>(num));}
void NewhavenDZW::print(uint16_t num) {print(static_cast<uint32_t>(num));}
void NewhavenDZW::print(int32_t num) {
    if(num < 0) {         // print negative sign if present
        send('-', HIGH);  // and make num positive
        print(static_cast<uint32_t>(-num));
    } else
        print(static_cast<uint32_t>(num));
}
void NewhavenDZW::print(uint32_t num) {
    if(num < 10) {  // print single digit numbers directly
        send(num + '0', HIGH);
        return;
    } else                    // use recursion to print nums with more
        recursivePrint(num);  // than two digits in the correct order
}
// recursive method for printing a number "backwards"
// used to correct the reversed order of digit extraction
void NewhavenDZW::recursivePrint(uint32_t num) {
    if(num) {  // true if num>0, false if num==0
        recursivePrint(num/10);    // maximum of 11 recursive steps
        send(num%10 + '0', HIGH);  // for a 10 digit number
    }
}

迭代

由于数字提取方法从 LSD 而不是 MSD 开始,所以提取的数字不能直接打印,除非我移动光标并告诉显示器从右到左打印。所以我必须在提取数字时存储它们,然后才能以正确的顺序将它们写入显示器。

void NewhavenDZW::print(uint32_t num) {
    if(num < 10) {
        send(num + '0', HIGH);
        return;
    }
    uint8_t length = 0;
    for(uint32_t i=num; i>0; i/=10)  // determine number of characters 
        ++length;                    // needed to represent number
    char text[length];
    for(uint8_t i=length; num>0; num/=10, --i)
        text[i-1] = num%10 + '0';    // map each numerical digit to 
    for(uint8_t i=0; i<length; i++)  // its char value and fix ordering
        send(text[i], HIGH);         // before printing result
}

更新

最终,递归占用最少的存储空间,但可能使用最多的内存。

在查看了 Igor G 和 darune 提供的代码,以及查看 Godbolt 上列出的指令数量(由 darune 和 old_timer 讨论)后,我相信 Igor G 的解决方案是最好的整体。在测试期间,darune 的函数(使用语句停止前导零并能够打印)编译为2076字节与2096字节。当附加必要的语句时,它还需要比 darune 的 (273) 更少的指令 (88) 。if0if

使用指针变量

void NewhavenDZW::print(uint32_t num) {
    char buffer[10];
    char* p = buffer;
    do {
        *p++ = num%10 + '0';
        num /= 10;
    } while (num);
    while (p != buffer)
        send(*--p, HIGH);
}

使用索引变量

这就是我最初的for循环试图做的,但是以一种天真的方式。正如 Igor G 所指出的那样,试图最小化缓冲区数组的大小确实没有意义。

void NewhavenDZW::print(uint32_t num) {
    char text[10];  // signed/unsigned 32-bit ints are <= 10 digits
    uint8_t i = sizeof(text) - 1;  // set index to end of char array
    do {
        text[i--] = num%10 + '0';  // store each numerical digit as 
        num /= 10;                 // its associated char value
    } while (num);
    while (i < sizeof(text))
        send(text[i++], HIGH);     // print num in the correct order
}

替代方案

这是 darune 的函数,添加了 if 语句,供那些不想筛选评论的人使用。条件pow10 == 100与 相同pow10 == 1,但在具有相同编译大小的情况下保存了循环的两次迭代以打印零。

void NewhavenDZW::print(uint32_t num) {
    for (uint32_t pow10 = 1000000000; pow10 != 0; pow10 /= 10)
        if (num >= pow10 || (num == 0 && pow10 == 100))
            send((num/pow10)%10 + '0', HIGH);
}
4

3 回答 3

2

对于较小的占用空间,您可以使用以下内容:

void Send(unsigned char);

void SmallPrintf(unsigned long val)
{
    static_assert(sizeof(decltype(val)) == 4, "expected '10 digit type'");
   for (unsigned long digit_pow10{1000000000}; digit_pow10 != 0; digit_pow10 /= 10)
    {
        Send((val / digit_pow10 % 10) + '0');
    }
}

这会产生大约 70 条指令——这比使用缓冲区并在之后迭代缓冲区少了大约 14 条指令。(代码也简单多了)

链接到神螺栓

如果不需要前导零,那么 if 子句可以避免这种相当简单的情况 - 例如:

    if (val >= digit_pow10) {
        Send((val / digit_pow10 % 10) + '0');
    }

但是它会花费一些额外的指令(〜9) - 但是总数仍然低于缓冲示例。

于 2019-09-05T10:16:00.683 回答
1

试试这个。Myavr-gcc-5.4.0 + readelf告诉函数体只有 138 个字节。

void Send(uint8_t);

void OptimizedPrintf(uint32_t val)
{
    uint8_t         buffer[sizeof(val) * CHAR_BIT / 3 + 1];
    uint8_t*        p = buffer;
    do
    {
        *p++ = (val % 10) + '0';
        val /= 10;
    } while (val);
    while (p != buffer)
        Send(*--p);
}
于 2019-09-05T06:27:16.377 回答
0

有趣的实验。

unsigned long fun ( unsigned long x )
{
    return(x/10);
}
unsigned long fun2 ( unsigned long x )
{
    return(x%10);
}
int main ( void )
{
   return(0);
}

是否/可以提供 apt-got 工具链:

00000000 <fun>:
   0:   2a e0           ldi r18, 0x0A   ; 10
   2:   30 e0           ldi r19, 0x00   ; 0
   4:   40 e0           ldi r20, 0x00   ; 0
   6:   50 e0           ldi r21, 0x00   ; 0
   8:   0e d0           rcall   .+28        ; 0x26 <__udivmodsi4>
   a:   95 2f           mov r25, r21
   c:   84 2f           mov r24, r20
   e:   73 2f           mov r23, r19
  10:   62 2f           mov r22, r18
  12:   08 95           ret

00000014 <fun2>:
  14:   2a e0           ldi r18, 0x0A   ; 10
  16:   30 e0           ldi r19, 0x00   ; 0
  18:   40 e0           ldi r20, 0x00   ; 0
  1a:   50 e0           ldi r21, 0x00   ; 0
  1c:   04 d0           rcall   .+8         ; 0x26 <__udivmodsi4>
  1e:   08 95           ret

00000020 <main>:
  20:   80 e0           ldi r24, 0x00   ; 0
  22:   90 e0           ldi r25, 0x00   ; 0
  24:   08 95           ret

00000026 <__udivmodsi4>:
  26:   a1 e2           ldi r26, 0x21   ; 33
  28:   1a 2e           mov r1, r26
  2a:   aa 1b           sub r26, r26
  2c:   bb 1b           sub r27, r27
  2e:   ea 2f           mov r30, r26
  30:   fb 2f           mov r31, r27
  32:   0d c0           rjmp    .+26        ; 0x4e <__udivmodsi4_ep>

00000034 <__udivmodsi4_loop>:
  34:   aa 1f           adc r26, r26
  36:   bb 1f           adc r27, r27
  38:   ee 1f           adc r30, r30
  3a:   ff 1f           adc r31, r31
  3c:   a2 17           cp  r26, r18
  3e:   b3 07           cpc r27, r19
  40:   e4 07           cpc r30, r20
  42:   f5 07           cpc r31, r21
  44:   20 f0           brcs    .+8         ; 0x4e <__udivmodsi4_ep>
  46:   a2 1b           sub r26, r18
  48:   b3 0b           sbc r27, r19
  4a:   e4 0b           sbc r30, r20
  4c:   f5 0b           sbc r31, r21

0000004e <__udivmodsi4_ep>:
  4e:   66 1f           adc r22, r22
  50:   77 1f           adc r23, r23
  52:   88 1f           adc r24, r24
  54:   99 1f           adc r25, r25
  56:   1a 94           dec r1
  58:   69 f7           brne    .-38        ; 0x34 <__udivmodsi4_loop>
  5a:   60 95           com r22
  5c:   70 95           com r23
  5e:   80 95           com r24
  60:   90 95           com r25
  62:   26 2f           mov r18, r22
  64:   37 2f           mov r19, r23
  66:   48 2f           mov r20, r24
  68:   59 2f           mov r21, r25
  6a:   6a 2f           mov r22, r26
  6c:   7b 2f           mov r23, r27
  6e:   8e 2f           mov r24, r30
  70:   9f 2f           mov r25, r31
  72:   08 95           ret

回答了我的一个问题,除法功能的 78 条指令。此外,它在一次调用中同时返回分子和分母,如果绝望,可以利用这些东西。

unsigned int fun ( unsigned int x )
{
    return(x/10);
}
unsigned int fun2 ( unsigned int x )
{
    return(x%10);
}
int main ( void )
{
   return(0);
}

00000000 <fun>:
   0:   6a e0           ldi r22, 0x0A   ; 10
   2:   70 e0           ldi r23, 0x00   ; 0
   4:   0a d0           rcall   .+20        ; 0x1a <__udivmodhi4>
   6:   86 2f           mov r24, r22
   8:   97 2f           mov r25, r23
   a:   08 95           ret

0000000c <fun2>:
   c:   6a e0           ldi r22, 0x0A   ; 10
   e:   70 e0           ldi r23, 0x00   ; 0
  10:   04 d0           rcall   .+8         ; 0x1a <__udivmodhi4>
  12:   08 95           ret

00000014 <main>:
  14:   80 e0           ldi r24, 0x00   ; 0
  16:   90 e0           ldi r25, 0x00   ; 0
  18:   08 95           ret

0000001a <__udivmodhi4>:
  1a:   aa 1b           sub r26, r26
  1c:   bb 1b           sub r27, r27
  1e:   51 e1           ldi r21, 0x11   ; 17
  20:   07 c0           rjmp    .+14        ; 0x30 <__udivmodhi4_ep>

00000022 <__udivmodhi4_loop>:
  22:   aa 1f           adc r26, r26
  24:   bb 1f           adc r27, r27
  26:   a6 17           cp  r26, r22
  28:   b7 07           cpc r27, r23
  2a:   10 f0           brcs    .+4         ; 0x30 <__udivmodhi4_ep>
  2c:   a6 1b           sub r26, r22
  2e:   b7 0b           sbc r27, r23

00000030 <__udivmodhi4_ep>:
  30:   88 1f           adc r24, r24
  32:   99 1f           adc r25, r25
  34:   5a 95           dec r21
  36:   a9 f7           brne    .-22        ; 0x22 <__udivmodhi4_loop>
  38:   80 95           com r24
  3a:   90 95           com r25
  3c:   68 2f           mov r22, r24
  3e:   79 2f           mov r23, r25
  40:   8a 2f           mov r24, r26
  42:   9b 2f           mov r25, r27
  44:   08 95           ret

22 行,44 个字节用于除法。也很有趣。可以在 C++ 级别利用以节省空间(如果此显示循环是您进行除法/取模的唯一地方)。

当然,优化器知道库函数同时执行结果和余数:

unsigned long fun ( unsigned long x )
{
    unsigned long res;
    unsigned long rem;

    res=x/10;
    rem=x&10;

    res&=0xFFFF;
    rem&=0xFFFF;
    return((res<<16)|rem);
}
int main ( void )
{
   return(0);
}


00000000 <fun>:
   0:   cf 92           push    r12
   2:   df 92           push    r13
   4:   ef 92           push    r14
   6:   ff 92           push    r15
   8:   c6 2e           mov r12, r22
   a:   d7 2e           mov r13, r23
   c:   e8 2e           mov r14, r24
   e:   f9 2e           mov r15, r25
  10:   2a e0           ldi r18, 0x0A   ; 10
  12:   30 e0           ldi r19, 0x00   ; 0
  14:   40 e0           ldi r20, 0x00   ; 0
  16:   50 e0           ldi r21, 0x00   ; 0
  18:   19 d0           rcall   .+50        ; 0x4c <__udivmodsi4>
  1a:   a2 2f           mov r26, r18
  1c:   b3 2f           mov r27, r19
  1e:   99 27           eor r25, r25
  20:   88 27           eor r24, r24
  22:   2a e0           ldi r18, 0x0A   ; 10
  24:   c2 22           and r12, r18
  26:   dd 24           eor r13, r13
  28:   ee 24           eor r14, r14
  2a:   ff 24           eor r15, r15
  2c:   68 2f           mov r22, r24
  2e:   79 2f           mov r23, r25
  30:   8a 2f           mov r24, r26
  32:   9b 2f           mov r25, r27
  34:   6c 29           or  r22, r12
  36:   7d 29           or  r23, r13
  38:   8e 29           or  r24, r14
  3a:   9f 29           or  r25, r15
  3c:   ff 90           pop r15
  3e:   ef 90           pop r14
  40:   df 90           pop r13
  42:   cf 90           pop r12
  44:   08 95           ret

00000046 <main>:
  46:   80 e0           ldi r24, 0x00   ; 0
  48:   90 e0           ldi r25, 0x00   ; 0
  4a:   08 95           ret

0000004c <__udivmodsi4>:
  4c:   a1 e2           ldi r26, 0x21   ; 33
  4e:   1a 2e           mov r1, r26
  50:   aa 1b           sub r26, r26
  52:   bb 1b           sub r27, r27
  54:   ea 2f           mov r30, r26
  56:   fb 2f           mov r31, r27
  58:   0d c0           rjmp    .+26        ; 0x74 <__udivmodsi4_ep>

0000005a <__udivmodsi4_loop>:
  5a:   aa 1f           adc r26, r26
  5c:   bb 1f           adc r27, r27
  5e:   ee 1f           adc r30, r30
  60:   ff 1f           adc r31, r31
  62:   a2 17           cp  r26, r18
  64:   b3 07           cpc r27, r19
  66:   e4 07           cpc r30, r20
  68:   f5 07           cpc r31, r21
  6a:   20 f0           brcs    .+8         ; 0x74 <__udivmodsi4_ep>
  6c:   a2 1b           sub r26, r18
  6e:   b3 0b           sbc r27, r19
  70:   e4 0b           sbc r30, r20
  72:   f5 0b           sbc r31, r21

00000074 <__udivmodsi4_ep>:
  74:   66 1f           adc r22, r22
  76:   77 1f           adc r23, r23
  78:   88 1f           adc r24, r24
  7a:   99 1f           adc r25, r25
  7c:   1a 94           dec r1
  7e:   69 f7           brne    .-38        ; 0x5a <__udivmodsi4_loop>
  80:   60 95           com r22
  82:   70 95           com r23
  84:   80 95           com r24
  86:   90 95           com r25
  88:   26 2f           mov r18, r22
  8a:   37 2f           mov r19, r23
  8c:   48 2f           mov r20, r24
  8e:   59 2f           mov r21, r25
  90:   6a 2f           mov r22, r26
  92:   7b 2f           mov r23, r27
  94:   8e 2f           mov r24, r30
  96:   9f 2f           mov r25, r31
  98:   08 95           ret

很抱歉使用这个空间来享受这个练习,看看编译器对这个问题做了什么。尝试使用 16 位除法开始通过保存的 34 条指令来爆炸寄存器使用量。

由于分母是固定的,而且这是一个 8 位处理器,您可以使用编译器玩优化游戏,但您可能会走上一条不可读的道路。

仍然可以肯定的是,这可以在不使用除法库函数的情况下更紧密地完成,并且知道这是一个 AVR。移位虽然很残酷,有很多寄存器,但是一旦溢出,函数的大小也会爆炸。非常细腻。

以一个 uno 的价格,你可以买一把蓝色药丸,里面有更多的东西,包括 32 位寄存器和一个将除以 10 变成几条指令的乘法。仍然可以使用 arduino 沙箱,而且运行速度更快。(更多的闪存,更多的内存,编译器友好的指令集,可能不再需要计算字节数,应该尝试为该目标编译您的项目并查看使用了多少闪存)。

于 2019-09-07T07:28:08.700 回答