Как производственные компиляторы реализуют обработку деструкторов в управлении потоком

Короче говоря, я пишу компилятор и, достигнув функций ООП, столкнулся с дилеммой, связанной с обработкой деструкторов. В основном у меня есть два варианта:

  • 1 — поместить все деструкторы для объектов, которые необходимо вызвать в этой точке программы. Эта опция звучит так, как будто она будет дружественной к производительности и простой, но будет раздувать код, поскольку в зависимости от потока управления определенные деструкторы могут дублироваться несколько раз.

  • 2 — разделить деструкторы для каждого блока кода с метками и «спагетти-прыжком» только через те, которые нужны. Преимущество — никакие деструкторы не будут дублироваться, недостаток — это будет включать непоследовательное выполнение и скачкообразное переключение, а также дополнительные скрытые переменные и условия, которые понадобятся, например, чтобы определить, оставляет ли выполнение блок для продолжения выполнения в родительском объекте. заблокировать или разбить / продолжить / перейти / вернуть, что также увеличивает его сложность. И дополнительные переменные и проверки вполне могут поглотить пространство, сэкономленное этим подходом, в зависимости от того, сколько объектов и как сложная структура и поток управления внутри него.

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

Я поместил c ++ в теги, потому что это язык, который я использую, и я немного знаком с ним и с парадигмой RAII, которую также моделирует мой компилятор.

8

Решение

По большей части вызов деструктора может обрабатываться так же, как и обычный вызов функции.

Меньшая часть имеет дело с EH. Я заметил, что MSC генерирует смесь встроенных вызовов деструкторов в «обычном» коде и для x86-64 создает отдельный код очистки, который сам может содержать или не содержать копии логики деструктора.

ИМО, самым простым решением было бы всегда называть нетривиальные деструкторы обычными функциями.

Если оптимизация кажется возможной на горизонте, относитесь к вышеупомянутым вызовам как к чему-либо еще: будет ли он помещаться в кеш со всем остальным? Будет ли это занимать слишком много места на изображении? Так далее..

Интерфейс может вставлять «вызовы» нетривиальным деструкторам в конце каждого действующего блока в своем выходном AST.

Бэкэнд может обрабатывать такие вещи, как обычные вызовы функций, связывать их вместе, создавать где-то большую логику вызова block-o-destructor и переходить к ней и т. Д …

Связывание функций с одной и той же логикой кажется довольно распространенным явлением. Например, MSC стремится связать все тривиальные функции с одной и той же реализацией, деструктором или иным образом, оптимизируя или нет.

Это в первую очередь из опыта. Как обычно, YMMV.

Еще кое-что:

Логика очистки EH имеет тенденцию работать как таблица переходов: для данной функции вы можете просто перейти в один список вызовов деструкторов, в зависимости от того, где было сгенерировано исключение (если применимо).

4

Другие решения

Я не знаю, как коммерческие компиляторы создают код, но, предполагая, что в этот момент мы игнорируем исключения [1], я бы выбрал вызов деструктора, а не его добавление в строку. Каждый деструктор будет содержать полный деструктор для этого объекта. Используйте цикл для борьбы с деструкторами массивов.

Встроить вызовы — это оптимизация, и вы не должны этого делать, если не «знаете, что это окупается» (размер кода в зависимости от скорости).

Вам нужно будет разобраться с «разрушением во вмещающем блоке», но при условии, что у вас нет прыжков из блока, это должно быть легко. Прыжки из блока (например, возврат, разрыв и т. Д.) Будут означать, что вам нужно перейти к фрагменту кода, который очищает блок, в котором вы находитесь.

[1] Коммерческие компиляторы имеют специальные таблицы, основанные на «где было выброшено исключение», и фрагмент кода, сгенерированный для этой очистки — обычно повторное использование одной и той же очистки для многих точек исключения с использованием нескольких меток перехода в каждом фрагменте очистки.

2

Компиляторы используют сочетание обоих подходов. MSVC использует встроенные вызовы деструкторов для нормального потока кода и очищает блоки кода в обратном порядке для ранних возвратов и исключений. Во время обычного потока он до сих пор использует единственное скрытое локальное целое число для отслеживания прогресса конструктора, поэтому он знает, куда переходить при досрочном возврате. Достаточно одного целого числа, потому что область действия всегда формирует дерево (а не, скажем, использование битовой маски для каждого класса, который был успешно создан или не был создан). Например, следующий довольно короткий код, использующий класс с нетривиальным деструктором и некоторыми случайными возвращениями, разбросан по всему …

    ...
if (randomBool()) return;
Foo a;
if (randomBool()) return;
Foo b;
if (randomBool()) return;

{
Foo c;
if (randomBool()) return;
}

{
Foo d;
if (randomBool()) return;
}
...

…может расширяться до псевдокода, как показано ниже на x86, где прогресс конструктора увеличивается сразу после каждого вызова конструктора (иногда более чем на единицу до следующего уникального значения) и уменьшается (или «переносится» на более раннее значение) непосредственно перед каждым вызовом деструктора. Обратите внимание, что классы с тривиальными деструкторами не влияют на это значение.

    ...
save previous exception handler // for x86, not 64-bit table based handling
preallocate stack space for locals
set new exception handler address to ExceptionCleanup
set constructor progress = 0
if randomBool(), goto Cleanup0
Foo a;
set constructor progress = 1 // Advance 1
if randomBool(), goto Cleanup1
Foo b;
set constructor progress = 2 // And once more
if randomBool(), goto Cleanup2

{
Foo c;
set constructor progress = 3
if randomBool(), goto Cleanup3
set constructor progress = 2 // Pop to 2 again
c.~Foo();
}

{
Foo d;
set constructor progress = 4 // Increment 2 to 4, not 3 again
if randomBool(), goto Cleanup4
set constructor progress = 2 // Pop to 2 again
d.~Foo();
}

// alternate Cleanup2
set constructor progress = 1
b.~Foo();
// alternate Cleanup1
set constructor progress = 0
a.~Foo();

Cleanup0:
restore previous exception handler
wipe stack space for locals
return;

ExceptionCleanup:
switch (constructor progress)
{
case 0: goto Cleanup0; // nothing to destroy
case 1: goto Cleanup1;
case 2: goto Cleanup2;
case 3: goto Cleanup3;
case 4: goto Cleanup4;
}
// admitting ignorance here, as I don't know how the exception
// is propagated upward, and whether the exact same cleanup
// blocks are shared for both early returns and exceptions.

Cleanup4:
set constructor progress = 2
d.~Foo();
goto Cleanup2;
Cleanup3:
set constructor progress = 2
c.~Foo();
// fall through to Cleanup2;
Cleanup2:
set constructor progress = 1
b.~Foo();
Cleanup1:
set constructor progress = 0
a.~Foo();
goto Cleanup0;
// or it may instead return directly here

Компилятор может, конечно, переставить эти блоки в любом случае, если сочтет это более эффективным, вместо того, чтобы завершить всю очистку. Ранние возвраты могут перейти вместо альтернативной очистки 1/2 в конце функции. В 64-битном коде MSVC исключения обрабатываются с помощью таблиц, которые отображают указатель команд, когда исключение произошло с соответствующими блоками очистки кода.

2

Оптимизирующий компилятор преобразует внутренние представления скомпилированного исходного кода.

Обычно он строит ориентированный (обычно циклический) граф основные блоки. При строительстве этого график потока управления это добавление вызова деструкторам.

За НКУ (это бесплатный программный компилятор — и так Clang / LLVM -, чтобы вы могли изучить его исходный код), вы, вероятно, могли бы попытаться скомпилировать некоторый простой код теста C ++ с -fdump-tree-all а затем посмотреть, что это сделано в gimplification время. Кстати, вы можете настроить g++ с ПЛАВИТЬСЯ изучить его внутренние представления.

Кстати, я не думаю, что то, как вы имеете дело с деструкторами, так важно (обратите внимание, что в C ++ они неявно вызываются в синтаксически определенные места, как } их определяющей области). Большая часть работы такого компилятора заключается в оптимизации (тогда работа с деструкторами не очень актуальна; они почти такие же процедуры, как и другие).

1
По вопросам рекламы ammmcru@yandex.ru
Adblock
detector