Как стать автором
Обновить
139.7
Карма
0
Рейтинг
Павел @ProgerXP

Всеядный вольный программист и дизайнер

  • Подписчики 269
  • Подписки 2

saneex.c: try/catch/finally на базе setjmp/longjmp (C99) быстрее стандартных исключений C++¹

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


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

saneex.c: try/catch/finally на базе setjmp/longjmp (C99) быстрее стандартных исключений C++¹

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

saneex.c: try/catch/finally на базе setjmp/longjmp (C99) быстрее стандартных исключений C++¹

будут ли в нем учтены файлы и вызовы функций по стеку, в которых нет макросов (они ведь макросы?) 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) нет.

saneex.c: try/catch/finally на базе setjmp/longjmp (C99) быстрее стандартных исключений C++¹

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

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

saneex.c: try/catch/finally на базе setjmp/longjmp (C99) быстрее стандартных исключений C++¹

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


error: stray ‘#’ in program
#define TRY #include ...

saneex.c: try/catch/finally на базе setjmp/longjmp (C99) быстрее стандартных исключений C++¹

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


блок обязан заканчиваться на 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, приходится следить программисту.


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

saneex.c: try/catch/finally на базе setjmp/longjmp (C99) быстрее стандартных исключений C++¹

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

saneex.c: try/catch/finally на базе setjmp/longjmp (C99) быстрее стандартных исключений C++¹

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

saneex.c: try/catch/finally на базе setjmp/longjmp (C99) быстрее стандартных исключений C++¹

на фоне потока непрерывных новостей о коронавирусе она вселяет настоящий оптимизм.

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

saneex.c: try/catch/finally на базе setjmp/longjmp (C99) быстрее стандартных исключений C++¹

Насколько я понимаю, кэш в 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.

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

saneex.c: try/catch/finally на базе setjmp/longjmp (C99) быстрее стандартных исключений C++¹

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

saneex.c: try/catch/finally на базе setjmp/longjmp (C99) быстрее стандартных исключений C++¹

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

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

saneex.c: try/catch/finally на базе setjmp/longjmp (C99) быстрее стандартных исключений C++¹

Я уже привык к тому, что если кто-то выкладывает контекстную ссылку — там нечто очень желательное к ознакомлению.

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


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

saneex.c: try/catch/finally на базе setjmp/longjmp (C99) быстрее стандартных исключений C++¹

построение вот этого самого стека, насколько я помню — это как раз то, что в Java сильно снижает производительность при использовании исключений.

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


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


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

saneex.c: try/catch/finally на базе setjmp/longjmp (C99) быстрее стандартных исключений C++¹

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

Да: 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 это дает из коробки.

«Прокачиваем» notepad.exe

Есть ещё Notepad 2e с ещё большим количеством плюшек типа подсветки слов.

Ваш wi-fi расскажет мне, где вы живёте, где работаете и где путешествуете

Ага. Сделали. Только они не совсем рандомные, не всегда включаются и вообще — почти не работают. Пруф.

Не учите фреймворки, учите архитектуру

А если честнее – потому что не видят альтернативы. А альтернатива есть, просто о ней забыли.

Яростно плюсую, именно то, о чём я постоянно говорю коллегам. А альтернатива есть — "Backbone без фантиков", когда на единственном принципе строится вся библиотека. Как его повернёшь — то и получишь: наследование, привязки, наблюдателе, всё прочее… Просто, компактно.

Полезная статья, буду отсылать на неё несогласных.

Дюжина дизайнерских косяков

А как мелкое, но полезное (для кого-то) дополнение к более наглядным подсказкам — пусть будет!

Согласен. Вопрос исчерпан, спасибо за обсуждение :)

Дюжина дизайнерских косяков

Какой холиварный сабж оказался :)

Согласен, что сейчас четкой грани нет, мой предыдущий коммент скорее пытался пояснить исторические корни этой разницы.

Так ведь формы были ещё тогда, когда у элементов не было даже :hover. Точно во времена CSS1. ИМХО, тут создатели смотрели в первую очередь на десктопные интерфейсы, где, действительно, только особых случаях использовалась «рука» (как сейчас помню — в ссылках Acrobat Reader'а).

Кстати, ссылки на другие языки в той выпадушке нормальные, их можно мидл-кликнуть в новую вкладку и т.д.

Я тоже заметил, но тем сильнее мой аргумент про то, что раз никто (даже Яндекс с его экстазом по поводу интерфейсов) не делает 100% правильно и последовательно, то нет смысла к этому стремиться. Только путает.

И у меня до сих пор осталось твердое убеждение, что нужны и ссылки на объекты («существительные»), и кнопки для действий («глаголы»), которые никогда не смогут заменить друг друга и визуально им тоже лучше быть разными.

Это интересно, но всё равно не понимаю, почему их недостаточно просто сделать разными визуально, без изменения курсора при наведении по-разному. Опять же, на планшетах его нет — проблемы тоже нет (?), тогда зачем это городить для десктопов?

Визуальные подсказки — как раз ответ, ИМХО. А курсор — уже так, дань традиции. Суровый верстальщик будет делать, как глаголет спецификация, выражая этим своё отношение к сабжу наподобие того, как некоторые технические переводчики пытаются найти английским словам русский эквивалент, а какие-то просто делают транслит (browser = браузер = обозреватель). Тоже холивар ещё тот.

У меня опасение, что такая «забота о пользователе» через переупрощение

Не буду спорить. Меня сейчас ни сколько не радует тотальная унификация (в этом контексте — обезличивание) интерфейсов с отпиливанием всего и вся, поэтому я б предпочёл описанное вами и стандартом поведение, и чтобы пользователи тоже его понимали, но сейчас идёт к тому, что пользователь перестанет понимать вообще что бы то ни было и разница между рукой и стрелкой для него будет просто верхом мыследеятельности.

Информация

В рейтинге
Не участвует
Откуда
Россия
Зарегистрирован
Активность