Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

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

Anna | 24.06.2014 | нет комментариев

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

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

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

Как обстоят дела.

Существует два подхода к реализации обработки исключений:

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

Несколько примеров:

  • 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 считают первую тактику больше перспективной.
    • Инициирование исключения сделано через программное прерывание.

    Данному способу присущи те же недочеты, что и 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)....

      минимум издержек

Подробности дозволено посмотреть туттут и тут.

А что, если …
А, казалось бы, каждого и дел то – вызвать в необходимом порядке деструкторы, тела которых теснее существуют. Как же случилось, что простая, в всеобщем-то, задача имеет такие вязкие, массивные и притом самостоятельно прогрессировавшие решения? Сложно сказать, так исторически сложилось.
Испробуем набросать решение, усердствуя оставить его простым и по вероятности архитектурно-само­стоятельным.

  • Первым делом выбираем тактику — это будет 2-й вариант.
  • Передача управления – 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: Отдельное спасибо Александру Артюшину за обстоятельное обсуждение.

Источник: programmingmaster.ru
Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB