Что же там такого тяжелого в обработке исключений C++?

    image
    Исключения и связанная с ними раскрутка стека – одна из самых приятных методик в C++. Обработка исключений интуитивно понятно согласуется с блочной структурой программы. Внешне, обработка исключений представляется очень логичной и естественной.

    Аккуратное использование стековых объектов позволяет создавать очень эффективный и безопасный код, где, в отличие от систем со сборкой мусора, сохраняется локальность ссылок, что дает возможность уменьшить число обращений к системе за памятью, снизить её фрагментацию, более эффективно использовать кэш памяти.

    Тем не менее, в C++, исключения традиционно рассматриваются буквально как исключительные ситуации, связанные с восстановлением после ошибок. Трудно сказать, является ли это причиной или следствием того, что реализация обработки исключений компиляторами чрезвычайно дорога. Попробуем разобраться почему.

    Продолжение здесь.
    Как обстоят дела.

    Существует два подхода к реализации обработки исключений:
    • Первый можно охарактеризовать словами – “пусть неудачник платит”. Компилятор старается минимизировать издержки в тех случаях, когда исключения не возникают. В идеале, программа не несет никакой дополнительной нагрузки, вся необходимая информация расположена в стороне от кода в некотором удобном для раскрутки виде.
    • Вторая стратегия – “понемногу платят все и всегда". Иными словами, в процессе работы программа несет определенные издержки на поддержание актуальности информации, необходимой для корректной раскрутки стека. При этом, в случае возникновения исключения, раскрутка стека обходится дешевле.

    Несколько примеров:
    • GCC/SJLJ. SJLJ есть сокращение от setjmp/longjmp. Относится скорее к первому подходу, но благодаря схеме передачи управления, в нем есть еще и фиксированная плата за каждый try. В сущности, данная реализация вобрала в себя всё худшее, что присуще обоим подходам. Вплоть до четвертой версии, это был основной вариант обработки исключений.
      Как явствует из названия, передача управления осуществляется через вызов longjmp, каждый try порождает вызов setjmp. Соответствующий буфер выделяется в стеке для каждого try блока.
      В начале каждой функции создается пролог, который регистрирует текущий фрейм в стеке контекстов. Аналогично, создается эпилог, удаляющий текущий контекст с вершины стека контекстов. Рядом с каждой функцией создается вспомогательный код для очистки ресурсов.
      Если точнее, компилятор каждый возврат из функции, потенциально способный завершиться исключением, заносит в дерево как ключ, значением является указатель на код очистки, который надо в этом месте предпринять для очистки контекста функции. Линкер собирает куски этих деревьев для каждого модуля линковки в единое дерево проекта (очень грубо). Называется это чудо LSDA (language specific data area) и расположено оно в секции ".gcc_except_table".
      При возникновении исключения, на основании type_info возбуждаемого исключения отыскивается блок, который может это исключение обработать. Начиная с текущего контекста и вплоть до контекста-обработчика (с помощью навигации по фреймам вызовов), извлекаются и выполняются адреса кода, который (в зависимости от областей видимости локальных переменных) надо исполнить именно в этом месте. После чего передается управление.

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

      А вот что действительно бросается в глаза, так это объемный вспомогательный код, особенно в нетривиальных случаях. Компилятор, подобно YACC, расписывает все состояния стекового автомата. И, хотя, оптимизатор по — возможности вычищает избыточность и тривиальный код, того, что остается, более чем достаточно.
    • GCC/DW2. Это как раз пример первого подхода к обработке исключений. DW2 означает DWARF2(теперь уже и 3) – формат хранения вспомогательной, в том числе отладочной информации в исполняемом файле. Ведь отладочная информация нужна и для того, чтобы в любой момент можно было узнать значение любой переменной, в том числе и во фреймах предыдущих (верхних) вызовов. Поэтому компилятор в процессе генерации кода откладывает информацию о том, что он выделяет в стеке, в каких регистрах размещает переменные, когда их сохраняет… В действительности, этот формат не идентичен DWARF, хотя и очень близок к нему. Стандартный вариант для четвертой версии GCC.

      Концептуально, на каждый адрес кода программы хранится информация о том, как попасть в вышестоящий фрейм вызова. На практике ввиду объемности этой информации, она сжимается, фактически, вычисляется с помощью интерпретации байт-кода. Этот байт-код исполняется при возникновении исключения. Расположено всё это в секциях ".eh_frame" и ".eh_frame_hdr".
      Да, помимо всего прочего, DWARF интерпретатор представляет собой отличный backdoor, с помощью которого, подменив байт-код, можно перехватить исключение и отправить его на обработку куда душе угодно.
      GCC/DW2 использует практически такую же секцию LSDA, что и GCC/SJLJ.

      Как мы видим, издержки, связанные с раскруткой стека (в отсутствие исключений) практически отсутствуют. Однако, стоимость возбуждения исключения велика. Кроме того, нельзя не отметить сильную интеграцию архитектурно-зависимой частью компилятора и достаточно высоко-уровневыми его слоями.
    • MS VC++. Этот компилятор реализует вторую стратегию обработки.
      • Для каждой функции, которая потенциально может выбрасывать исключения, компилятор создает в качестве стековой переменной структуру из указателя на предыдущую подобную структуру, адреса функции-обработчика и вспомогательных данных. Адрес этой структуры заносится в FS:[0], который является вершиной стека этих структур. Регистр FS в Win32 используется как Thread Information Block (TIB) (GS в Win64). Создается также функция-обработчик (со своим набором данных) и эпилог, который восстанавливает FS:[0] в случае успешного завершения.
      • Компилятор создает таблицу структур – по элементу для каждого try-блока в функции. Каждый try-блок имеет индекс начала и конца в этой таблице (вложенный блок имеет вложенный интервал), соответствующий некоторому состоянию, за актуальностью этого индекса сам компилятор и следит. Таким способом компилятор реализует стек try-блоков.
      • На каждый try-блок заводится таблица catch-блоков. На каждый тип исключения заводится таблица type_info всех базовых классов в иерархии данного типа исключения.
      • Для каждой функции создается unwind таблица, каждый элемент которой содержит указатель на функцию, освобождающую некоторый ресурс и номер предыдущего элемента. В таблице может быть несколько цепочек, в зависимости от областей видимости объектов с деструкторами. В момент исключения, по индексу текущего состояния, который упоминался выше, можно найти необходимую цепочку и вызвать все необходимые деструкторы.
      • Для версии x64 вспомогательные стековые структуры по возможности переносились в .pdata, вероятно, в MS считают первую стратегию более перспективной.
      • При инициирование исключения основная работа проводится операционной системой, куда управление попадает через SYSENTER.
      Данному методу присущи те же недостатки, что и SJLJ – обширный вспомогательный код и низкая переносимость.
    • Процесс возбуждения исключения и выбора подходящего catch блока везде выглядит примерно одинаково:
      • При возбуждении исключения создается его описатель, в котором содержатся копия объекта, его type_info, указатель на деструктор
      • Поднимаясь по стеку try блоков и очищая за собой все зарегистрированные стековые объекты, (навигация по этому стеку везде разная, но суть одна), просматриваем списки catch блоков и ищем подходящий.
      • Если подходящий catch блок найден, объект-исключение становится локальной переменной, вызываем этот блок. Если catch блок принимает исключение по значению, а не ссылке, создастся его копия.
      • Если перевызова исключения не было, убиваем объект — исключение
      • «Хозяйке на заметку»:
        some_exception exc("oioi");
        throw exc;
        порождает лишний конструктор копирования / деструктор
        throw *new some_exception("oioi");
        дает утечку памяти
        catch(some_exception exc) ...
        опять лишний вызов конструктора и деструктора
        catch(const some_exception *exc) ...
        исключение пролетит мимо, если не бросить именно указатель
        throw some_exception("oioi");
        ...
        catch (const some_exception &exc)....
        
        минимум издержек
    Подробности можно посмотреть здесь, здесь и здесь.

    А что, если ...
    А, казалось бы, всего и дел то – вызвать в нужном порядке деструкторы, тела которых уже существуют. Как же случилось, что простая, в общем-то, задача имеет такие вязкие, тяжеловесные и притом независимо развивавшиеся решения? Трудно сказать, так исторически сложилось.
    Попробуем набросать решение, стараясь оставить его простым и по возможности архитектурно-независимым.
    • Первым делом выбираем стратегию — это будет второй вариант.
    • Передача управления – setjmp/longjmp
    • Создаем структуру, все потомки которой обладают способностью само — регистрироваться для возможной раскрутки.
      struct unw_item_t {
          unw_item_t ();
          virtual ~unw_item_t ();
          void unreg();
          unw_item_t  *prev_;  
      };
      
    • И еще одну, областью видимости которой является try-блок
      struct jmp_buf_splice {    
          jmp_buf_splice ();
          ~jmp_buf_splice ();    
          jmp_buf         buf_;    
          jmp_buf_splice *prev_;    
          unw_item_t      objs_;  
      };
      
    • Для простоты, будем бросать только исключения типа const char * с помощью
          extern int throw_slice (const char *str);
      
    • Несколько макросов для имитации try-блока
      // начало блока
      #define TRY_BLOCK { \
        jmp_buf_splice __sl; \
        const char *__exc = (const char *)setjmp (__sl.buf_); \
        if (NULL == __exc) {
      ...
      // что-то вроде catch(…) т.к. мы бросаем только const char*
      #define CATCH_BLOCK_FIN  \
        } else { 
      ...
      // конец блока
      #define FIN_BLOCK  \
          } \
        }
      ...
      // бросаем исключение 
      #define THROW_IN_BLOCK(exc)  \
        throw_slice (exc); 
      ...
      // перебрасываем исключение наверх, __exc определено в TRY_BLOCK
      #define RETHROW_IN_BLOCK  \
        throw_slice (__exc); 
      
    • Теперь покажем тела членов класса jmp_buf_splice:
      static jmp_buf_splice *root_slice_ = NULL;  
      jmp_buf_splice::jmp_buf_splice ()
      {
        objs_ = NULL;
        prev_ = root_slice_;
        root_slice_ = this;
      }
      jmp_buf_splice::~jmp_buf_splice ()
      {
        root_slice_ = prev_;
      }
      
      Здесь приведен вариант для однопоточной реализации. При наличии нескольких потоков, вместо root_slice_ мы должны будем использовать TLS, аналогично тому, например, как это делает GCC.
    • Пришла пора для членов класса unw_item_t:
      unw_item_t::unw_item_t ()
      {
        if (NULL != root_slice_) 
        {
            prev_ = root_slice_->objs_;
            root_slice_->objs_ = this;
        }
      }
      unw_item_t::~unw_item_t ()
      {
        unreg();
      }
      unw_item_t::unreg ()
      {
        if (NULL != root_slice_ && 
          (prev_ != reinterpret_cast<unw_item_t *>(~0))) 
        {
            root_slice_->objs_ = prev_;
            prev_ = reinterpret_cast<unw_item_t *>(~0);
        }
      }
      
    • Теперь рассмотрим процесс возбуждения исключения и раскрутки стека:
      static int pop_slice ()
      {
        jmp_buf_splice *sl = root_slice_;
        assert (NULL != sl);
        root_slice_ = sl->prev_;
        return 0;
      }
      int throw_slice (const char *str, bool popstate)
      {
        if (NULL == str)
          return -1;
        jmp_buf_splice *sl = root_slice_;
        unw_item_t *obj = root_slice_->objs_;
        while (NULL != obj)
          {
            unw_item_t *tmp = obj;
            obj = obj->prev_;
            tmp->~unw_item_t ();
          }
        if (popstate)
          pop_slice ();
        longjmp (sl->buf_, int(str));	
        return 0;
      }
      
    • Сервисный класс – аналог std::auto_ptr:
        template<typename cl>
        class deleter_t : public unw_item_t {
        public:
          deleter_t (cl *obj){ptr_ = obj;};
          virtual ~deleter_t () {delete ptr_;};
        private:
          cl *ptr_;
       
          deleter_t ();
          deleter_t (const deleter_t &);
          deleter_t &operator= (const deleter_t &);
        };
      
    • Сервисный класс – массив:
      template<typename cl>
        class vec_deleter_t : public unw_item_t {
        public:
          vec_deleter_t (cl *obj){ptr_ = obj;};
          virtual ~ vec_deleter_t () {delete [] ptr_;};
        private:
          cl *ptr_;
          vec_deleter_t ();
          vec_deleter_t (const vec_deleter_t &);
          vec_deleter_t &operator= (const vec_deleter_t &);
        };
      
    • Примеры.
      Тестовый класс
      class _A {
      public:
      _A():val_(++cnt_){printf ("A::A(%d)\n",val_);}
      	_A(int i):val_(i){printf ("A::A(%d)\n",val_);}
      	virtual ~_A(){printf ("A::~A(%d)\n",val_);}
      static int cnt_;
      };
      int _A::cnt_ = 0;
      class A : public unw_item_t, _A {};
    • Пример 1
      A a(1);
        TRY_BLOCK {
      	A b(2);
      	THROW_IN_BLOCK("error\n");
            std::cerr << "notreached\n";
        }
        CATCH_BLOCK_FIN {
            std::cerr << __exc;
        }
        FIN_BLOCK;

      A::A(1)
      A::A(2)
      A::~A(2)
      error
      A::~A(1)
    • Пример 2
      A a(1);
        TRY_BLOCK {
      	A b(2);
      	TRY_BLOCK {
      	  A c(3);
      	  THROW_IN_BLOCK("error\n");
      	  std::cerr << "notreached\n";
      	}
      CATCH_BLOCK_FIN {
      	  std::cerr << "." << __exc;
      	  RETHROW_IN_BLOCK;
      	}
      	FIN_BLOCK;
            std::cerr << "notreached\n";
          }
        CATCH_BLOCK_FIN {
            std::cerr << ".." << __exc;
          }
        FIN_BLOCK;
      

      A::A(1)
      A::A(2)
      A::A(3)
      A::~A(3)
      .error
      A::~A(2)
      ..error
      A::~A(1)
    • Пример 3
        TRY_BLOCK {
          vec_deleter_t<_A> da(new _A[3]);
          TRY_BLOCK {
      	THROW_IN_BLOCK("error\n");
      	std::cerr << "notreached\n";
          }
          CATCH_BLOCK_FIN {
            std::cerr << "." << __exc;
      	RETHROW_IN_BLOCK;
          }
          FIN_BLOCK;
          std::cerr << "notreached\n";
        }
        CATCH_BLOCK_FIN {
            std::cerr << ".." << __exc;
          }
        FIN_BLOCK;
      

      A::A(1)
      A::A(2)
      A::A(3)
      .error
      A::~A(3)
      A::~A(2)
      A::~A(1)
      ..error

    Ограничения
    Такое решение обладает массой недостатков:
    • Нельзя бросать исключения в деструкторе. Деструктор unw_item_t еще не удалил ссылку на данный экземпляр, в результате деструктор будет вызван повторно.
    • Создавать объект наследованного от unw_item_t класса посредством оператора new очень опасно. Даже, если о памяти заботиться самому, такой указатель может попасть в чужой контекст или даже в чужой поток, у объекта, на который он смотрит, могут неожиданно вызвать деструктор, что кончится метаболической катастрофой.
    • Класс, наследованный от unw_item_t, не может быть агрегирован как член другого класса, иначе его деструктор вызовется дважды.
    • Описанный метод невозможно интегрировать с аппаратными исключениями.
    • Ограничения на типы исключений. Выше мы использовали только строковый указатель. Если передавать в качестве исключения примитивные типы, то может быть только один вариант. Если в качестве исключения использовать указатель на объект, то имеем возможность воспользоваться RTTI. Можно предложить что-то вроде
      #define CATCH_BLOCK_TYPED(t)  \
        } else if (NULL != dynamic_cast<t>(__exc)) {
      
      И это даст нам возможность использовать исключения разных типов. Но тогда невозможно бросать исключения примитивных типов.
    • Удалять брошенный объект-исключение должен сам пользователь.

    И всё же.
    Несмотря на описанные ограничения, описанный метод обладает неотъемлемыми достоинствами:
    • Простота. Несколько десятков строк кода — и все работает.
    • Прозрачность концепции.
    • Легкая переносимость. Никакой зависимости от архитектуры.
    Существует ли возможность устранить недостатки данного метода, сохранив его преимущества? И да, и нет. Пользуясь исключительно средствами C++, это сделать невозможно.

    К чему клонит автор.
    В порядке технического бреда подумаем, как надо модифицировать компилятор, чтобы корректно реализовать вышеописанную схему?
    Чего не хватало в вышеприведенном решении? Знания о том, как был порожден объект.
    Например, если объект построен на памяти, выделенной из общей кучи и может мигрировать между потоками, его ни в коем случае нельзя регистрировать в потоко-зависимом стеке. Не стоит нигде регистрировать объект, агрегированный в другой объект.
    А с объектом того же типа, но на стековой памяти, это сделать необходимо. Конечно, есть возможность отдать указатель на этот стековый объект в другой поток, но трудно представить, в какой ситуации это могло бы быть полезным.
    Итак:
    • Для стековых объектов типа Т компилятор создает на самом деле оберточный класс типа
      template<class T>
      class __st_wrapper : public unw_item_t  {
      public:
          virtual ~__st_wrapper() 
          {
            unreg();
            ((T*)data_)->T::~T();
          };
      private:
         char data_[sizeof(T)];
      };
      
      а так же вызов нужного конструктора T.
    • Статический член класса jmp_buf_splice::root_slice_ реализуется либо через TLS, либо через соответствующий регистр, если есть
    • Программист по прежнему видит только объект типа Т, расположенный в data_
    • У стековых объектов без виртуальных деструкторов, таковой появляется в обертке
    • Бросать исключения в деструкторах теперь можно т.к. перед вызовом собственно деструктора мы разрегистрировались.
    • Не поддерживаем аппаратные исключения (исключения ядра), поэтому на момент возбуждения исключения компилятор знает какие регистры надо «приземлить» и обязан это сделать
    • Для штатного уничтожения стековых объектов компилятор создает вызовы деструкторов __st_wrapper'ов
    • Механизм выбора подходящего catch блока оставляем как есть. Т.е. вспомогательная табличная информация с описателями этих блоков вне кода нам всё-таки потребуется.
    • Передачу управления будем осуществлять с помощью аналога setjmp. Предлагается реализовать промежуточный (по отношению к двум описанным выше) вариант передачи управления. Setjmp обладает существенным недостатком – размер буфера довольно велик, тогда как реально используется его малая часть.
      С другой стороны, исполнение байт-кода в духе DWARF представляется весьма расточительным.
      Поэтому, вместо буфера setjmp будем хранить список регистров, требующих восстановления и сдвиги относительно указателя стека, где лежат актуальные значения. В случае вычисленного значения в регистре хранится непосредственно значение. Для этого в стеке выделяется дополнительная память и отдается сдвиг на нее. Фактически, заводится временная переменная.
      Перед возбуждением исключения компилятор выгружает все актуальные данные из регистров, в этом случае можно восстановиться без потерь.

      Всё же, стоит отметить, использование блока try — это сознательный акт, нет ничего плохого в том, что это несет за собой определенные издержки. IMHO эти (умеренные) издержки даже полезны т.к. стимулируют ответственное отношение к инструментам языка.
    • Перехват исключений при вызове оператора new и new [ ] оставляем как есть. Т.е. каждую итерацию защищаем внутренним try блоком и уничтожаем всё созданное в предыдущих итерациях, если произошло исключение, которое потом перевозбуждаем. И, конечно, отдаем обратно память, выделенную под объект[ы.]
    • Для реализации массива стековых объектов и делать ничего не надо. Но можно сохранить немного памяти, реализовав специальный стековый объект — вектор, аналогичный тому, что используется при вызове оператора new [].


    Кстати.
    • Объект может узнать, что он стековый. Для этого его this должен быть в пределах сегмента стека текущего потока.
    • Значит, можно снять объект с крючка? Не представляю, зачем это может понадобиться, но такая возможность существует.
    • Раз можно снять, значит, можно и посадить. Выделить память в стеке через alloca, принудительно вызвать конструктор и подключить к механизму раскрутки стека.
    • Для архитектур с раздельными стеками данных и управления можно реализовать обработку исключений весьма эффективно, используя стек управления вместо списка.


    PS: Отдельное спасибо Александру Артюшину за содержательное обсуждение.
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 38
    • +1
      Благодарю за статью. Интересно, как обстоят дела с обработкой исключений в коде, который собрал clang. Есть ли отличия от gcc?
      • 0
        Никогда плотно с clang-ом не общался, но, насколько я знаю, формат выполняемого файла тот же и те же sjlj/dw2 в нем реализованы.
        • +9
          Clang, будучи построенным на базе LLVM, использует zero cost exceptions. Почитать можно здесь: llvm.org/docs/ExceptionHandling.html

          В двух словах, это Itanuim Exception ABI и DWARF4.
          • +2
            Советую кстати почитать еще по поводу personality функций, посредством которых реализуется поддержка общего формата исключений в едином рантайме для нескольких языков. Так например происходит в случае пары C++ и C. Последний, хоть ислючения и не поддерживает напрямую, тоже вынужден иметь свою personality функцию для соединения.

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

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

                В действительности же setjump как правило реализован с помощью большого макроса с inline assembler'ом внутри. И у компилятора почти нет никакой возможности сохранить только полезные регистры. Конечно, компилятор не вызывает setjmp (по крайней мере, ни gcc, ни clang) напрямую. У него есть своя внутренняя реализация, которая по сути аналогична вызовам setjmp / longjmp. Правда в том, что «раскрутка» этих внутренних реализаций в реальный код осуществляется уже в backend'е, где почти нет никаких оптимизации. Так что тут тоже пролет. sjlj исключения очень тяжелы на каких-нибудь SPARC'ах с их огромным регистровым набор (и регистровыми окнами).
                • +1
                  Но ведь это происходит после назначения регистров, так что формально вся информация у компилятора есть. Может, сочли что овчинка выделки не стоит.
                  • +4
                    Штука вся в том, что обычно это происходит до назначения регистров. Это нужно как раз из-за того, что код сохранения может быть достаточно нетривиален и вызвать пару-тройку spill'ов, вообщем, вообще говоря, изменить аллокацию регистров. И если делать все после, то было бы очень неприятно «патчить» уже готовое распределение так, чтобы его не испортить.
                  • +2
                    Я выскажусь в защиту clang. setjmp / longjmp являются интринсиками, т.е. являются частью LLVM IR, а не реализованы с помощью макросов. И как раз в backend'e осуществляются все оптимизации(так называемые проходы).
                    • +5
                      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
                      • +1
                        У нас расхождение в терминалогии =) Я считал, что обработка IR и генерация машинозависимого кода и есть backend, но в парочке статей нашёл робкое «middle layer».
                        • +1
                          Пользуясь случаем, позвольте немного поофтопить. Вы случайно не в курсе, работает ли кто-то сейчас над shadow-stack-gc? В нем есть некоторое количество неприятных багов/неточностей, которые нам в llst пришлось обходить. В частности: использование __gcc_personality_v0 вместо желаемой gxx и слишком поздняя инициализация подсистемы shadow stack в случае с чисто JIT модулем (насколько я помню, обработка LLVM валился с сегфолтом, если shadow stack pass включен в pass manager, но в модуле нет еще ни одной функции с атрибутом gc «shadow-stack» ).

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

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

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

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

                              А вообще я ожидал здесь увидеть сравнение производительности для различных компиляторов, а в результате обнаружилось достаточно поверхностное обсуждение подходов.
                              • +1
                                Непонятно, что это вообще такое?

                                LMGTFY: www.linfo.org/software_interrupt.html
                                • 0
                                  По вашей ссылке — описание древнего Linux 2.4. Ну можно было еще дать ссылку на википедию:

                                  MS-DOS API:

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

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

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

                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.