Cycle counter (CYCCNT) differs when single-stepping vs. running code - Page 1
Forum section: electronics / general

author: uer166
author_rank: Super Contributor
timestamp: April 22, 2022, 03:18:22 am
content: I was doing some experiments with floating point assembly for fun, and am trying to time the performance of some floating point MACs and other assembly. This is a STM32G474 running at 15MHz (no flash wait states AFAICT). For whatever reason, multi-cycle instructions such as vsqrt (should be 14 cycles), and vmla (should be 3 cycles), only increment the CYCCNT timer by 1, when single stepping or otherwise debugging the code.When the entire thing runs without debug, the counter value seems to resemble those 14 and 3 cycles length respectively. So, what gives? Is this a limitation of the debugger? The DWT core cycle counter, or something else? Attached is code and disassembly as seen by debugger, and the code is:Code:int main(void){/* USER CODE BEGIN 1 *//* USER CODE END 1 *//* MCU Configuration--------------------------------------------------------*//* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* USER CODE BEGIN Init *//* USER CODE END Init *//* Configure the system clock */SystemClock_Config();/* USER CODE BEGIN SysInit *//* USER CODE END SysInit *//* Initialize all configured peripherals *//* USER CODE BEGIN 2 *//* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */DWT->CTRL &= ~SysTick_CTRL_ENABLE_Msk;CoreDebug->DEMCR |= 0x01000000; // TRCENADWT->CYCCNT = 0;DWT->CTRL |= SysTick_CTRL_ENABLE_Msk;volatile float fl = 69134.420f;volatile uint32_t start = DWT->CYCCNT;asm volatile("nop \n\t""vmov.f32 s0, #3.37500000 \n\t""vmov.f32 s1, #3.37500000 \n\t""vmla.f32 s0, s0, s1 \n\t ""vmla.f32 s0, s0, s1 \n\t ""vmla.f32 s0, s0, s1 \n\t ""vmla.f32 s0, s0, s1 \n\t ""vmla.f32 s0, s0, s1 \n\t ""vmov s0, % \n\t""vsqrt.f32 s0, s0 \n\t""vsqrt.f32 s0, s0 \n\t""vsqrt.f32 s0, s0 \n\t""vsqrt.f32 s0, s0 \n\t":: /* Output */"w" (fl) :/* Input */"memory", "r3", "s0", "s1", "s2");/* Clobber list */volatile uint32_t delta = DWT->CYCCNT - start;while (1){/* USER CODE END WHILE *//* USER CODE BEGIN 3 */}/* USER CODE END 3 */}

author: ataradov
author_rank: Super Contributor
timestamp: April 22, 2022, 04:33:56 am
content: I would think this is normal. Cycle counter is stopped for debugging, but execution of the current instruction is not stopped. So, debugger blocking the counter takes precedence.There are a lot of things like this, debuggers are not transparent to the executed code.

author: SiliconWizard
author_rank: Super Contributor
timestamp: April 22, 2022, 04:54:10 pm
content: It is by design, and it's ARM-related, so you'll get the same behavior with any other Cortex-M-based MCU.https://developer.arm.com/documentation/ddi0403/d/Debug-Architecture/ARMv7-M-Debug/The-Data-Watchpoint-and-Trace-unit/CYCCNT-cycle-counter-and-related-timers?lang=enQuote

author: uer166
author_rank: Super Contributor
timestamp: April 22, 2022, 07:45:25 pm
content: Quote from: SiliconWizard on April 22, 2022, 04:54:10 pmI'm aware of that, but that doesn't quite explain (in my head), why each single-stepping iteration, it only increments by one, and not 14 if the instructions it's stepping through takes 14 cycles to complete. If it didn't increment at all and stayed at the same value, that would have been more satisfying than incrementing by an arbitrary value of 1.

author: ataradov
author_rank: Super Contributor
timestamp: April 22, 2022, 08:02:49 pm
content: Debugger break interrupts instruction fetch, not execution. Anything that is already in the pipeline will execute. Incrementing by one is because increment happens on the fetch, and each debugger step fetches one instruction.I think this simply was not a design consideration, so the behaviour is just a side effect of all other things combined. You will not get a true measure of performance when debugger is attached no matter what you do. Debugger performs transfers, which affect execution of the program anyway.

author: SiliconWizard
author_rank: Super Contributor
timestamp: April 22, 2022, 09:57:29 pm
content: Yes this is a pipelining problem.ARM probably implemented this the easier way, stopping the counter on the breakpoint and then letting the pipeline flush as ataradov said.And even without halting the CPU, inspecting the cycle counter for single instructions is ridden with pitfalls, due to the pipelining. For a meaningful estimate of how many cycles a given instruction takes, you should usually time the execution of a number of such instructions, not just a single one.

author: hans
author_rank: Super Contributor
timestamp: April 23, 2022, 09:23:31 pm
content: I agree that this is probably a side-effect how CPUs are designed.Pipelined RISC CPUs need some mechanism to introduce stalls to resolve data hazards or do branching. For example, a common way of doing this in a classic pipeline is inserting bubbles or "NOP" instructions. When this logic is already present in the core (e.g. to resolve read-after-write hazards) then a debugger halt is likely going to use the same mechanism.For multi-cycle ALU operations, it could also be that some kind of backpressure mechanism is added between pipeline stages (for example, by making use of skid buffers). The execution of a multi-cycle arithmetic operation is managed by the ALU which is probably not stalled when the debugger "halts" (the final stage of) the CPU. It basically allows the ALU to sneak in these 'missing' cycles when the CYCCNT is indeed not counting up...

author: wek
author_rank: Frequent Contributor
timestamp: April 23, 2022, 10:40:26 pm
content: And there's also this "superscalar-lite" thing:QuoteThis won't influence the CYCNT-during-single-step thing, but those who count cycles might want to know this interesting fact.JW

author: Marco
author_rank: Super Contributor
timestamp: April 24, 2022, 05:05:22 am
content: Quote from: wek on April 23, 2022, 10:40:26 pmI'd say the instructions have floating point delay slots.

author: uer166
author_rank: Super Contributor
timestamp: April 24, 2022, 11:25:39 pm
content: Quote from: wek on April 23, 2022, 10:40:26 pmVery curious. I think that implies that 14 cycles worth of integer instructions following a VSQRT are executed in parallel with the VSQRT. I suppose that makes sense since there is no dependency there, the FP ALU can crunch the numbers locally and write back the result to the FP register set while everything else is running.Other interesting things I found is that even though most of FP instructions are single cycle, the ones that matter for DSP (e.g. a FP MAC), take more cycles than their integer counterparts, so fixed point math would be 2-3x faster in that case, as a crude generalization.

author: westfw
author_rank: Super Contributor
timestamp: April 25, 2022, 05:09:16 am
content: Quote"Could be", anyway. Presumably if you do something that requires the FPU or some resource that it requires, the CPU will stall until the VSQRT completes...It would be "somewhat interesting" to see exactly which instructions cause such stalls; Obviously, accessing the result would do it. What about moves to/from uninvolved FPU registers?

author: Jeroen3
author_rank: Super Contributor
timestamp: April 25, 2022, 10:03:26 am
content: If your debugger implements stepping by moving the breakpoint, you may also get tricked by having an ISR run during your step.

author: DavidAlfa
author_rank: Super Contributor
timestamp: April 29, 2022, 02:14:01 pm
content: Have you tried CPI?https://developer.arm.com/documentation/ddi0337/e/ch11s05s01Edit: Nope. I enabled all the DWT counters, none worked for this purpose.