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

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

Благодарю за статью. Интересно, как обстоят дела с обработкой исключений в коде, который собрал clang. Есть ли отличия от gcc?
Никогда плотно с clang-ом не общался, но, насколько я знаю, формат выполняемого файла тот же и те же sjlj/dw2 в нем реализованы.
Советую кстати почитать еще по поводу personality функций, посредством которых реализуется поддержка общего формата исключений в едином рантайме для нескольких языков. Так например происходит в случае пары C++ и C. Последний, хоть ислючения и не поддерживает напрямую, тоже вынужден иметь свою personality функцию для соединения.

P.S.: У нас в проекте llst (см. мои статьи) с помощью personality функций реализуется операция block return и единое пространство исключений для Smalltalk и C++ Native API (а также бакенда VM).
Спасибо! Конечно, прочту.
Собственно, да. Cross-language взаимодействие — это одна из немногих причин, почему все реализовано именно так, а не иначе. Исключения в разных языках разный, и тут нужен был некоторый «общий знаменатель».
Тип исключений — часть platform ABI, т.к. нам, очевидно, нужно уметь ловить внешние исключения. Поэтому там sjlj для arm/darwin (сугубо по историческим причинам) и dw2 везде дальше. Стоит, кстати, отметить, что 32-битные SEH-based (используемые на msvc под win32) исключения запатентованы Borland (http://www.google.com/patents/US5628016), что препятствует реализации их где бы то ни было «за так». Хорошие новости состоят в том, что патент заканчивается в июле этого года :)
Не понял причем тут GC. В момент выброса исключения требования к объектам очень жесткие, если соблюдать те же требования в любом другом языке с GC то будет тот же результат.
А… GC это что?
garbage collector
Действительно, а причем здесь GC.
>>Аккуратное использование стековых объектов позволяет создавать очень эффективный и безопасный код, где, в отличие от систем со сборкой мусора, сохраняется локальность ссылок, что дает возможность уменьшить число обращений к системе за памятью, снизить её фрагментацию, более эффективно использовать кэш памяти.

Я вот это комментировал.
Виноват, не понял. Сборка мусора — альтернативный метод автоматического освобождения ресурсов. Со своими плюсами и минусами.
В действительности же, компилятор поступает весьма рационально. Он разворачивает setjmp, причем, сохраняет только полезные регистры (уж эта информация у него есть). Автор сомневается, что издержки на setjmp так уж высоки.

В действительности же setjump как правило реализован с помощью большого макроса с inline assembler'ом внутри. И у компилятора почти нет никакой возможности сохранить только полезные регистры. Конечно, компилятор не вызывает setjmp (по крайней мере, ни gcc, ни clang) напрямую. У него есть своя внутренняя реализация, которая по сути аналогична вызовам setjmp / longjmp. Правда в том, что «раскрутка» этих внутренних реализаций в реальный код осуществляется уже в backend'е, где почти нет никаких оптимизации. Так что тут тоже пролет. sjlj исключения очень тяжелы на каких-нибудь SPARC'ах с их огромным регистровым набор (и регистровыми окнами).
Но ведь это происходит после назначения регистров, так что формально вся информация у компилятора есть. Может, сочли что овчинка выделки не стоит.
Штука вся в том, что обычно это происходит до назначения регистров. Это нужно как раз из-за того, что код сохранения может быть достаточно нетривиален и вызвать пару-тройку spill'ов, вообщем, вообще говоря, изменить аллокацию регистров. И если делать все после, то было бы очень неприятно «патчить» уже готовое распределение так, чтобы его не испортить.
Я выскажусь в защиту clang. setjmp / longjmp являются интринсиками, т.е. являются частью LLVM IR, а не реализованы с помощью макросов. И как раз в backend'e осуществляются все оптимизации(так называемые проходы).
wearing my llvm developer hat on

В бекенде (по сравнению с middle-end'ом) больших оптимизаций почти нет. Если кому-то интересны детали реализации sjlj в LLVM, то цепочка следующая (только для arm/darwin, sjlj больше нет нигде):

1. Интринсик превращается в SelectionDAG node EH_SJLJ_SETJMP (общий код в target independent backend)
2. Дальше, EH_SJLJ_SETJMP превращается в ARM-specific EH_SJLJ_SETJMP node'у (код в ARM backend'е)
3. Ну а эта node'а превращается в псевдо-инструкцию, которая раскрывается в большой код, который одновременно сохраняет все регистры в буфер определенного формата и еще выводит кучу другого кода.

wearing my llvm developer hat off
У нас расхождение в терминалогии =) Я считал, что обработка IR и генерация машинозависимого кода и есть backend, но в парочке статей нашёл робкое «middle layer».
Пользуясь случаем, позвольте немного поофтопить. Вы случайно не в курсе, работает ли кто-то сейчас над shadow-stack-gc? В нем есть некоторое количество неприятных багов/неточностей, которые нам в llst пришлось обходить. В частности: использование __gcc_personality_v0 вместо желаемой gxx и слишком поздняя инициализация подсистемы shadow stack в случае с чисто JIT модулем (насколько я помню, обработка LLVM валился с сегфолтом, если shadow stack pass включен в pass manager, но в модуле нет еще ни одной функции с атрибутом gc «shadow-stack» ).

Я все это говорю к тому, что может быть, имеет эти задачи взять нам с humbug? Принимаете ли вы патчи со стороны и насколько охотно?
Насколько я знаю, никто сейчас не работает и не собирается. Что касается «патчей со стороны», то такого понятия нет :) Any patches are more than welcome. Некоторое количество полезной информации есть вот тут: llvm.org/docs/DeveloperPolicy.html#making-and-submitting-a-patch
Посмотрите еще на реализацию исключений в MSVC на x64 — она с нулевым оверхедом в случае отсутствия исключений.
Когда я изучал этот вопрос, в gcc все-таки цена не возникающих исключений была не совсем нулевая (для x86) — не допускалась оптимизация -fomit-framepointer. А сейчас эта оптимизация поддерживается?
В версии GCC 4.4 — точно поддерживается, про более ранние — не знаю, уже давно не общался.
framepointer'ы нужны отладчику, для раскрутки стека используются другие механизмы.
Тут, например, этот флаг используется совместно с исключениями.
Незнаю как на x86/x64 но на Arm GCC/SJLJ дает жуткий оверхед по размеру кода (30-50%) даже если исключения вообще не кидать (т.е. достаточно просто скомпилировать С++ код с поддержкой исключений). Никак не могу назвать такой подход «пусть неудачник платит», там платят все и всегда, причем так прилично что на дремучих embedded их тупо отключают.

В этом плане GCC/DW2 более полезен так как не генерит такого оверхеда (по % сейчас не скажу, но занчительно меньше) — как написано для раскрутки стека там используются сжатые таблицы плюс динамической дизасемблирование/анализирование самих фреймов для получения недостающей информации. И то что данные раскрутки находятся отдельно от полезного кода — неоспоримый плюс — кеш процессора меньше захламляется тем что в теории должно редко использоватся. То что сама раскрутка становится медленней имхо проблемой не является, не в этом смысл исключений крутить их в цикле и ожидать реактивной производительности.

Кстати говоря раскрутка стека в коде нужна не только для C++ а например для красивых краш-дампов или анализа утечек памяти. В этом плане GCC/DW2 полезней GCC/SJLJ хотя бы тем что может использоватся в C для улутшений диагностики кода. Тот-же valgrind помоему раскручивает стек на ARMе через GCC/DW2 и если его нет с раскруткой стека начинются серьезные проблемы (на ARMе).
Так и было описано в статье про SJLJ, разве нет? Для ARM он был как-то особо неудачно реализован, не зря в android ndk изначально исключения вообще не поддерживались, пока arm/dw2 был сыроват
Автор написал что «SJLJ Относится скорее к первому подходу» что меня смутило, невнимательно читал (скорее). SJLJ наверно относится к подходу «хотели как лутше а получилось как всегда» ).
MS VC++. Этот компилятор реализует вторую стратегию обработки… Для версии x64 вспомогательные стековые структуры по возможности переносились в .pdata, вероятно, в MS считают первую стратегию более перспективной.
А вот и нет. Следует различать x86 компилятор и x64 компилятор. x86 действительно использует вторую стратегию, а вот x64 — первую. Причем первая стратегия существенно отличается от используемой в GCC компиляторе: GCC использует таблицы, в MSVC вставляет NOP операции в код, которые практически бесплатные, т.к. процессоры их игнорируют. NOP используется как метки при раскрутке стека, чтобы позвать соответствующий деструктор.

Инициирование исключения сделано через программное прерывание.
Непонятно, что это вообще такое? А вообще MSVC использует SEH — structured exception handling.

А вообще я ожидал здесь увидеть сравнение производительности для различных компиляторов, а в результате обнаружилось достаточно поверхностное обсуждение подходов.
По вашей ссылке — описание древнего Linux 2.4. Ну можно было еще дать ссылку на википедию:

MS-DOS API:

Most calls to the DOS API are invoked using software interrupt 21h (INT 21h).

Только какое это имеет отношение к обработке исключений в Windows мне не очень понятно. Может проясните? Собственно, изначально и был вопрос про то, какая связь между программным прерыванием и exception handling. Мне всегда казалось, что используется SEH для этого.
_CxxThrowException передает управление операционной системе через программное прерывание. Это с одной стороны.
А с другой, в MS VC++(32) стек контекстов процедур реализован через общий с SEH регистр FS[0]. Какая-то путаница, видимо произошла.
_CxxThrowException передает управление операционной системе через программное прерывание.
Тут несколько вещей непонятно:
1. Зачем _CxxThrowException передавать управление операционной системе? Тут имеется в виду ядро, или что-то другое? Вроде как исключение можно полностью обработать в user space.
2. Управление операционной системе уже давно не передается через программное прерывание, а через SYSENTER/SYSEXIT или SYSCALL/SYSRET. Или тут рассматриваются древние процессоры с древними операционными системами?
Да, SYSENTER/SYSEXIT.
Да, SYSENTER/SYSEXIT.
Хм, не совсем понял. Либо SYSENTER (что есть Fast call to privilege level 0 system procedures), либо программные прерывания. Т.е. сначала утверждается, что программное прерывание, а потом — что нечто другое. Как это понимать?

И что по поводу первого вопроса о том, зачем нужно переходить в ядро?
Ну что же делать, если «управление операционной системе уже давно не передается через программное прерывание». Кроме int 03, пожалуй, которая стоит особняком из за своей однобайтовости.
А передать хочется. Зачем — отдельный вопрос и не совсем ко мне. Видимо, для удобства отладки.
Пожалуй, да. Поправил текст.
Меня ввела в заблуждение фраза "_CxxThrowException passes control to the operating system (through software interrupt, see function RaiseException) passing it both of its parameters" отсюда.
Конечно поверхностное, для полноценного вскрытия темы потребовалось бы написать «Войну и мира» или «Хождение по мукам», что было бы интересно только автору (ну и еще паре человек).
Что касается сравнения компиляторов, не очень понятно что именно сравнивать, вот есть такое сравнение, бессмысленное и беспощадное.
Исключения в нынешнем виде нельзя использовать для передачи управления, сравнивать как быстро программы восстанавливаются после сбоев?
Объем кода и pdata? Что именно Вам хочется сравнить?
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.