Счетчик циклов STM32 DWT (CYCLCNT) неожиданное поведение (rust)

#rust #arm #embedded #stm32

#Ржавчина #arm #встроенный #stm32

Вопрос:

Я пытаюсь протестировать ряд функций, предназначенных для встроенного использования (микроконтроллер STM743 arm cortex m7). Весь код написан на Rust. Чтобы сравнить мои функции, я использую DWT CYCLCOUNT (счетчик циклов) для подсчета количества тактов процессора, необходимых для выполнения каждой из этих функций. Затем я отображаю счетчик на главном компьютере с помощью semihosting. Я вижу результаты, которые зависят от изменений в коде, которые, по моему мнению, не должны иметь эффекта. Вот код, который я использую

 #![no_std]
#![no_main]

use panic_halt as _;
use cortex_m::peripheral::{Peripherals, DWT};
use cortex_m_rt::entry;
use cortex_m_semihosting::hprintln;
use core::f32::consts::PI;

use trig_lib::{sin, cos, atan2};

const U32_MAX: u32 = 4_294_967_295u32;

macro_rules! op_cyccnt_diff {
    ( $( $x:expr )* ) => {
        {
            let before = DWT::get_cycle_count();
            $(
                let res = $x;
            )*
            let after = DWT::get_cycle_count();
            let diff =
                if after >= before {
                    after - before
                } else {
                    after   (U32_MAX - before)
                };
            (res, diff)
        }
    };
}

#[entry]
fn main() -> ! {
    let mut peripherals = Peripherals::take().unwrap();
    peripherals.DWT.enable_cycle_counter();
    let mut theta: f32 = 0.;
    let dtheta: f32 = 2. * PI / 10.6;

    loop {
        let (res, diff) = op_cyccnt_diff!(sin(theta));
        // test 1
        hprintln!("  sin({:.6})={:.6}: {}", theta, res, diff).unwrap();
        // test 2
        // hprintln!("  sin={:.6}: {}", res, diff).unwrap();

        theta  = dtheta;
        if theta > 2. * PI {
            theta -= 2. * PI;
        }
    }
}
  

Как вы, наверное, догадались, sin , cos , и atan2 являются тригонометрическими функциями, которые я сравниваю. Я только показал тест sin здесь. Я не считаю, что их реализация имеет решающее значение для этого вопроса, и их интерфейс является стандартным.

Если я использую // test 1 , я получаю что-то вроде:

   sin(0.000000)=0.000000          : 101
  sin(0.592753)=0.559139          : 171
  sin(1.185507)=0.927135          : 164
  sin(1.778260)=0.978837          : 140
  sin(2.371013)=0.697104          : 140
  sin(2.963767)=0.177065          : 147
  sin(3.556520)=-0.403504         : 172
  sin(4.149273)=-0.846134         : 179
  sin(4.742027)=-0.999605         : 172
  sin(5.334780)=-0.813040         : 172
  sin(5.927534)=-0.348535         : 172
  sin(0.237102)=0.235117          : 164
  sin(0.829855)=0.738394          : 164
  sin(1.422608)=0.989249          : 171
  sin(2.015362)=0.903282          : 140
  sin(2.608115)=0.508991          : 140
  

Однако, если я использую // test 2 , я получаю что-то вроде:

   sin=0.000000          : 99
  sin=0.559139          : 121
  sin=0.927135          : 121
  sin=0.978837          : 122
  sin=0.697104          : 122
  sin=0.177065          : 122
  sin=-0.403504         : 151
  sin=-0.846134         : 151
  sin=-0.999605         : 158
  sin=-0.813040         : 151
  sin=-0.348535         : 151
  sin=0.235117          : 121
  sin=0.738394          : 121
  sin=0.989249          : 121
  sin=0.903282          : 122
  sin=0.508991          : 122
  

Разница в том, печатаю ли я theta значение. Результаты, по-видимому, указывают на то, что для того, чтобы не печатать значение theta, требуется меньше тактов, чем для его печати. Однако единственным утверждением между получением начального и конечного количества циклов является let res = sin(theta); , поэтому я не понимаю, почему печать theta оказывает какое-либо влияние на результат. Более того, если я заменю hprintln! оператор на hprintln!("{}", diff).unwrap(); , я получу еще более низкие результаты:

 51
66
66
94
92
68
74
74
68
68
68
68
68
66
117
117
74
74
68
68
68
68
  

Is my setup somehow flawed? Or, am I not measuring what I think I’m measuring? Why does changing hprintln! change the cycle count result? Is there a way to perform this test such that the result only depends on the number of clock cycles required to compute sin(theta) ?

Я пытался посмотреть на дизассемблирование, но компилятор вставляет много функций, и поэтому сложно сопоставить инструкции с исходным кодом.


Редактировать

Я попытался обернуть код подсчета циклов в ограждение компилятора следующим образом:

 compiler_fence(Ordering::Acquire);
let before = DWT::get_cycle_count();
$(
    let res = $x;
)*
let after = DWT::get_cycle_count();
compiler_fence(Ordering::Release);
  

К сожалению, это, похоже, не имеет эффекта.


Правка 2

Я вывел ввод-вывод из цикла, чтобы попытаться отделить код, который я сравниваю, от отображаемого вывода. Соответствующее изменение

     let mut i: usize = 0;
    let mut theta_log: [f32; ITER] = [0.; ITER];
    let mut sin_res_log: [f32; ITER] = [0.; ITER];
    let mut sin_diff_log: [u32; ITER] = [0; ITER];

    let mut cos_res_log: [f32; ITER] = [0.; ITER];
    let mut cos_diff_log: [u32; ITER] = [0; ITER];

    while i < ITER {
        let (res, diff) = op_cyccnt_diff!(sin(theta));
        theta_log[i] = theta;
        sin_res_log[i] = res;
        sin_diff_log[i] = diff;

        // let (res, diff) = op_cyccnt_diff!(cos(theta));
        // cos_res_log[i] = res;
        // cos_diff_log[i] = diff;

        theta  = dtheta;
        if theta > 2. * PI {
            theta -= 2. * PI;
        }

        i  = 1;
    }

    for i in 0..ITER {
        hprintln!("  sin({:.6})={:.6}          : {}", theta_log[i], sin_res_log[i], sin_diff_log[i]).unwrap();
        // hprintln!("  cos({:.6})={:.6}          : {}", theta_log[i], cos_res_log[i], cos_diff_log[i]).unwrap();
        // hprintln!("----------------------------").unwrap();
    }

    loop {
    }
  

Выходные данные только для синуса — это все 2 и несколько 8 (это кажется слишком низким …), А выходные данные, когда часть косинуса раскомментирована, сохраняются в 100-х годах.

Комментарии:

1. Я предполагаю, что вы компилируете в режиме выпуска. В этом случае компилятору разрешается изменять порядок инструкций во время оптимизации. Возможно, что часть инструкций, сгенерированных hprintln вызовом, может быть перемещена до let after = DWT::get_cycle_count(); .

2. Рассматривали ли вы возможность добавления соответствующих ограждений компилятора (см. Здесь ) до и после измерения?

3. @Jmb, я. Есть ли какой-либо способ выборочно предотвратить переход чего-либо еще между let before и let after ? Не означает ли это также, что некоторые вычисления синуса могут выполняться за пределами этого интервала? Если это так, то этот тест по сути бессмыслен… Я забочусь только об этой метрике для оптимизированного кода, поэтому компиляция с отключенной оптимизацией не помогает.

4. @L.Riemer У меня не было, но это выглядит очень многообещающе! Спасибо, что указали на это

5. Когда все остальное не удается, посмотрите на выходные данные сборки каждого из них, а также обратите внимание, хорошо ли выровнен код в кэше для каждого. Вы говорите, что посмотрели на сборку, но теперь вам нужно попробовать и увидеть различия. Я бы также никогда не пытался включить ввод-вывод в профиль, если вы не профилируете ввод-вывод. Вывод результатов в буфер и печать по завершении, чтобы изолировать это.