LINUX.ORG.RU

мини-C++ - оценка доли процессорного времени когда бесконечный цикл не выполняется

 , , ,


0

4

Столкнулся с нюансами PCIe-устройства, которое при ничего не делании на ровном месте генерировало много irq, а htop показывал что average-загрузка малоядерного CPU 0.4% (при том что драйвер на каждое прерывание что-то делал).

По /proc/interrupts от этого устройства было более 10000 прерываний в секунду, все приходились на 1 ядро.

Оказалось что ubuntu-ядро для «оптимизации» не ведёт учёт времени потраченного в аппаратных прерываниях, и таким образом просто закрывает глаза на то куда «утекают» (перестают быть доступны userspace) вычислительные ресурсы [точнее говоря процессорное время]

Прежде чем пытаться устройство лечить - решил понять, а сколько же на самом деле ресурсов отнято с точки зрения потерь времени, когда вместо исполнения программы ядро процессора занято чем-то другим. Причём, максимально примитивным методом - оценив насколько разнится ожидаемое время выполнения цикла от максимально быстрого, и для уменьшения риска влияния внешних факторов - не использовать никакой runtime (аллокаторы, локали, динамическая загрузка, thread-local-storage, всякие протекторы и прочее) используя для времени rdtsc.

Но, для интереса - в качестве языка - хотелось остаться в рамках относительно современного C++, включая его легковесные функции не привязанные к runtime и оставить код достаточно высокоуровневым. Поскольку без runtime - никакой портабельности, только linux x86_64 с относительно новыми процессорами, компиляция только с определёнными параметрами. Переносимость и не требовалась, всё это только для одной системы.

И вот, что получилось:

// g++ -Wextra -Wall -Os -std=c++20 -fcf-protection=none -fno-stack-protector -static -nostartfiles -Wl,--nmagic -Wl,-z,nosectionheader
#include <sys/syscall.h>
#include <sys/uio.h>
#include <unistd.h>
#include <cstdlib>
#include <charconv>
#include <cstdint>
#include <array>
#include <x86intrin.h>

struct PrintIoVec {
	template <typename T>
	explicit PrintIoVec(T number)
		: inner_ref_iovec(format_buf.begin(),
				std::to_chars(format_buf.begin(), format_buf.end(), number).ptr - format_buf.begin()){};

	template <std::size_t N>
	explicit PrintIoVec(const char (&text)[N]) : inner_ref_iovec((void*)text, N){};

	PrintIoVec(const PrintIoVec&) = delete;
	std::array<char, 22> format_buf;
	const iovec inner_ref_iovec;
};

template <typename... Args>
void println(Args&&... args)
{
	iovec endl = {(char*)"\n", 1};
	syscall(SYS_writev, 1, std::array{PrintIoVec(args).inner_ref_iovec..., endl}.data(), sizeof...(args) + 1);
}

// 16-byte stack alignment ABI assumes that stack is aligned at call site, not at callee
extern "C" void __attribute__((naked)) _start() { __asm__("call stack_aligned_start"); }

const int print_period = 0x10000000;
const int estimation_period = 0x400;

extern "C" void stack_aligned_start()
{
	uint64_t prev_print = 0;
	uint64_t min_diff = UINT64_MAX;
	uint64_t prev[estimation_period] = {};
	for (uint64_t i = 0;;) {
		int idx = ++i % estimation_period;
		uint64_t now = __rdtsc();
		if (i % print_period == 0) {
			if (prev_print) {
				uint64_t actual = now - prev_print;
				uint64_t min_expected = print_period * min_diff / estimation_period;
				uint64_t delay = actual - min_expected;
				println("calibration: TSC per ", estimation_period , " iterations=", min_diff,
								"  process was off-cpu for ", delay * 1000 / actual, "‰");
				min_diff = UINT64_MAX;
			}
			prev_print = __rdtsc();
			continue;
		}
		min_diff = std::min(min_diff, now - prev[idx]);
		prev[idx] = now;
	}
	//_Exit(0); //compiles but effectively unreachable
}

Командой g++ -Wextra -Wall -Os -std=c++20 -fcf-protection=none -fno-stack-protector -static -nostartfiles -Wl,--nmagic -Wl,-z,nosectionheader собирается в ~1500 байт статический бинарник, что кажется вполне адекватным размером для программы которая делает что-то осмысленное.

При работе периодически печатает текущую оценку количества TSC/цикл и собственно оценку доли времени которую программа была вытеснена с активной фазы исполнения в промилле. Адекватность этой оценки проверил запустив несколько экземпляров программы привязанных к одному ядру через taskset 1 ./a.out

Для себя сделал вывод - что C++ всё еще может в минимальные программы) Читабельность - «как всегда у C++», тут ничего не изменилось.

P.S. исходную задачу программа решила: когда устройство входило в шквал прерываний, программа привязанная к тому же ядру где были прерывания показала рост off-cpu-time c ~10 до ~250 промилле.

P.P.S. Да, это можно считать мерялкой «на каком языке можно получить относительно высокоуровневую функцию печати в рамках статического 1500 байт бинарника» ))

★★★★

C++ всё еще может в минимальные программы

C++

extern «C» void __attribute__((naked)) _start() { __asm__

#include <x86intrin.h>

-std=c++20

/0

То что ты эту прогу, прибитую гвоздями к системе, собирал g++ и указал С++20 случайность. Мог и std не использовать, раз уж упоролся в остальные рукоделия, которые все еще называешь «С++».

Читабельность - «как всегда у C++», тут ничего не изменилось.

И виноват в этом лично ты :)

slackwarrior ★★★★★
()

std::to_chars

А у меня в прошивке для микроконтроллера попытка применения std::to_chars для вывода плавучки давала сразу под сотню килобайт к прошивке. Для интов да, можно использовать, там мало добавляет.

Beewek ★★★★
()

исходную задачу программа решила

Криво поставил задачу, криво решил, получил ненужные значения, сам себя похвалил.

devl547 ★★★★★
()

g++ -Wextra -Wall -Os -std=c++20 -fcf-protection=none -fno-stack-protector -static -nostartfiles -Wl,--nmagic -Wl,-z,nosectionheader собирается в ~1500 байт статический бинарник, что кажется вполне адекватным размером для программы которая делает что-то осмысленное.

Когда количество приседаний для этого вот жлобства превышает количество «чего собственно делает программа». Ну и да. Ее итоговый размер не относится к С++ и заслуга внешних утилит, которыми ты варварски покоцал итоговый ELF... Но минимальную программу все равно не получил.

slackwarrior ★★★★★
()
Ответ на: комментарий от slackwarrior

У тебя приступ фанатизма кажется.

Впрочем, если отбросить эмоции, то ты частично прав. А именно, C++ тут несколько лишний (лучше было делать на Си), и нечитабельность исходника действительно из-за его автора случилась.

firkax ★★★★★
()
Ответ на: комментарий от I-Love-Microsoft

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

А ядра ubuntu и debian собраны без этого параметра.

GPFault ★★★★
() автор топика
Последнее исправление: GPFault (всего исправлений: 1)
Ответ на: комментарий от COKPOWEHEU

Посмотрел на примере нагрузки входяшими пингами в несколько flood-потоков, которая (при конкретной загрузке) даёт softirq-нагрузку на cpu0 (обработчик прерываний не мигрирует между ядрами). Программу повесил на то же ядро, остальные ядра по сути в простое (менее 1% утилизации)

По показателям htop на этом ядре - user: 75%, system 12%, softirq 13%. При этом в попроцессном просмотре он почему-то приплюсовал user и system в 87% для процесса off_cpu_estimate, хотя тот делает крайне мало сисколлов.

grep 'cpu0' /proc/stat; timeout 100 taskset 1 ./a.out; grep cpu0 /proc/stat;
cpu0 3496 0 582 82904 12 838 19635 0 0 0
calibration: TSC per 1024 iterations=26560  process was off-cpu for 153‰
calibration: TSC per 1024 iterations=26302  process was off-cpu for 162‰
....
calibration: TSC per 1024 iterations=26372  process was off-cpu for 160‰
cpu0 10987 0 1745 82904 12 873 20947 0 0 0

То есть программа определяет off-cpu время как ~15-16%. Это явно всё время проведённое в softirq и похоже какая-то небольшая доля времени проведённго в system

Разбивка по user+system+softirq в /proc/stat такая же как в htop - ибо он отсюда и берёт данные.

В общем, мне по итогу не очень понятно насколько это всё точно, интуитивно кажется что 12% system, которые появляются только когда ядро одновременно загружено и обработкой ответов на пинги и userspace приложением - это какая-то очень неточная оценка затрат на переключение контекста.

GPFault ★★★★
() автор топика
Последнее исправление: GPFault (всего исправлений: 1)
Ответ на: комментарий от slackwarrior

Ее итоговый размер не относится к С++ и заслуга внешних утилит, которыми ты варварски покоцал итоговый ELF…

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

GPFault ★★★★
() автор топика
Ответ на: комментарий от Beewek

А у меня в прошивке для микроконтроллера попытка применения std::to_chars для вывода плавучки давала сразу под сотню килобайт к прошивке. Для интов да, можно использовать, там мало добавляет.

Да, для плавучки у std::to_chars там огромная добавка получается, наверное оно тянет полновенсную libc-реализацию. Поэтому только целочисленные.

GPFault ★★★★
() автор топика
Ответ на: комментарий от slackwarrior

C++

extern «C» void attribute((naked)) _start() { asm

#include <x86intrin.h>

-std=c++20

/0

это иллюстрация того что язык позволяет опуститься на очень низкий уровень там где захотелось, а там где не захотелось - одновременно с этим выборочно использовать стандартную билиотеку (std::to_chars). Если стандарт взять меньше 20го то вываливает стену ошибок компиляции, там сложнее будет реализовать подобную смесь с минимлистично-эргономичной функцией печати.

GPFault ★★★★
() автор топика

Я так тоже однажды usb-контроллер пожёг, так он стал генерировать 100 тысяч прерываний в секунду. И без тестовых програм было видно тормоза.

anonymous
()
Ответ на: комментарий от Beewek

Я вообще плохо понимаю зачем на МК что-то кроме сишки надо. Ну раст может можно, если он поддерживается. МК ненавидят динамическую память, т.к. куча подвержена фрагментации, если нет полноценной ОС. А C++ без динамической памяти ну такое… Тем более когда каждый бит на счету.

ЗЫ

Это ардуинщики что-то навредили, явно.

peregrine ★★★★★
()
Последнее исправление: peregrine (всего исправлений: 2)
Ответ на: комментарий от peregrine

Во-первых, в плюсах есть много полезностей и без динамической памяти. Шаблоны, constexpr, ооп, RAII, и прочие штуки.

Во-вторых, в эмбеде есть много библиотек, уже использующих динамическую память (LwIP, FreeRTOS…), и это вполне работает годами без фрагментации. Понятно, что с дополнительными ухищрениями, но работает.

И в третьих, "каждый бит на счету - это уже давно редкость. Сейчас микроконтроллеры в большинстве своём довольно богаты и флеш-памятью, и ОЗУ. Не сравнить с PIC16F84 или MCS-51.

Beewek ★★★★
()
Ответ на: комментарий от peregrine

Ну то есть ты понимаешь, что твоё изначальное обобщение «Я вообще плохо понимаю зачем на МК что-то кроме сишки надо.» совсем мимо кассы. Но даже на совсем мелкой мелочи можно вместо сишки использовать плюсы, с ограничениями конечно, но это всё равно лучше сишки. Хотя бы constexpr и шаблоны - уже значительное улучшение.

Beewek ★★★★
()
Ответ на: комментарий от peregrine

Я вообще плохо понимаю зачем на МК что-то кроме сишки надо.

Ну раст может можно, если он поддерживается.

А C++ без динамической памяти ну такое…

Ну так ты же ничего не знаешь ни про сишку, ни тем более про цпп, ни даже про раст. Как же ты понимать собрался, ничего не зная?

Экспертиза такая экспертиза.

anonymous
()

Чёт с алгоритмом подсчёта перемудрил, и возня странная с массивом prev, и рекомендуется для таких вещей __rdtscp использовать чтобы проц команды не переставлял. Вот версия на более подходящем для системщины язычке, раз уж такая пьянка:

#![no_std]
#![no_main]

use core::str::from_utf8;
use core::arch::{asm};
use core::hint::black_box;
use core::arch::x86_64::{__rdtscp, _rdtsc};
use core::fmt::{self, Write};
use core::str::from_utf8_unchecked;
use core::usize;

fn rd_tim()->u64 { let mut r=0; unsafe { __rdtscp(&mut r) }}
// fn rd_tim()->u64 {  unsafe { _rdtsc() }}

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! { loop{} }

struct StrBuf<const SZ:usize> {
    buf: [u8; SZ],
    pos: usize,
}
impl<const SZ:usize> StrBuf<SZ> {
    pub fn new() -> Self { Self{ buf:[0; SZ], pos:0 } }
    pub fn as_str(&self) -> &str {
        unsafe { from_utf8_unchecked( &self.buf[..self.pos] ) }
    }
}
impl<const SZ:usize> Write for StrBuf<SZ> {
    fn write_str(&mut self, s: &str) -> fmt::Result {
        if (self.pos+s.len()) > SZ { return Err(fmt::Error) }
        for i in 0..s.as_bytes().len() {
            self.buf[i+self.pos] = s.as_bytes()[i];
        }
        self.pos += s.len();
        Ok(())
    }
}

#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
    let est_per = 1000;
    let tot_est_per = 2_000_000;
    let mut tot_actual = 0;
    let mut min_diff = u64::MAX;
    loop {
        for _ in 0..tot_est_per {
            let now = rd_tim();
            for i in 0..est_per {
                black_box(i);
            }
            let diff = rd_tim()-now;
            min_diff = core::cmp::min(min_diff, diff);
            tot_actual += diff;
        }
        let off_cpu_permille = (tot_actual-min_diff*tot_est_per)*1000/tot_actual;
        let mut str_buf = StrBuf::<200>::new();
        let _ = write!(&mut str_buf,   "process was off-cpu for  {}‰\n",  off_cpu_permille);
        write_stdout(str_buf.as_str());
        tot_actual = 0;
    }
}
fn write_stdout(s: &str) {
    unsafe {
        asm!(
            "syscall",
            in("rax") 1,
            in("rdi") 1,
            in("rsi") s.as_ptr(),
            in("rdx") s.len(),
            out("rcx") _,
            out("r11") _,
        );
    }
}

~2.9 КиБ с одной стороны(и не пытался особо минимизировать), но с другой - полноценное продвинутое форматирование, и разница между rdtscp и rdtsc есть, последний занижает. И c выводом флоатов разрастается до 16.6 КиБ, что даже для ембедеда всё ещё терпимо, но там, обычно, свои форматтеры, более лёгкие.

zurg ★★
()
Последнее исправление: zurg (всего исправлений: 3)
Ответ на: комментарий от zurg

разница между rdtscp и rdtsc есть, последний занижает

чуднО, не углублялся в эту тему; казалось бы оба должны монотонно расти в идеале не завися от частоты процессора.

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

#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {

А вот тут не происходит часом той же проблемы что в плюсах? У меня на C++ вначале всё работало без трюка с stack_aligned_start, пока в выводимых данных не появились объёмы достаточно длинные для того, чтоб компилятор начал вставлять в их копирование на стек инструкции movaps, которые генерируются компилятором с такими смещениями от стека, в которых предполагается что перед вызовом функции стек был выровнен по 16 байтам согласно x86_64 ABI calling convention (а внутри функции соответственно стек «выровненный» на 8 по модулю 16). А лоадер ядра - при передаче управления в _start - стек естественно выравнивает на 0 по модулю 16. Но это НЕ то что должно быть по ABI вызова функции, и при появлении в коде movaps оперирующей со стеком - он стал сегфолтиться. Потому что в преамбуле функции компилятор вставляет операций с rsp по выделению сохранённых регистров и локальных переменных на 0x????8 байт делая rsp снова кратным 0x10, готовым к работе инструкций вида %xmm0,0xc0(%rsp) и вызову функций. А когда он это делает после лоадера - он наоборот портит выровненность.

Добавление stack_aligned_start решило проблему. Не знаю, не разруливает ли это как-то линкер в случае rust, но если нет - там тоже нужна какая-то вариация «клея», которая сдвинет стек на 8 любым способом.

GPFault ★★★★
() автор топика
Последнее исправление: GPFault (всего исправлений: 3)
Ответ на: комментарий от GPFault

А вот тут не происходит часом той же проблемы что в плюсах?

С этим небыло проблем, но вот write! валился с дефолтной моделью релокации (PIC) при сборке, поставил static - стабильно заработало, может и в плюсах также

zurg ★★
()
  • Markdown
Пустая строка (два раза Enter) начинает новый абзац. Знак '>' в начале абзаца выделяет абзац курсивом цитирования.
Внимание: прочитайте описание разметки Markdown.
Используйте Ctrl-Enter для размещения комментария