#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. Когда все остальное не удается, посмотрите на выходные данные сборки каждого из них, а также обратите внимание, хорошо ли выровнен код в кэше для каждого. Вы говорите, что посмотрели на сборку, но теперь вам нужно попробовать и увидеть различия. Я бы также никогда не пытался включить ввод-вывод в профиль, если вы не профилируете ввод-вывод. Вывод результатов в буфер и печать по завершении, чтобы изолировать это.