LINUX.ORG.RU

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

 , , ,


0

3

Столкнулся с нюансами 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
()
  • Markdown
Пустая строка (два раза Enter) начинает новый абзац. Знак '>' в начале абзаца выделяет абзац курсивом цитирования.
Внимание: прочитайте описание разметки Markdown.
Используйте Ctrl-Enter для размещения комментария