fk0, легенда (11.12.2019 12:39, просмотров: 852) ответил Constantin24_ на со 2го раза дошло ))). В таком случае в релизе не должно быть printf совсем, а хотелось бы чтобы он был разрешен в одном файле. Кстати поделиться кто реализацией Loggerа? Интересует небольшой объем данных, хранение последних сообщений в озу в
Нормальный логгер, к сожалению, делается только на C++... На C сам язык начинает сильно мешать. Что потенциально можно было бы сделать, и какие есть сложности: 1) основная: язык C не умеет выводить тип данных из, например, аргумента функции и
2) язык C не умеет производить какие-либо вычисления в момент компиляции.
Комбинируя 1 и 2 можно было бы разобрать строки формата и проверить их на соответствие аргументам в момент компиляции, а не тормозить в момент исполнения. Более того, строки формата сами по себе стали бы не нужны. Кроме того, можно было бы реализовать запрет на передачу r-values в качестве параметров логгера -- чтоб при запрете логгирования не лишиться вызова важной функции (идея в том, что передать параметром можно только переменную, которую если присваивали как результат вызова функции, то этот вызов будет вынужденно вынесен в отдельную строчку кода).
С пунктом 1 вообще не так-то просто. Во-первых gcc имеет расширение, оператор typeof(), возвращающий тип из выражения (которое может быть аргументом макроса, например). В C11 есть generics, но я не понял можно ли его использовать. Во-вторых из типов для printf-подобной функции интересуют только int, long, long long, double и потенциально long double (все остальные приводятся к одному из них). В принципе у них sizeof различается, и хотя типы задекларировать нельзя, но этот факт можно использовать для правильного извлечения аргументов со стека по одному (через va_arg). В C++ тип вызываемых функий (как аргументов) можно выводить до C++11 (в C++03) через sizeof(func(x)) или тернарный оператор (0 ? func(x) : template), где func не вызывается, а тип template приводится к результату func(), после C++11 появился наконец-то decltype().
Идея в том, что можно снять со стека все аргументы и сохранить в кольцевом буфере, например. А от строки формата вообще сохранить только её адрес. И потом, когда строчка лога понадобиться: фактически её распечатать. Так быстрей, чем печатать сразу (тем более, что может и не понадобится). Да, это можно сделать в лоб и без извращений, если распарсить строку формата. Но это не быстро делается. А вычислить типы (верней размеры, на основе которых угадать типы) можно с помощью последовательного применения некого макроса к аргументам макроса замещающего printf. Есть известный уже макрос EVAL (рекурсивно исполняющий другие макросы, с ограниченной глубиной рекурсии конечно), есть вариант из boost, с подсчётом числа аргументов и написанием вариантов для одного, двух, пяти, десяти... В итоге последовательное применение одного макроса к аргументам другого вполне реально. Результатом может быть число типа long, которое передаётся в функцию уже, вместе с адресом строки формата, и в котором закодированы размеры всех аргументов, по два бита (2, 4, 8 или 10 байт) на аргумент. До 16 аргументов хватит, а больше не нужно. И следовательно функция знает как все аргументы снять со стека (число аргументов тоже отдельно передавать нужно, из макроса) без разбора строки формата. Вычисления размеров и числа аргументов естественно это выражения рассчитываемые компилятором при компиляции, constexpr в терминах C++. Так что вызов функции потяжелел всего лишь на два лишних константных аргумента, но сильно ускорился по времени.
Есть ещё негатив:
3) функции с переменным числом аргументов не инлайнятся.
С одной стороны, если говорить о компактности кода, вызов vararg функции на самом деле самый компактный способ вызова логгера. Но если говорить о скорости, и если аргументов буквально один-два, то по скорости это может быть не оптимальный вариант. В C++ варианте можно путём увеличения кода конечно, но буквально несколькими инструкциями быстренько класть результат прямо в кольцевой буфер, без всяких копирований по памяти.
Далее, что можно сделать, и о чём я ранее писал на сахаре:
4) строки формата вообще в памяти можно не хранить!
В чём заключается идея высказанная мною ранее: строки формата кладутся в специальную секцию (т.е. у них есть адреса), которая в память потом не линкуется (чем экономится программная память контроллера). Важно, что адрес остаётся и этот адрес потом используется. Но дополнительно, путём препроцессирования исходников макропроцессором m4, получается дополнительная строка формата, в которой остаются спецификаторы типов (%d, %s, %f...), но вырезается весь текст между ними. И вот эта вторая строка формата используется для фактического вызова функции, которая разбирает эту укороченную строку формата и извлекает аргументы со стека, и хранит их в буфере, вместе с адресом оригинальной, отсутствующей в программной памяти строки формата. И по запросу адреса оригинальной строки формата и аргументы (числа, адреса строк находящихся в сегменте .rodata или сами строки если они расположены в другим сегменте) начинают распечатываться в последовательный порт, просто текстом. Их на ПК парсит специальная программа, которая так же имеет ELF-файл прошивки с не удалённой специальной секцией, содержащей полноценные строки формата. И эта программа берёт полноценные строки формата, аргументы, и распечатывает уже в нормальном, понимаемом человеком виде. Такой способ позволяет сильно сэкономить программную память (на 5-10 процентов).
Но данную идею можно развить, комбинировав с тем, что высказано выше под пунктом 2 -- если мы умеем снять со стека (через va_arg с правильным типом, угаданным с помощью sizeof и макросов) все аргументы, то укороченные строки формата и их парсинг в момент исполнения не нужны. Т.е. всё можно сделать ещё быстрей и ещё сэкономить программную память.
Более того, можно передавать в последовательный порт не адрес строки формата, а индекс: адреса, в свою очередь, сложить в другую секцию (тоже не линкуемую) и индекс вычислять из смещения от начала секции. Индексы, правда, не уникальные получатся и будет много дублей, но типичный индекс всё равно меньше бит займёт, чем полноценный адрес.
И наконец самый важный пункт:
5) распечатка enum'ов.
Это реальная проблема. Дело в том, что в сколько-нибудь объёмном программном проекте их будет масса. И зачастую видеть код вместо буквенного значения -- большая проблема для программиста анализирующего логи. Нужно открыть исходники, найти нужный enum, перевести число в имя... В голове для большого проекта не удержать. И хуже того, разные enum'ы зарыты в разных слоях API, искать их тяжело, в разных версиях они *часто* *меняются*, и логи тоже, периодически приходится просматривать от разных версий. Есть такая отговорка, мол сделай скрипт, который enum'ы из логов расшифровывает. Так вот не получится по нескольким причинам: исходники постоянно меняются (нумерация в enum'е), строки формата тоже, в выводимой строке нет указания на то, какого типа enum распечатался. Часто сам факт перевода enum'а в символьный вид даёт понять и то какого типа этот enum, из остального распечатанного текста часто не понятно.
Для C++, даже для C++03 возможно следующее решение: каждый аргумент логгера заворачивается в некоторую функцию tostring(), которая для известных ей enum'ов (а сами эти функции могут определятся в тех же namespaces, где определяются enum'ы -- argument depentent lookup) выдаёт его символьное значение (внутри функции просто switch-case и возврат константной строки). А для неизвестных типов просто возвращается само же значение этого типа, и дальше с ним, условно, printf разбирается. Ну и само собой строку формата нужно оперативно поменять и для случаев, когда подстановка произошла (функция tostring() нашлась) нужно заменить "%d" на "%s".
Таким образом можно распечатывать не только enum'ы но и некоторые другие типы данных. Обычно неудобства доставляют ещё типы хранящие дату-время, в основном time_t (опять же при анализе лога приходится руками пересчитывать в нормальный вид).
В рамках C такое реализовать не представляю как. Собственно нет самого понятия argument dependent lookup, нет понятия перегрузки функций. Да и понятия типа тоже нет, любой enum -- int. Можно, конечно, руками писать tostring(enumvalue), но всех не заставишь, а потом лог опять становится "слишком цифровым" и непонятным для человека.
Кто поделится реализацией -- хочешь что-то получить нахаляву, а сам-то чем поделишься? Увы. Идеи -- даны, и они интересны. А реализация -- это чисто технический момент, это работа программиста, за что зарплату кстати платят. Ты ж зарплатой не поделишься?
Кроме того, обычно заставляют кровью подписываться на двух десятках листов, где слева текст на английском, справа на русском, а за нарушение -- расстрел из ЗСУ-23.
[ZX]