Столкнулся с нюансами 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 байт бинарника» ))






