Как стать автором
Обновить

Комментарии 44

Чего только не придумают, чтобы не ходить на овощную базу (с) (анекдот советских времен)

Я правильно понял, что само исключение у вас это структура, в которой есть сообщение, строка где его кинули, и указатель еще на что-то?

Не уловил, можно ли будет посмотреть стектрейс, как скажем в Java, все строки, по которым проходил вызов до точки, где кинули исключение. На мой взгляд, для Java исключений это чуть ли не самое удобное, что дают исключения. Причем без усилий вообще.
Я правильно понял, что само исключение у вас это структура, в которой есть сообщение, строка где его кинули, и указатель еще на что-то?

Да: https://github.com/ProgerXP/SaneC/blob/master/saneex.h#L187


struct SxTraceEntry {
  int   code;
  char  uncatchable;
  char  file[SX_MAX_TRACE_STRING];
  int   line;
  char  message[SX_MAX_TRACE_STRING];
  void  *extra;
};

можно ли будет посмотреть стектрейс, как скажем в Java, все строки, по которым проходил вызов до точки, где кинули исключение

Конечно, в этом и смысл. На КДПВ справа внизу — именно такой stack trace.


Uncaught exception (code 1) - terminating.
Feeling blue today...
    ...at saneex-demo.c:7, code 0
rethrown by ENDTRY
    ...at saneex-demo.c:11, code 1
Bye-bye my little pony
    ...at saneex-demo.c:18, code 0
rethrown by ENDTRY
    ...at saneex-demo.c:19, code 1

Структура доступна в рантайме, ее можно проитерировать через sxWalkTrace():
https://github.com/ProgerXP/SaneC/blob/master/saneex.c#L42


Причем без усилий вообще.

Да, "без усилий вообще" — это как раз "привычный по другим языкам механизм исключений" для меня. saneex это дает из коробки.

Да, понял. Спасибо

Кстати, построение вот этого самого стека, насколько я помню — это как раз то, что в Java сильно снижает производительность при использовании исключений. На хабре даже пару раз упоминали небольшой хак, как все ускорить, кидая исключения без stacktrace.
построение вот этого самого стека, насколько я помню — это как раз то, что в Java сильно снижает производительность при использовании исключений.

Я с низким уровнем в JVM мало знаком, но, как пишут на SO, там используется подход zero-cost exceptions (см. в статье), то есть вся обработка делается в момент поимки исключения (если оно возникает). А Java — высокоуровневый язык с синхронизациями, объектами и прочим, поэтому такая обработка затратна.


В saneex и "голом" С все наоборот — throw это почти что один longjmp(), который, фактически, только сбрасывает указатель стека (ESP). Затраты на этот сброс околонулевые, что показывают мои замеры, по которым throw/longjmp() в C быстрее, чем throw в C++ (где, как и в JVM, дело не ограничивается только изменением ESP).


"Построение стека" происходит по мере вызовов try — там копируются параметры исключения (file, message и пр.) в статический массив, плюс вызывается setjmp(). Как раз последний является лимитирующим фактором, но от него избавиться нельзя никак, не уходя от C99. Но даже там счет идет на единицы-десятки мс при 100к повторений.

Про построение стека интересно — будут ли в нем учтены файлы и вызовы функций по стеку, в которых нет макросов (они ведь макросы?) try-catch? Если да, то как? Если я правильно понял, то они разворачиваются во что-то, использующее FILE (что-то такое), но как провалиться вниз к вызывающему файлу непонятно...

будут ли в нем учтены файлы и вызовы функций по стеку, в которых нет макросов (они ведь макросы?) try-catch?

Не будут, так как новая запись во внутренний стек saneex добавляется только при входе в блок try. Вероятно, можно придумать способ добавлять в него вызовы при входе в любую функцию, но точно не стандартными средствами.


Оданко проблемы в этом нет, так как обычно при возникновении исключения вполне достаточно функций с try/catch внутри, а все промежуточные не интересуют.


Пример:


  1 #include "saneex.h"
  2
  3 int subsub(void) {
  4   throw(msgex("Test"));
  5 }
  6
  7 int sub(void) {
  8   subsub();
  9 }
 10
 11 int main(void) {
 12   try {
 13     sub();
 14   } endtry
 15 }

Вывод:


Uncaught exception (code 1) - terminating. 
Test
    ...at saneex-demo.c:4, code 0
rethrown by ENDTRY
    ...at saneex-demo.c:14, code 1

Как видите, в стеке только функции main (строка 14 с endtry) и subsub (4 с throw). Функции sub (строки 7-9) нет.

Все в порядке с реализацией и полезностью стека, я лишь пытался понять, насколько много черной магии в его построении :) Получается, что не очень! Спасибо за пояснения.

Магия и стандарт не очень совместимы :)

Статью ещё читаю, очень интересно, но несколько перегружена хештегами ссылками. Я уже привык к тому, что если кто-то выкладывает контекстную ссылку — там нечто очень желательное к ознакомлению. Но я уже понял свою ошибку и теперь будет читаться быстрее ))
Я уже привык к тому, что если кто-то выкладывает контекстную ссылку — там нечто очень желательное к ознакомлению.

Ну, 9600 бод и все-все-все вполне попадает в категорию "очень желательно" :)


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

Да, спасибо, просто формат необычный, когда много ссылок.
Жаль на Хабре нет какого-нибудь автоматического сборщика ссылок, например вы ставите контекстную url — она автоматически превращается по випу как в Википедии (маленький номер в superscript со ссылкой), а внизу статьи полный список с расшифровкой.
Мне кажется это идея для Хабра — сделать выбираемый пользовательский стиль, который будет показывать статью либо как оформил автор, либо в википедийном стиле.

Статья, безусловно, в закладки.

Мне тоже предпросмотра ссылок часто не хватает — было бы неплохо, если бы deniskin взял это на вооружение. Хотя, возможно, это лучше решать на уровне плагина в браузере, а не отдельно взятого сайта.

Про более медленную работу fprinf по сравнению с cerr << в общем-то понятно — printf это комбайн на все случаи жизни и под многие типы переменных, ему приходится разбирать строку форматирования. Вывод в cerr, полагаю, пользуется какими-то узкотипизированными форматёрами.
Но несмотря на это я тоже сторонник тёплого лампового fprintf.
Про более медленную работу fprinf по сравнению с cerr << в общем-то понятно — printf это комбайн на все случаи жизни и под многие типы переменных, ему приходится разбирать строку форматирования. Вывод в cerr, полагаю, пользуется какими-то узкотипизированными форматёрами.

Здесь интересно другое (см. таблицу, №3 и сноску): fprintf() в коде бенчмарка работает в 2-3 раза быстрее всегда, кроме одного случая в Visual Studio, когда в цикле выбрасывается исключение — тогда внезапно << начинает работать в 3 раза быстрее fprintf(). Причем это подтвердилось у другого читателя. Я не могу это никак объяснить. И это не повторяется на gcc.

asm'овый код смотрели?
<< — точно еще нюансы с буферизацией вывода (я на них натыкался) — может оптимизатор хитро как-то срабатывает

Насколько понимаю, volatile гарантирует чтение-запись, если потоки находятся на одном процессоре. То есть если вдруг поток перекинули на другой процессор, то надо выполнять семантику release-aquire чтобы обеспечить когерентность кэша. В противном случае есть небольшой шанс прочитать устаревшее значение из кэша на другом процессоре.
НЛО прилетело и опубликовало эту надпись здесь
что значит
предписывает только чтение из памяти
?
НЛО прилетело и опубликовало эту надпись здесь
Для x86/x86_64 внутри контекста отдельного потока модель памяти CPU гарантируется когерентность кешей между процессорами, на каком бы из ядер/процессоров этот поток не исполнялся.
Интересует ситуация, если процесс содержит несколько потоков и какой-то поток перешел на другой процессор. Тогда volitale наверное будет мало…

Насколько я понимаю, кэш в x86/x64 сделан так, чтобы программисту не нужно было о нем думать вообще — его как бы нет. За его корректность отвечает ЦП и переключение между ядрами для программы прозрачно. При использовании нескольких потоков это не отменяет необходимость синхронизации (atomic, критические секции и прочее), но сам кэш при этом всегда остается корректным (с точки зрения программы).


Беглый поиск выдал вот такой вопрос-ответ на SO:


x86 CPUs use a variation on the MESI protocol (MESIF for Intel, MOESI for AMD) to keep their caches coherent with each other (including the private L1 caches of different cores). A core that wants to write a cache line has to force other cores to invalidate their copy of it before it can change its own copy from Shared to Modified state.

You don't need any fence instructions (like MFENCE) to produce data in one thread and consume it in another on x86, because x86 loads/stores have acquire/release semantics built-in. You do need MFENCE (full barrier) to get sequential consistency.

То же самое относится к многопроцессорным системам.

Я ответил именно на этот вопрос. Не важно на каком ядре, не важно на каком процессоре. В пределах контекста исполнения отдельного потока когерентность всех кешей — проблема самого процессора. Для программы число ядер/процессоров прозрачно.
volitale в статье нужен, поскольку, как красочно подметил автор, setjmp может быть возвращена несколько раз, что бы компилятор был готов к такому исходу.
НЛО прилетело и опубликовало эту надпись здесь
volatile предписывает не оптимизировать операции чтения/записи из памяти с данной переменной. И соответственно компилятор вынужден её класть либо в heap, либо в стек. Поскольку её размещение в регистре и как раз и есть оптимизация в рамках той абстрактной машины, которую должен реализовывать язык. Самое близкое, наверное, что бы много не читать: ISO/IEC 9899:201x (N1548) страница 121, замечание 134. В вольном переводе:
Декларация volatile может быть использована для описания объекта, соответствующего memory-mapped порту ввода-вывода или объекту к которому осуществляется доступ из прерываний. Действия на объявленных таким образом переменных не должны быть оптимизированы или переупорядочены, за исключением разрешенных правилами оценки выражений
Заметьте, достаточно одного volatile, чтобы переменная соответствовала чему-то в памяти (т.е. не оптимизировалась размещением в регистре). Про абстрактную машину и правила оценки глава 5.1.2, но там нужно вдумчивое чтение.
НЛО прилетело и опубликовало эту надпись здесь

Имеется в виду, что при наличии volatile не требуется других модификаторов/факторов — факт наличия только volatile является достаточной гарантией, что переменная не попадет в регистр и к ней не будут применены другие описанные оптимизации.

Спасибо за статью. Обожаю такие подробные обзоры и погружения в детали.

Осталось разобраться с управлением памятью…
Спасибо, статья отличная, с разборами кода, с заглядыванием под капот чистого Си.
И с временем публикации тоже все правильно получилось, на фоне потока непрерывных новостей о коронавирусе она вселяет настоящий оптимизм.
«Вирусы приходят и уходят, а Си вечно молодой, вечно пьяный.»
на фоне потока непрерывных новостей о коронавирусе она вселяет настоящий оптимизм.

А я вот смотрю на счетчик просмотров и понимаю, что теперь Хабр уже точно "не торт" :/ Верю и надеюсь, что это временно.

Все будет хорошо, и Хабр вновь будет «торт» ;)
нельзя делать return между try и endtry — это самый жирный минус, но моя фантазия не нашла способов отловить эту ситуацию; принимаются идеи и PR

B gcc можно поставить хук на выход из ф-ции с момщью -finstrument-functions
и так ловить

Имелось в виду использование стандартных переносимых конструкций, как, например, тройное { (как сделано для try и endtry) или #define return abort (но макросы не рекурсивны).

стандартных переносимых конструкций, как, например, тройное { (как сделано для try и endtry) или #define return abort (но макросы не рекурсивны).

А можно про это подробнее, не совсем понял что вы имели ввиду.

Рассмотрим две проблемы из статьи:


блок обязан заканчиваться на endtry

Задача компилятора/среды программирования — максимально разгрузить программиста от контроля за мелочами (синтаксиса, платформы и прочего). В контексте моей библиотеки, может быть такой случай использования:


try {
  func();
} endtry   // <<<

Допустим, код выше (где try и endtry это макросы) разворачивается в такой:


if (xxx)    // "try"
{
  func();
}
if (!handled) ...   // "endtry'

Что будет, если программист забудет endtry?


if (xxx)    // "try"
{
  func();
}

Последней строчки нет. При этом в endtry происходит очистка стека и его пропуск является фатальной ошибкой, которую не поймать даже в рантайме (с точки зрения saneex блок try продолжает быть открытым до завершения процесса). А забыть endtry очень легко.


Поэтому в saneex (см. исходники):


#define try     {{{ if (_sxEnterTry2( setjmp(*_sxEnterTry()) ))
#define endtry  _sxLeaveTry(__FILE__, __LINE__); }}}

Обратите внимание на {{{ и }}}. Теперь, если пропустить endtry, код будет таким:


{{{ if (xxx)    // "try"
{
  func();
}

Это, очевидно, является синтаксической ошибкой, о чем компилятор сразу предупредит. Сравните с:


{{{ if (xxx)    // "try"
{
  func();
}
if (!handled) ... }}}  // "endtry"

Забыть endtry и не заметить этого теперь можно только если пропустить 3 закрывающие скобки помимо собственно endtry, а это сложно сделать.


нельзя делать return между try и endtry

Теперь такой пример:


try {
  return func();
} endtry

Это является точно такой же фатальной ошибкой, т.к. вызывает повреждение стека. Скобки нам уже не помогут, потому что endtry на месте. Можно было бы сделать что-то подобное:


#define try    #define return abort();

И тогда:


try {
  abort(); func();
} endtry

В этом случае в рантайме при попытке выполнить такой блок гарантированно получили бы падение. Еще можно было бы #define return $#@!, чтобы вызвать синтаксическую ошибку — однако макросы не раскрываются рекурсивно, так что это не сработает и за пропущенным return, в отличии от endtry, приходится следить программисту.


Вот это я и имел в виду.

Спасибо за развернутый ответ!
Еще можно было бы #define return $#@!, чтобы вызвать синтаксическую ошибку — однако макросы не раскрываются рекурсивно, так что это не сработает и за пропущенным return, в отличии от endtry, приходится следить программисту.

Рекурсия не получится но можно использовать include
К примеру:
test.c:
#define TRY #include "inc.h"

int main(){
TRY
return 0;
}


inc.h:

#define return $#@!

gcc -E test.c:

int main(){
 #include "inc.h"
return 0;
}

Попробуйте скомпилировать #define с # внутри, без -E — у меня ругается:


error: stray ‘#’ in program
#define TRY #include ...
Да, не сделать такой финт, увы
stackoverflow.com/a/30465750

Спасибо за статью и стиль изложения.
Единственное что смутило: int i{ vec.at(4) }; — это C++ головного мозга?

Единственное что смутило: int i{ vec.at(4) }; — это C++ головного мозга?

Это пример с Википедии в чистом виде. Не знаю, почему они написали именно так, а мне просто нужен был "независимый" пример использования исключений для бенчмарка и этот вполне подошел.

Спасибо за пояснения на асме. Сейчас даже дизасемблированный или сгенереный код глаза упорно не хотят смотреть, а мозг нашептывает «забить на динозавра». Так скоро совсем его забуду…

"Диназавр" на сегодня это единственное человекочитаемое представление того, что, собственно, происходит внутри ЦП, так что вымирание ему не грозит. На многие вопросы однозначный ответ дает только ассемблер, иначе такое "исследование" будет сродни лечению по фотографиям.


Да и полезная разминка для мозгов, коим грозит тотальное заржавление из-за этих ваших "питонов" и прочих "жабоскриптов" :) </irony

Можно ловить двойные исключения из деструкторов, которые считаются, что они смертельны для программы. Небольшая утечка возможна, но программа продолжает работать.

    std::set_terminate([](){
        std::cout << "Unhandled exception "<<" uncaught_exceptions["<<std::uncaught_exceptions()<<']'<<"  \n" << std::flush;
        cout<<"begin long jump\n";
        longjmp(jmpBuf,1);
    });

    if(setjmp(jmpBuf)==0) {
        
            try {
                .......  // <<<  возникает двойное исключение в деструкторе
            } catch (...) { 
                cout << "catch\n"; // сюда не доходит, а идет в set_terminate
            }
        

    }else{
        cout<<"longjump completed";
    }
    

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации