arm上延迟循环中每条指令的周期数
Cycles per instruction in delay loop on arm
我正在尝试了解 arm-none-eabi-gcc 为 stm32f103 芯片组生成的一些汇编程序,这似乎 运行 正好是我预期速度的一半。我对汇编程序不是很熟悉,但是因为每个人总是说如果你想了解你的编译器在做什么,请阅读 asm,我看看我能走多远。它是一个简单的函数:
void delay(volatile uint32_t num) {
volatile uint32_t index = 0;
for(index = (6000 * num); index != 0; index--) {}
}
时钟速度是 72MHz,上面的函数给了我 1ms 的延迟,但我预计是 0.5ms(因为 (6000*6)/72000000 = 0.0005)。
汇编程序是这样的:
delay:
@ args = 0, pretend = 0, frame = 16
@ frame_needed = 0, uses_anonymous_args = 0
@ link register save eliminated.
sub sp, sp, #16 stack pointer = stack pointer - 16
movs r3, #0 move 0 into r3 and update condition flags
str r0, [sp, #4] store r0 at location stack pointer+4
str r3, [sp, #12] store r3 at location stack pointer+12
ldr r3, [sp, #4] load r3 with data at location stack pointer+4
movw r2, #6000 move 6000 into r2 (make r2 6000)
mul r3, r2, r3 r3 = r2 * r3
str r3, [sp, #12] store r3 at stack pointer+12
ldr r3, [sp, #12] load r3 with data at stack pointer+12
cbz r3, .L1 Compare and Branch on Zero
.L4:
ldr r3, [sp, #12] 2 load r3 with data at location stack pointer+12
subs r3, r3, #1 1 subtract 1 from r3 with 'set APSR flag' if any conditions met
str r3, [sp, #12] 2 store r3 at location sp+12
ldr r3, [sp, #12] 2 load r3 with data at location sp+12
cmp r3, #0 1 status = 0 - r3 (if r3 is 0, set status flag)
bne .L4 1 branch to .L4 if not equal
.L1:
add sp, sp, #16 add 16 back to the stack pointer
@ sp needed
bx lr
.size delay, .-delay
.align 2
.global blink
.thumb
.thumb_func
.type blink, %function
我已经评论了我认为每条指令的含义是通过查找它。所以我相信 .L4 部分是延迟函数的循环,它有 6 个指令长。我确实意识到时钟周期并不总是与指令相同,但由于存在如此大的差异,并且由于这是一个我认为可以有效预测和流水线的循环,我想知道是否有充分的理由让我看到 2 个时钟每条指令的周期数。
背景:
在我正在做的项目中,我需要使用5个输出引脚来控制线性ccd,据说时序要求相当严格。绝对频率不会达到最大值(我将比 cpu 能够为引脚提供时钟更慢)但是引脚之间的相对时序很重要。因此,我考虑使用循环来提供引脚电压变化事件之间的短延迟(大约 100 ns),或者甚至在展开的汇编程序中编写整个部分,而不是使用我能力极限的中断,并且可能会使相对时序复杂化,因为我有足够的程序存储 space。有一段时间引脚没有变化,在此期间我可以 运行 ADC 对信号进行采样。
尽管我所询问的奇怪行为并不能阻止我,但我宁愿在继续之前了解它。
编辑:根据评论,arm 技术参考给出了指令时间。我已将它们添加到组件中。但它仍然只有 9 个周期,而不是我预期的 12 个。跳跃本身就是一个循环吗?
TIA,皮特
虽然 Dwelch 提出了一些可能也非常相关的观点,但我想我必须把这个给 ElderBug,所以感谢大家。从这里开始,我将尝试使用展开的程序集来切换引脚,这些引脚在它们的变化中相隔 20ns,然后 return 返回到 C 以等待更长的时间,然后进行 ADC 转换,然后返回到程序集以重复该过程,保持关注 gcc 的汇编输出,大致了解我的计时是否正常。 BTW Elder 修改后的 wait_cycles 函数确实如您所说的那样工作。再次感谢。
首先,在 C 中执行自旋等待循环不是一个好主意。在这里,我可以看到您使用 -O0
(没有优化)进行编译,如果启用优化,您的等待时间会短得多(编辑:实际上,您发布的未优化代码可能只是来自 volatile
,但是这并不重要)。 C 等待循环不可靠。我维护了一个依赖于这样一个函数的程序,每次我们不得不改变一个编译器标志时,时间都被打乱了(幸运的是,结果有一个蜂鸣器走调了,提醒我们改变等待循环).
关于为什么你看不到每个周期有 1 个指令,这是因为有些指令不需要 1 个周期。例如,如果采用分支,bne
可以采用额外的周期。问题是你可以有更少的确定性因素,比如总线使用。访问 RAM 意味着使用总线,总线可能忙于从 ROM 获取数据或被 DMA 使用。这意味着 STR
和 LDR
等指令可能会延迟。在您的示例中,您在同一位置有一个 STR
后跟一个 LDR
(典型的 -O0
);如果 MCU 没有存储到加载转发,你可以有延迟。
我在计时方面所做的是使用硬件定时器实现 1µs 以上的延迟,并使用硬编码的汇编循环实现真正短的延迟。
对于硬件定时器,您只需设置一个固定频率的定时器(如果您希望延迟精确到 1µs,则周期 < 1µs),并使用一些像这样的简单代码:
void wait_us( uint32_t us ) {
uint32_t mark = GET_TIMER();
us *= TIMER_FREQ/1000000;
while( us > GET_TIMER() - mark );
}
你甚至可以使用mark
作为参数在某个任务之前设置它,并使用该函数等待剩余时间。示例:
uint32_t mark = GET_TIMER();
some_task();
wait_us( mark, 200 );
为了等待组装,我将这个用于 ARM Cortex-M4(接近你的):
#define CYCLES_PER_LOOP 3
inline void wait_cycles( uint32_t n ) {
uint32_t l = n/CYCLES_PER_LOOP;
asm volatile( "0:" "SUBS %[count], 1;" "BNE 0b;" :[count]"+r"(l) );
}
这非常简短、精确,并且不会受到编译器标志或总线负载的影响。
您可能需要调整 CYCLES_PER_LOOP
,但我认为它对您的 MCU 来说是相同的值(这里是 SUBS+BNE
的 1+2)。
这是一个 cortex-m3,所以您可能 运行 没有闪存?您是否尝试过 运行 from ram and/or 调整闪存速度,或调整时钟与闪存速度(减慢主时钟)以便您可以使闪存接近每次访问的单个周期能够。
你也在对其中一半指令进行内存访问,这是一个周期或更多的获取周期(如果你在同一个时钟上的 sram 运行 上,则为一个周期)和另一个用于 ram 访问的时钟(由于使用了 volatile)。因此这可能占每个时钟一个时钟和每个时钟两个时钟之间差异的一定百分比,分支也可能花费超过一个时钟,在 m3 上不确定您是否可以打开或关闭它(分支预测)和分支预测无论如何它的工作方式有点有趣,如果它太靠近获取块的开头那么它就不会工作,所以分支在 ram 中的位置会影响性能,其中任何一个在 ram 中都会影响性能,你可以通过在代码前面的任何地方添加 nop 来改变循环的对齐方式来做实验,影响缓存(你可能没有这里)并且还可以根据指令在获取中的大小和位置影响其他事情. (例如,某些手臂一次获取 8 条指令)。
您不仅需要了解汇编以了解您正在尝试做什么,而且还需要了解如何操作该汇编以及对齐、重新安排指令组合等其他事情,有时更多的指令比更少的指令更快等等.管道和缓存充其量也很难预测,并且可以很容易地通过手动优化代码摆脱假设和实验。
即使你克服了慢速闪存、缺少缓存(虽然你不能依赖它的性能)和其他问题,核心和I/O之间的逻辑和I/O 的位碰撞可能是另一个性能损失,没有理由期望 I/O 每次访问的周期数很少,它甚至可能是两位数的时钟数。在这项研究的早期,您需要启动 gpio 只读循环、只写循环和 read/write 循环。如果您依赖 gpio 逻辑只触摸端口中的一位而不是整个端口,这可能会产生周期成本,因此您还需要对其进行性能调整。
如果您在时序上什至接近极限并且必须是硬实时的,您可能想要考虑使用 cpld,因为额外的一行代码或新的编译器版本可以完全摆脱项目时间。
我正在尝试了解 arm-none-eabi-gcc 为 stm32f103 芯片组生成的一些汇编程序,这似乎 运行 正好是我预期速度的一半。我对汇编程序不是很熟悉,但是因为每个人总是说如果你想了解你的编译器在做什么,请阅读 asm,我看看我能走多远。它是一个简单的函数:
void delay(volatile uint32_t num) {
volatile uint32_t index = 0;
for(index = (6000 * num); index != 0; index--) {}
}
时钟速度是 72MHz,上面的函数给了我 1ms 的延迟,但我预计是 0.5ms(因为 (6000*6)/72000000 = 0.0005)。
汇编程序是这样的:
delay:
@ args = 0, pretend = 0, frame = 16
@ frame_needed = 0, uses_anonymous_args = 0
@ link register save eliminated.
sub sp, sp, #16 stack pointer = stack pointer - 16
movs r3, #0 move 0 into r3 and update condition flags
str r0, [sp, #4] store r0 at location stack pointer+4
str r3, [sp, #12] store r3 at location stack pointer+12
ldr r3, [sp, #4] load r3 with data at location stack pointer+4
movw r2, #6000 move 6000 into r2 (make r2 6000)
mul r3, r2, r3 r3 = r2 * r3
str r3, [sp, #12] store r3 at stack pointer+12
ldr r3, [sp, #12] load r3 with data at stack pointer+12
cbz r3, .L1 Compare and Branch on Zero
.L4:
ldr r3, [sp, #12] 2 load r3 with data at location stack pointer+12
subs r3, r3, #1 1 subtract 1 from r3 with 'set APSR flag' if any conditions met
str r3, [sp, #12] 2 store r3 at location sp+12
ldr r3, [sp, #12] 2 load r3 with data at location sp+12
cmp r3, #0 1 status = 0 - r3 (if r3 is 0, set status flag)
bne .L4 1 branch to .L4 if not equal
.L1:
add sp, sp, #16 add 16 back to the stack pointer
@ sp needed
bx lr
.size delay, .-delay
.align 2
.global blink
.thumb
.thumb_func
.type blink, %function
我已经评论了我认为每条指令的含义是通过查找它。所以我相信 .L4 部分是延迟函数的循环,它有 6 个指令长。我确实意识到时钟周期并不总是与指令相同,但由于存在如此大的差异,并且由于这是一个我认为可以有效预测和流水线的循环,我想知道是否有充分的理由让我看到 2 个时钟每条指令的周期数。
背景: 在我正在做的项目中,我需要使用5个输出引脚来控制线性ccd,据说时序要求相当严格。绝对频率不会达到最大值(我将比 cpu 能够为引脚提供时钟更慢)但是引脚之间的相对时序很重要。因此,我考虑使用循环来提供引脚电压变化事件之间的短延迟(大约 100 ns),或者甚至在展开的汇编程序中编写整个部分,而不是使用我能力极限的中断,并且可能会使相对时序复杂化,因为我有足够的程序存储 space。有一段时间引脚没有变化,在此期间我可以 运行 ADC 对信号进行采样。
尽管我所询问的奇怪行为并不能阻止我,但我宁愿在继续之前了解它。
编辑:根据评论,arm 技术参考给出了指令时间。我已将它们添加到组件中。但它仍然只有 9 个周期,而不是我预期的 12 个。跳跃本身就是一个循环吗?
TIA,皮特
虽然 Dwelch 提出了一些可能也非常相关的观点,但我想我必须把这个给 ElderBug,所以感谢大家。从这里开始,我将尝试使用展开的程序集来切换引脚,这些引脚在它们的变化中相隔 20ns,然后 return 返回到 C 以等待更长的时间,然后进行 ADC 转换,然后返回到程序集以重复该过程,保持关注 gcc 的汇编输出,大致了解我的计时是否正常。 BTW Elder 修改后的 wait_cycles 函数确实如您所说的那样工作。再次感谢。
首先,在 C 中执行自旋等待循环不是一个好主意。在这里,我可以看到您使用 -O0
(没有优化)进行编译,如果启用优化,您的等待时间会短得多(编辑:实际上,您发布的未优化代码可能只是来自 volatile
,但是这并不重要)。 C 等待循环不可靠。我维护了一个依赖于这样一个函数的程序,每次我们不得不改变一个编译器标志时,时间都被打乱了(幸运的是,结果有一个蜂鸣器走调了,提醒我们改变等待循环).
关于为什么你看不到每个周期有 1 个指令,这是因为有些指令不需要 1 个周期。例如,如果采用分支,bne
可以采用额外的周期。问题是你可以有更少的确定性因素,比如总线使用。访问 RAM 意味着使用总线,总线可能忙于从 ROM 获取数据或被 DMA 使用。这意味着 STR
和 LDR
等指令可能会延迟。在您的示例中,您在同一位置有一个 STR
后跟一个 LDR
(典型的 -O0
);如果 MCU 没有存储到加载转发,你可以有延迟。
我在计时方面所做的是使用硬件定时器实现 1µs 以上的延迟,并使用硬编码的汇编循环实现真正短的延迟。
对于硬件定时器,您只需设置一个固定频率的定时器(如果您希望延迟精确到 1µs,则周期 < 1µs),并使用一些像这样的简单代码:
void wait_us( uint32_t us ) {
uint32_t mark = GET_TIMER();
us *= TIMER_FREQ/1000000;
while( us > GET_TIMER() - mark );
}
你甚至可以使用mark
作为参数在某个任务之前设置它,并使用该函数等待剩余时间。示例:
uint32_t mark = GET_TIMER();
some_task();
wait_us( mark, 200 );
为了等待组装,我将这个用于 ARM Cortex-M4(接近你的):
#define CYCLES_PER_LOOP 3
inline void wait_cycles( uint32_t n ) {
uint32_t l = n/CYCLES_PER_LOOP;
asm volatile( "0:" "SUBS %[count], 1;" "BNE 0b;" :[count]"+r"(l) );
}
这非常简短、精确,并且不会受到编译器标志或总线负载的影响。
您可能需要调整 CYCLES_PER_LOOP
,但我认为它对您的 MCU 来说是相同的值(这里是 SUBS+BNE
的 1+2)。
这是一个 cortex-m3,所以您可能 运行 没有闪存?您是否尝试过 运行 from ram and/or 调整闪存速度,或调整时钟与闪存速度(减慢主时钟)以便您可以使闪存接近每次访问的单个周期能够。
你也在对其中一半指令进行内存访问,这是一个周期或更多的获取周期(如果你在同一个时钟上的 sram 运行 上,则为一个周期)和另一个用于 ram 访问的时钟(由于使用了 volatile)。因此这可能占每个时钟一个时钟和每个时钟两个时钟之间差异的一定百分比,分支也可能花费超过一个时钟,在 m3 上不确定您是否可以打开或关闭它(分支预测)和分支预测无论如何它的工作方式有点有趣,如果它太靠近获取块的开头那么它就不会工作,所以分支在 ram 中的位置会影响性能,其中任何一个在 ram 中都会影响性能,你可以通过在代码前面的任何地方添加 nop 来改变循环的对齐方式来做实验,影响缓存(你可能没有这里)并且还可以根据指令在获取中的大小和位置影响其他事情. (例如,某些手臂一次获取 8 条指令)。
您不仅需要了解汇编以了解您正在尝试做什么,而且还需要了解如何操作该汇编以及对齐、重新安排指令组合等其他事情,有时更多的指令比更少的指令更快等等.管道和缓存充其量也很难预测,并且可以很容易地通过手动优化代码摆脱假设和实验。
即使你克服了慢速闪存、缺少缓存(虽然你不能依赖它的性能)和其他问题,核心和I/O之间的逻辑和I/O 的位碰撞可能是另一个性能损失,没有理由期望 I/O 每次访问的周期数很少,它甚至可能是两位数的时钟数。在这项研究的早期,您需要启动 gpio 只读循环、只写循环和 read/write 循环。如果您依赖 gpio 逻辑只触摸端口中的一位而不是整个端口,这可能会产生周期成本,因此您还需要对其进行性能调整。
如果您在时序上什至接近极限并且必须是硬实时的,您可能想要考虑使用 cpld,因为额外的一行代码或新的编译器版本可以完全摆脱项目时间。