Обновить
0
0

Пользователь

Отправить сообщение
Да, в std::function тоже используется type erasure. Но здесь он не может быть применим по той причине, что сигнатура функций для run разная у разных проходов.
Я это просто расцениваю как предложение реализовать паттерн Visitor для некоторых упомянутых вами названий сущностей. Для лучшего восприятия я оставил канонические имена для функций accept и visit. И что?

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

Почему так? Представьте, что какому-нибудь отдельно взятому проходу понадобилось что-то добавить в класс Module для удобства работы именно этого прохода. Если он это сделает, то это нарушит ISP, и сделает Module сложным в использовании для остальных. Если каждый вспомогательный компонент (коим является PassManager) будет добавлять в фундаментальные классы LLVM IR средства только для того, чтобы было удобно работать данной компоненте, то архитектура проекта получится не масштабируемой и очень не очевидной в использовании для тех пользователей, которых не интересует как работает PassManager или другие вспомогательные компоненты.

Насчет визитора: все проходы должны переопределять метод run для каждой из IR-сущностей (Loop, Module, Function). Если вы посмотрите в код llvm, то увидите, что это далеко не весь список (посмотрите наследников класса Pass).

Почему вдруг PassManager превращается в PassModel на основании того, что в нем тоже присутствует метод, побуквенно совпадающий с имени метода из PassModel, но семантически никак ему не эквивалентный (и он начинает сам выступать как PassModel, сам себя вкладывает, и сам себя вызывает) можно объяснить лишь дизайнерским выпендрёжом — чтобы было красиво. Из-за постоянно возникающих безответных вопросов «зачем?, почему это так надо?»

Никто ни в кого не превращается. PassManager не наследуется от PassModel. Здесь просто используется статический полиморфизм. Насчет «безответных вопросов»: одно из важных использований этой идиомы — поддержка вложенности (чтобы помодульные/попроцедурные проходы можно было легко чередовать).

В Legacy PM для этого сделаны сложные колбеки, которые читаются гораздо тяжелее. А также немаловажный выигрыш — существенная простота кода и уменьшение зависимостей.

разобраться с этим финтом (без подсказки в виде отдельной статьи или презентации о «новой интересной идиоме») уже сложновато.

С этим согласен, тема непростая. В llvm новый PM уже больше 6 лет делают (и ни кто бы то ни было, а сам Chandler Carruth :).

Плюс к этому статья предполагает наличие знакомства с llvm, что к сожалению сокращает количество читателей (или оставляет много недопонимания).

Ну и под конец когда на сцене вдруг появляется адаптер

Адаптер здесь как раз уместен: функции вложены в модули, а адаптер нужен, чтобы достать функции из модуля и пробежаться по ним, вызвав все зарегистрированные для функции проходы.
А как определить должны или не должны?

Это определяется из предметной области разрабатываемого проекта. Но если вы можете предложить общего предка для этих сущностей без делания down-cast при каждом использовании, мне интересно будет почитать. Желательно с примерами кода.

Если для вас это нормальное решение, то тогда именно поэтому вы костылей не наблюдаете.

Вы считаете это ответом на вопрос?

По иронии в GoF паттерн Visitor описывается как раз на примере с компилятором.

А в чем здесь ирония? Если вы считаете, что визиторы здесь применимы, то перечитайте еще раз мой 1й комментарий. А насчет использования визиторов в компиляторах — это очень распространенный паттерн, посмотрите, например класс InstVisitor.
Да, конечно. Тут сложно судить со стороны т.к. не понятно почему, находясь под полным контролем разработчика, классы IR-сущностей не сделаны под общим предком с некоторым интерфейсом, который бы давал последующую возможность добавлять выполнение относительно произвольных действий над ними с помощью Visitor.

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

Повторюсь еще раз: классы Module, Function и Loop не образуют иерархию и не должны этого делать. Введение для них общего предка — прямое нарушение LSP. Более того, даже если у вас будет этот общий предок и у всех проходов будет один метод run, который будет его принимать, то внутри run вам придется делать down-cast к той сущности, которая нужна этому проходу.

Возможно смысл был в том, чтобы сделать новый PassManager никак не трогая определения классов IR-сущностей, а также и определения классов PassT-сущностей.

Об этом и речь. Legacy PM тоже не трогал определение IR-сущностей. А New PM, к тому же, не обязует связывать классы проходов в общую иерархию (строгости ради, стоит заметить, что там есть mixin класс для всех пассов, но это уже другой разговор).

И чтобы зарегистрировать все проходы в красивый единый вектор требуется некий костыль в виде некоего адаптера

А тут я видимо где-то отстал: можете рассказать, с каких это пор классы-адаптеры стали костылями? В данном случае я никаких костылей не наблюдаю. Ниже привожу код данного адаптера. Как по мне, это совершенно нормальное решение, которое хорошо ложится в данный концепт.

template <typename FunctionPassT>
class ModuleToFunctionPassAdaptor {
...
/*ret val*/ run(Module &M,/*params*/) {
    // ...
    for (Function &F : M) {
        Pass.run(F, /*args*/);
   // ...
}

private:
  FunctionPassT Pass
};
Можно и run сделать, но это же не решает проблему. Сигнатуры функций все равно будут разные: один проход будет принимать Module&, другой Function&, третий еще что-то.
Дело в том, что Module, Function, Loop и остальные IR сущности не образуют иерархию классов, что не позволяет обрабатывать их полиморфно. По этой причине приходится делать различные методы у самих проходов и составлять из этих проходов более сложные иерархии: от Pass наследовать ModulePass, FunctionPass etc, а в свою очередь от них наследовать уже сами проходы, которые работают с определенной IR entity (Inline, LoopFuse etc). Поэтому мы получаем различные виртуальные функции: для ModulePass — runOnModule(Moduel&), для FunctionPass — runOnFunction(Function&), для LoopPass — runOnLoop(Loop*) и т.д.

А это в свою очередь не позволяет нам работать с любой коллекцией проходов полиморфно. Т.е. нельзя просто взять набор проходов и сделать так:

for (auto& pass: Passes) {
  pass.run(...);
}


А concept-based polymorphism дает такую возможность
К сожалению все так. До сих пор нужна специальная опция, чтобы включить новый PM -fexperimental-new-pass-manager. А разработчики проходов поддерживают две версии — и для Legacy и для нового PM.

Но при сборке llvm можно подать влажок, который включает его по умолчанию: -DLLVM_USE_NEWPM=ON
Спасибо за хороший вопрос!

Боюсь, что без дополнительных crutches сделать это не получится. В чем здесь проблема

class PassVisitor {
public:
    excplicit PassVisitor(Module& M) : M(M) {}

    void visit(SomeModulePass& m) { m.runOnModule(/*Ok, we know about module*/M); }
    void visit(SomeFunctionPass& m) { m.runOnFunction(/*What function?*/); }
    void visit(SomeLoopPass& m) { m.runOnLoop(/*What loop?*/); }

private:
    Module& M;
};

class Pass {
    virtual void accept(PassVisitor &v) = 0;
};


class SomeModulePass : public Pass {
public:
    void accept(PassVisitor &v) override { v.visit(*this); }
    bool runOnModule(Module& M);

};

class SomeFunctionPass : public Pass {
public:
    void accept(PassVisitor &v) override { v.visit(*this); }
    bool runOnFunction(Function& F);
};

class SomeLoopPass : public Pass {
public:
    void accept(PassVisitor &v) override { v.visit(*this); }
    bool runOnLoop(Loop& L);
};

/// ...

PassManager pm;
pm.addPass(SomeModulePass());
pm.addPass(SomeFunctionPass());

// ...

PassVisitor v(M);
for (auto& pass : pm.getAllPasses()) {
    pass.accept(v);
}


Это бы хорошо работало, если бы все пассы работали с единой иерархией классов: нечто, от чего наследуется модуль, функций и т.д. В этом случае мы могли бы сделать визитор на каждую из этих сущностей и, таким образом, применить паттерн Visitor. В нашем же случае получается, что непонятно, какую функцию (или любую другую IR сущность) нужно передавать проходу в функции visit
В докладе идет речь об известных оптимизациях, результаты производительности которых хорошо описаны в различных статьях.
Если бы эти оптимизации были бесполезны, то они бы не присутствовали в продуктовых компиляторах (т.к. замедляют время сборки ваших программ).

Когда было сказано про «цифры», имелось ввиду, что производительность рассматривается в контексте применения оптимизаций как таковых (конечно, есть редкие случаи, когда оптимизация является деоптимизацией для данного конкретного приложения, но это исключение из правил).
Компилятор все увидит. Если у вас в объявлении функции прописан noexcept, то компилятор будет на это закладываться, даже если определение функции будет в другом translation unit.
Как я писал выше, «везде» предполагает — исходно по дизайну, а не везде без разбора.
Спасибо за фидбэк. Все правильно говорите: noexcept — это часть интерфейса (и на последнем слайде, где главный вывод, упомянуто, что нужно его ставить с осторожностью).

А насчет изменения ситуации, то это как если бы вы сделали метод константным (что тоже, конечно, часть интерфейса), а потом, например, поняли, что это hot path и что ради увеличения производительности нужно кешировать какие-то значения в этом методе, то для этого вероятно объявите кеш как mutable, но не снимите константости с интерфейса (т.к. в этом случае нарушите интерфейс).

Аналогично и здесь: если при начальном дизайне ваш метод помечен как noexcept, а потом, в силу implementation details причин, вы решаете вызвать из этой функции (помеченной noexcept) функцию, бросающую исключение, то делаете try/catch внутри самой функции, но функция при этом остается noexcept. А если же изначальный дизайн по каким-то причинам меняется принципиально, например, полностью переделывается схема обработки ошибок, то, думаю, в таком случае снятие с функций noexcept будет далеко не главной проблемой.

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

Данный доклад не преследует цель пропаганды отказа от использования исключений. Он нацелен показать, что относиться к простановке noexcet следует более внимательно.

Ставьте noexcept там, где исключений нет и не будет по дизайну (например, в move конструкторах & операторах, обертках над C-кодом и т.п.), а не там, где их нет сегодня, потому что звёзды так сложились

Согласен: именно если так по дизайну, а не сегодня так, завтра по-другому. Единственное дополню, что это могут быть не только обертки и move конструкторы, но также и многое другое, если это четко прописано в интерфейсе функции (например, что функция возвращает std::optional)
И это не совсем верно. Получается, что процесс еще продолжается, но очевидно, что человек уже прочитал статью. Это чистый present perfect — действие завершено, но период времени еще не закончен.

You have made my friday
Скорее всего это сделали в целях безопасности.
Secure Coding Standard

Информация

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