【问题标题】:Optimising this C (AVR) code优化此 C (AVR) 代码
【发布时间】:2011-04-13 07:52:42
【问题描述】:

我有一个中断处理程序,它运行得不够快,无法完成我想做的事情。基本上,我使用它通过将查找表中的值输出到 AVR 微控制器上的端口来生成正弦波,但不幸的是,这发生得不够快,无法获得我想要的波形频率。有人告诉我,我应该考虑在汇编中实现它,因为编译器生成的汇编可能效率稍低,并且可能能够进行优化,但是在查看汇编代码之后,我真的看不出我可以做得更好。

这是 C 代码:

const uint8_t amplitudes60[60] = {127, 140, 153, 166, 176, 191, 202, 212, 221, 230, 237, 243, 248, 251, 253, 254, 253, 251, 248, 243, 237, 230, 221, 212, 202, 191, 179, 166, 153, 140, 127, 114, 101, 88, 75, 63, 52, 42, 33, 24, 17, 11, 6, 3, 1, 0, 1, 3, 6, 11, 17, 24, 33, 42, 52, 63, 75, 88, 101, 114};
const uint8_t amplitudes13[13] = {127, 176,  221, 248,  202, 153, 101, 52, 17,  1, 6,  33,  75};
const uint8_t amplitudes10[10] = {127, 176,   248,  202, 101, 52, 17,  1,  33,  75};

volatile uint8_t numOfAmps = 60;
volatile uint8_t *amplitudes = amplitudes60;
volatile uint8_t amplitudePlace = 0; 

ISR(TIMER1_COMPA_vect) 
{
    PORTD = amplitudes[amplitudePlace];

    amplitudePlace++; 

    if(amplitudePlace == numOfAmps)
    {
        amplitudePlace = 0;
    }

}

amplitudes 和 numOfAmps 都被另一个运行比这个慢得多的中断程序改变(它基本上是为了改变正在播放的频率而运行的)。归根结底,我不会使用那些确切的数组,但它会是一个非常相似的设置。我很可能有一个包含 60 个值的数组,而另一个只有 30 个值的数组。这是因为我正在构建一个频率扫描器,并且在较低频率下我可以提供更多样本,因为我有更多的时钟周期可以玩但是在更高的频率上,我的时间非常紧迫。

我确实意识到我可以让它以较低的采样率工作,但我不想每个周期的样本数低于 30 个。我不认为拥有指向数组的指针会使它变得更慢,因为从数组中获取值的程序集和从指向数组的指针中获取值的程序集似乎相同(这是有道理的)。

在我必须产生的最高频率下,我被告知我应该能够让它在每个正弦波周期处理大约 30 个样本。目前有 30 个样本,它运行的最快速度大约是所需最大频率的一半,我认为这意味着我的中断需要以两倍的速度运行。

因此,模拟时那里的代码需要 65 个周期才能完成。再一次,我被告知我最多应该能够将其减少到大约 30 个周期。

这是生成的 ASM 代码,我想到了它旁边的每一行的作用:

ISR(TIMER1_COMPA_vect) 
{
push    r1
push    r0
in      r0, 0x3f        ; save status reg
push    r0
eor     r1, r1      ; generates a 0 in r1, used much later
push    r24
push    r25
push    r30
push    r31         ; all regs saved


PORTD = amplitudes[amplitudePlace];
lds     r24, 0x00C8     ; r24 <- amplitudePlace I’m pretty sure
lds     r30, 0x00B4 ; these two lines load in the address of the 
lds     r31, 0x00B5 ; array which would explain why it’d a 16 bit number
                    ; if the atmega8 uses 16 bit addresses


add     r30, r24            ; aha, this must be getting the ADDRESS OF THE element 
adc     r31, r1             ; at amplitudePlace in the array.  

ld      r24, Z              ; Z low is r30, makes sense. I think this is loading
                            ; the memory located at the address in r30/r31 and
                            ; putting it into r24

out     0x12, r24           ; fairly sure this is putting the amplitude into PORTD

amplitudePlace++; 
lds     r24, 0x011C     ; r24 <- amplitudePlace
subi    r24, 0xFF       ; subi is subtract imediate.. 0xFF = 255 so I’m
                        ; thinking with an 8 bit value x, x+1 = x - 255;
                        ; I might just trust that the compiler knows what it’s 
                        ; doing here rather than try to change it to an ADDI 

sts     0x011C, r24     ; puts the new value back to the address of the
                        ; variable

if(amplitudePlace == numOfAmps)
lds     r25, 0x00C8 ; r24 <- amplitudePlace
lds     r24, 0x00B3 ; r25 <- numOfAmps 

cp      r24, r24        ; compares them 
brne    .+4             ; 0xdc <__vector_6+0x54>
        {
                amplitudePlace = 0;
                    sts     0x011C, r1 ; oh, this is why r1 was set to 0 earlier
        }


}

pop     r31             ; restores the registers
pop     r30
pop     r25
pop     r24
pop     r19
pop     r18
pop     r0
out     0x3f, r0        ; 63
pop     r0
pop     r1
reti

除了可能在中断中使用更少的寄存器以减少推送/弹出次数之外,我真的看不出这个汇编代码效率低下的地方。

我唯一的另一个想法是,如果我能弄清楚如何在 C 中获得一个 n 位 int 数据类型,以便数字在到达末尾时会回绕,那么 if 语句可能会被去掉?我的意思是我将有 2^n - 1 个样本,然后让amplitudePlace 变量继续计数,这样当它达到 2^n 时它会溢出并重置为零。

我确实尝试在没有 if 位的情况下完全模拟代码,虽然它确实提高了速度,但它只需要大约 10 个周期,因此一次执行大约需要 55 个周期,不幸的是,它仍然不够快所以我确实需要进一步优化代码,如果不考虑它只有 2 行,这很难考虑!!

我唯一真正的想法是看看我是否可以将静态查找表存储在需要更少时钟周期来访问的地方?它用来访问数组的 LDS 指令我认为都需要 2 个周期,所以我可能不会真正节省太多时间,但在这个阶段我愿意尝试任何事情。

我完全不知道从这里去哪里。我看不出如何让我的 C 代码更有效率,但我对这类事情还很陌生,所以我可能会遗漏一些东西。我很想得到任何帮助。我意识到这是一个非常特殊且涉及问题的问题,通常我会尽量避免在这里提出这些问题,但我已经为此工作了很长时间,并且完全不知所措,所以我真的会寻求任何我能得到的帮助。

【问题讨论】:

  • 我不知道 AVR,所以我想知道我对以下内容有什么误解:为什么r19r18 在 ISR 结束时被弹出而不被推送(或以其他方式使用)?为什么amplitudePlace 有时明显位于地址 0x00c8 有时位于 0x011c?
  • 只是出于好奇:当这个问题得到解答并且您的项目完成后,您介意发布最终的源代码和生成的程序集作为答案吗?
  • @Michael - 这是因为我正在尝试优化 C 代码,并且正在手动将生成的程序集的更改添加到我自己的文档中,在那里我试图弄清楚什么正在发生。但是,是的,你说的是对的,但我认为组装应该是正确的! :)
  • @ndim - 当然!我真的希望我能把它降到 30 个周期,但我们会看看我怎么走哈哈。 :)
  • @Sam:不要忘记,如果你有一个 16MHz AVR,进入和离开 ISR 需要 4 个周期(20MHz 型号为 5 个周期)。

标签: c optimization assembly avr


【解决方案1】:

我可以看到一些开始工作的领域,没有特别的顺序:

1. 减少要推送的寄存器数量,因为每个推送/弹出对需要四个周期。例如,avr-gcc 允许您从其寄存器分配器中删除一些寄存器,因此您可以将它们用于该单个 ISR 中的寄存器变量,并确保它们仍然包含上次的值。如果您的程序从不将r1 设置为除0 之外的任何内容,您也可以摆脱r1eor r1,r1 的推送。

2. 为数组索引的新值使用一个局部临时变量,以将不必要的加载和存储指令保存到该易失性变量。像这样的:

volatile uint8_t amplitudePlace;

ISR() {
    uint8_t place = amplitudePlace;
    [ do all your stuff with place to avoid memory access to amplitudePlace ]
    amplitudePlace = place;
}

3. 从 59 倒数到 0,而不是从 0 到 59,以避免单独的比较指令(与 0 的比较无论如何都会发生在减法中)。伪代码:

     sub  rXX,1
     goto Foo if non-zero
     movi rXX, 59
Foo:

而不是

     add  rXX,1
     compare rXX with 60
     goto Foo if >=
     movi rXX, 0
Foo:

4. 或许使用指针和指针比较(使用预先计算的值!)而不是数组索引。需要检查与倒数哪个更有效。也许将数组对齐到 256 字节边界并仅使用 8 位寄存器作为指针以节省加载和保存地址的高 8 位。 (如果您的 SRAM 用完了,您仍然可以将这 60 字节数组中的 4 个内容放入一个 256 字节数组中,并且仍然可以获得由 8 个恒定高位和 8 个可变低位组成的所有地址的优势。)

uint8_t array[60];
uint8_t *idx = array; /* shortcut for &array[0] */
const uint8_t *max_idx = &array[59];

ISR() {
    PORTFOO = *idx;
    ++idx;
    if (idx > max_idx) {
        idx = array;
    }
}

问题是指针是 16 位,而您的简单数组索引以前是 8 位大小。如果您将数组地址设计为使地址的高 8 位是常量(在汇编代码中为hi8(array)),并且您只处理 ISR 中实际发生变化的低 8 位,那么帮助解决这个问题可能是一个技巧。不过,这确实意味着编写汇编代码。从上面生成的汇编代码可能是在汇编中编写该版本的 ISR 的一个很好的起点。

5.如果从时序的角度来看可行,请将样本缓冲区大小调整为 2 的幂,以用简单的 i = (i+1) &amp; ((1 &lt;&lt; POWER)-1); 替换 if-reset-to-zero 部分。如果您想采用 4. 中提出的 8 位/8 位地址拆分,甚至可能采用 256 的 2 次幂(并根据需要复制样本数据以填充 256 字节buffer) 甚至会在 ADD 之后为您保存 AND 指令。

6. 如果 ISR 只使用不影响状态寄存器的指令,请停止推送和弹出SREG

常规

以下内容可能会派上用场,尤其是对于手动检查所有其他汇编代码的假设:

firmware-%.lss: firmware-%.elf
        $(OBJDUMP) -h -S $< > $@

这会生成整个固件映像的注释完整汇编语言列表。您可以使用它来验证注册(非)使用情况。请注意,启动代码仅在您首次启用中断之前运行一次,不会干扰您的 ISR 以后对寄存器的独占使用。

如果您决定不直接在汇编代码中编写该 ISR,我建议您编写 C 代码并在每次编译后检查生成的汇编代码,以便立即观察您的更改最终会生成什么。

您最终可能会用 C 和汇编语言编写十几个 ISR 变体,将每个变体的周期相加,然后选择最好的一个。

注意 在不进行任何寄存器保留的情况下,ISR 最终得到了大约 31 个周期(不包括进入和离开,这又增加了 8 或 10 个周期)。完全摆脱寄存器推送将使 ISR 减少到 15 个周期。更改为 256 字节的恒定大小的样本缓冲区并让 ISR 独占使用四个寄存器,可以将 ISR 中花费的周期减少到 6 个(加上 8 或 10 个用于进入/离开)。

【讨论】:

  • 您不必纠结于要推送哪些寄存器或编写递减计数循环。任何半体面的编译器都应该能够为您做到这一点。对我来说,解决方案是增加编译器优化设置,如果这没有帮助,那就换一个新的编译器。
  • 在一个拥有大量优秀编译器的理想世界中,我会同意。然而,在现实世界中,新的编译器会出现一系列不同的问题,您需要解决这些问题、价格过高或其他问题。这种 C 级源代码优化可以是同时获得 C 和汇编的最佳性能的好方法,而无需直接编写汇编源代码。由于 AVR 项目的典型代码大小有限,因此 C 源代码级别非常低,仍然可以管理。
  • 1) 是的.. 好主意!如何检查我的程序的其余部分是否使用 r1(或 rN),以便避免推送/弹出它?如果我保留一个寄存器,我如何确保(例如)一个库函数不会尝试使用我保留的寄存器? 2)所以我认为本地临时变量是指仅存在于 ISR 中的变量?它不需要是全局的,所以我可以记住下次调用 ISR 时我在数组中的位置吗? 3)我可能会遗漏一些东西,但如果我这样做了,我是否只需要检查它何时等于 0,然后将其放回 59?
  • 4) 哈哈,大部分内容都在我脑海中浮现……你能推荐任何地方让我读到这个吗?我知道指针是什么,但还没有听说过指针比较。不过感谢您的提示,非常有用!
  • @ndim 但是编译器确实应该能够从那些纤细的 C 代码中做一些更好的事情,尤其是您指出的寄存器推送/弹出。虽然我没有与 Atmel 合作过,但它并不是一个稀有的平台,一定有很多编译器可供选择。
【解决方案2】:

我想说最好的办法是用纯汇编程序编写您的 ISR。这是非常简短的代码,您可以使用现有的反汇编程序来指导您。但是对于这种性质的事情,您应该能够做得更好:例如使用更少的寄存器,以节省 pushpop;重构它,使其不会从内存中加载 amplitudePlace 三个不同的时间,等等。

【讨论】:

  • 啊,真不敢相信我什至没有意识到我可以将amplitudePlace 变量保存在同一个寄存器中......这是一个很好的提示,谢谢。希望我能够减少它使用的寄存器数量。我摆脱的每个寄存器都节省了我刚刚意识到的 4 个时钟周期。 :)
【解决方案3】:

您必须与程序的其余部分共享所有这些变量吗?由于您共享的每个此类变量都必须是易失的,因此不允许编译器对其进行优化。至少amplitudePlace看起来可以改成局部静态变量,然后编译器或许可以进一步优化。

【讨论】:

  • 再说一次,我不太确定如何将其设为局部变量。它是否必须是全局变量,以便我记得下次 ISR 时我的目标位置叫?还是我想错了?你是对的,虽然在那个amplitudePlace中只需要被这个特定的ISR读取/修改,但是我们强调从ISR修改的任何变量都应该设置为volatile,这样编译器就不会假设它知道它会优化时保持不变。从你所说的来看,这似乎过于简单化了?
  • @Sam:它需要有静态存储持续时间(这是全局变量的唯一选项),但它不需要全局可见。您可以通过将其放入 ISR 并使用 static 关键字,使其成为具有静态存储持续时间的局部变量(意味着它在一次迭代到下一次迭代中保持其值)。如果它被 ISR 使用,它也不需要是volatile。该要求仅适用于 ISR 程序其他部分使用的变量。
  • 在 ISR 中将其声明为静态并在模块级别声明将具有相同的性能。摆脱 volatile 会有所帮助,同时仍允许它在 ISR 之外进行更新 - 假设它是机器字长或在更新期间中断关闭。
【解决方案4】:

为了澄清,你的中断应该是这样的:

ISR(TIMER1_COMPA_vect) 
{
    PORTD = amplitudes[amplitudePlace++];
    amplitudePlace &= 63;
}

这将要求您的表格长度为 64 个条目。如果您可以选择表的地址,则可以使用单个指针,将其递增,然后使用 0xffBf 进行 & 处理。

如果使用变量而不是固定常量会减慢速度,您可以将指针变量替换为特定数组:

PORTD = amplitudes13[amplitudePlace++];

然后更改中断指针以对每个波形使用不同的函数。这不太可能节省大量资金,但我们总共减少了 10 个周期。

至于寄存器使用的东西。一旦你得到一个像这样非常简单的 ISR,你可以检查 ISR 的序言和结语,它们会推送和弹出处理器状态。如果您的 ISR 仅使用 1 个寄存器,您可以在汇编程序中执行此操作,并且只保存和恢复该一个寄存器。这将减少中断开销而不影响程序的其余部分。一些编译器可能会为您执行此操作,但我对此表示怀疑。

如果有时间和空间,您还可以创建一个长表并将 ++ 替换为 +=freq,其中 freq 将导致波形为基频的整数倍(2x、3x、4x 等... ) 通过跳过那么多样本。

【讨论】:

  • 选择你的表的地址 这样(table &amp; 64) == 0你可以只用一个指针,递增它,@ 987654324@它与 ~64
  • 另请注意,更改 AVR 上的中断指针并非易事,因为它需要您更改 Flash 中的程序存储器,即您需要处理 Flash 页擦除等问题。将新波形的数据复制到单个 SRAM 波形采样缓冲区中也不是微不足道的 - 但是您可以将新波形保存在程序存储器中,即闪存中,它比 SRAM 更广泛!
  • avr-gcc 仅推送其生成的代码实际破坏的寄存器。因此,通过手动编写汇编来节省 push/pop 指令的任何节省都必须是在更有效的注册代码中,而不仅仅是删除一堆无用的 push/pop 指令。
  • 我喜欢i = (i+1) &amp; ~64; 的想法,以及跳过样本的想法。只有作为 2 次方的 freq 值(对于 i = (i+freq) &amp; ~64)才能避免舍入误差,这将在某些频率/定时器组合中变得明显。
  • 当然,freq &lt;= 64 必须持有才能使 &amp; ~64 技巧发挥作用。
【解决方案5】:

您是否考虑过解决问题并以固定中断频率以可变速率步进,而不是一次以不同的中断频率逐个遍历表?这样 ISR 本身会更重,但您可以负担得起以较低的速度运行它。此外,通过一点点定点算法,您可以轻松生成更广泛的频率谱,而不会弄乱多个表格。

无论如何,有一百零一种作弊方法可以为此类问题节省周期,如果您可以稍微改变您的要求以适应硬件。例如,您可以将定时器的输出链接到另一个硬件定时器,并使用第二个定时器的计数器作为表索引。您可能会保留全局寄存器或滥用未使用的 I/O 来存储变量。您可以在 COMPA 中断中一次查找两个条目(或插值),并在其间设置一个微小的第二个 COMPB 中断以发出缓冲的条目。以此类推。

只要稍微滥用硬件和精心编写的汇编代码,您应该可以在 15 个周期左右完成这项工作,而不会遇到太多麻烦。你能否让它与系统的其他部分配合得很好是另一个问题。

【讨论】:

    【解决方案6】:

    也许通过使用算术表达式将条件和比较全部去掉就足够了:

    ISR(TIMER1_COMPA_vect) 
    {
            PORTD = amplitudes[amplitudePlace];
    
            amplitudePlace = (amplitudePlace + 1) % numOfAmps;
    }
    

    如果您的 CPU 以合理的速度执行模运算,这应该会快得多。如果还是不行,试试用汇编写这个版本。

    【讨论】:

    • AVR 没有除法运算。
    • 啊,这可能会为我节省几个时钟周期——谢谢!我想我需要检查添加和模数是否比比较和分支更快。我有一种感觉,它们会非常相似,但我想值得一试。 :)
    • 通过添加模运算符来优化代码通常不是一个好主意。在大多数 CPU 上,除法非常缓慢。
    • 啊,好的。有没有办法通过某种位掩码操作来做到这一点,以便它在达到某个值时重置但不会改变?我在想如果是这样的话,这可能是一种方法。
    • @Sam:如果你的长度都是 2 的幂,那么可以。您可以应用位掩码而不是进行比较。
    猜你喜欢
    • 1970-01-01
    • 2011-11-24
    • 2020-09-02
    • 1970-01-01
    • 1970-01-01
    • 2012-05-23
    • 2018-08-04
    • 2011-04-09
    • 2012-11-13
    相关资源
    最近更新 更多