Проблемы и решения в крупных проектах на примере LLVM-snippy
Всем, здравствуйте.
Меня зовут Константин Владимиров и, несмотря на то, что представили меня как блогер и преподаватель, оно сегодня я пришел выступать как программист и руководитель отдела разработки компиляторов и средств разработки в компании «Синтакор».
Компания Cintakor занимается тем, что мы выпускаем процессорные ядра на открытой расширяемой архитектуре Risk Five.
Если у кого-нибудь из вас есть ноутбук, там, скорее всего, архитектура X86, если у кого-нибудь из вас есть телефон, там, скорее всего, архитектура ARM, мы пользуемся архитектурой Risk Five.
Поднимите руки, кто что-нибудь знает про архитектуру Risk Five.
Ого, довольно много, поэтому мы обойдемся одним слайдом.
Соответственно, да.
А архитектура Risk Five открытая и открытая, что это значит.
Это значит, что вас из нее не выгонят.
Это как линукс.
Как вас могут, например, добрать лицензию на арм, лицензию на Risk Five, никто никогда не отберет.
И у нас, соответственно.
И она расширяемая.
Есть не так много инструкций базового набора.
И вы можете дальше выбирать расширение и стекотить расширение.
То есть базовая AISA это что-то совсем микроконтроллерное, что-то совсем маленькое.
Да, кто не знает.
AISA Instruction Set Architecture это грубо говоря все, что может сделать микропроцессор.
Вы можете добавить некоторые расширения, например, вы добавляете расширение М и у вас появляется умножение деления, вы добавляете расширение А, у вас появляются атомики и так далее.
Таким образом, из этого конструктора вы можете собирать свой микропроцессор, и компания, в которой я работаю, делает целую линейку.
процессоров, и вы видите, что есть от самых маленьких типа SKR-1.
SKR-1 может стоять, например, в вашем газовом счетчике, и, скорее всего, стоит в вашем газовом счетчике, вот до самых больших высокопроизводительных дизайнов, таких как SKR-9, который может стоять, например, в дата-центре, куда стекается информация с газовых счетчиков там со всего города.
Таких дизайнов у нас пока широко не внедрено, но они вполне возможно.
И вы видите, что вся эта линейка отличается наборами расширений.
То есть, если вы выбираете только Bazwise, у какой-нибудь Rw32e, то в этом случае дизайн наверилогии такого рода могут написать, ну, там, двое студентов за полгода.
И мало того, дизайн SK-1, он, в принципе, у нас выложен на гитхабе.
Вы можете зайти на наш гитхаб, скачать с арцы и наверилогии, это абсолютно open source.
Другое дело, если вы целитесь во что-то типа RW64GCV, тогда даже команда из опытнейших разработчиков на RTL может делать это очень долго.
И это дизайн сложный.
Ну вот, например, вам буквально на прошлом докладе рассказывали про киши, да?
Вам рассказывали про работающие киши.
Представьте, что вы сделали дизайн, и киши там сломаны.
А что вы будете делать?
И когда вы сделали логический дизайн, вы хотите его отверифицировать.
Что такое задача системной верификации?
Задача системной верификации — это примерно так.
У вас есть еще даже не железка.
У вас есть просто логический дизайн на верилоге.
И у вас есть какая-то модель.
Вы должны взять откуда-то тестов, прогнать через дизайн, прогнать через модель и в конце что-нибудь сравнить.
Я условно написал трасс, эта картинка специально сделана примитивной, в первой редакции слайдов она была слишком сложной, меня на предпрогоне покритиковали, я ее немножко упростил.
А вы должны что-то сравнить, может быть состояние регистра после программы, может быть память, может быть память и регистра в каких-то контрольных точках.
И теперь вопрос.
Итак, вы написали свой процессор.
Откуда вы возьмете тесты?
Два варианта.
Первый вариант — это взять тесты, откуда-нибудь, где есть программы, в конструкции в мире есть много программ.
Давайте попробуем забутать на вашем дизайне линукс.
Давайте попробуем поднять на этом линуксе хромиум.
Давайте попробуем сделать что-нибудь еще.
Где-нибудь в процессе у нас что-нибудь упадет.
И мы, конечно же, отладим ошибку.
Ага, я слышу грустный смех в зале.
Смех должен быть грустным в эти моменты.
Второй вариант — мы можем нагенерировать тесты.
И идея написать тестовый генератор, особенно для Risk 5, где все Open Source, где работает Community — это очень распространенная идея.
И по интернетам валяются обломки этих тестовых генераторов.
А их пишут все, их пишут по одному и тому же сценарию, которое я назвал судьба от Хоктестова генератора.
Мы соблазняемся простотой базовой системы команд, сажаем одну талантливую студентку, и она пишет описание инструкции, кодировку базовой системы команд за две недели.
Я проверил.
А дальше мы пишем модель семантика этих инструкций еще две недели.
И до этой точки дошло масса генераторов.
На этом слайде перечислены раз, два, три, четыре, пять, шесть.
И там еще десяток не перечисленен, там тысяча шесть.
А дальше мы смело беремся за расширение.
И вот тут оказывается, что наш конструктор очень сложен.
Например, scalable vector extension 600 обкадов, сложнейшая кодировка, сложнейшая семантика.
Попробуйте это напишите в вашем генераторе.
Ну хорошо, кое-кто написал, ForSriskFive написал, Microtask написал.
Но дальше мы вспоминаем, что есть Bitmoneyp, что есть VectorCrypto, что есть PacketSimpt, что существует сотни расширений, многие из которых даже особо недокументированы, а не приватные.
А кроме того, когда вы написали собственную кодировку хотя бы для РВВ, как вы ее напишете?
Скорее всего вы напишете ее в чем-то вроде XML.
Давайте я вам покажу кусочек описания такой кодировки в ForSriskFive.
слайд сделан специально, чтобы вас напугать.
Это маленькая выдержка из XMLки на 6 тысяч инструкций.
6 тысяч.
Ну, строг, я имею в виду, в XMLки.
И после этого мы понимаем, что это только вершина из Борга.
На самом деле, написание всего лишь кодировки инструкций — это такая мелочь, что мы сегодня даже об этом больше не будем разговаривать.
А написание тестового генератора — это очень сложная логика генерации.
Потому что представьте, что вам нужно проверить бронч-предиктор.
Это значит, вам что нужно сгенерировать?
Вам нужно сгенерировать брончи, вам нужно сгенерировать циклы, вам нужно сгенерировать сложный контрол-флоу, причем сложный, но с такими элементами регулярности, чтобы бронч-предиктор что-то предсказывал.
Представьте, что вам нужно проверить киши.
Все вот эти вытеснения, всю вот эту когерентность, что у вас при каких-то там вытеснениях по кишам, по этим протоколам все нормально идет, а у вас многоядерный дизайн, у вас там 8 ядер, например.
И между ними какой-нибудь протокол.
И эта логика, она на самом деле сложная.
Потому что, например, вы хотите проверять схемы страйдами.
Только что нам рассказывали про то, что страйда в кишах это наше все.
Замечательно, мы хотели бы, чтобы страйды генерились.
Мы хотели бы, чтобы генерировались вызвы функции и так далее.
И когда ко мне пришли с идеей написать генератора, казалось бы, где системные верификации, где я, я все-таки руковожу отделом компиляторов и инструментов разработки, я сказал, ребят, мы знаем как все упростить.
Потому что у нас есть LVM.
И в LVM вся кодировка инструкции уже описана.
Да, она там описана тоже, ну, сложно.
Но вот эта сложность, она, во-первых, гораздо более структурирована, а во-вторых, что вы видите на этом слайде?
Вы видите на этом слайде что-то, что уже есть, значит, вам это не надо писать, и мало того, оно не просто уже есть, оно уже есть в промышленном компиляторе, которым пользуется весь мир.
Вы более-менее можете этому доверять.
Мало того, LLVM — это компилятор, то есть он генерирует код.
По сути, идея использовать для генератора тестов LLVM — это базовая идея.
Давайте использовать для кода-генератора и кода-генератор.
Это очевидно.
Там из коробки подержаны вызовы функции, многое другое.
Там просто удобно писать логику.
И мы написали наш генератор Луэм Сниппи, но я хотел бы его на секундочку отложить.
Почему?
Потому что на самом деле этот доклад вообще про сифлоспласс и про архитектур.
Поэтому давайте позволим себе небольшой нырок.
Давайте я покажу вам некоторые решения в архитектуре LLVM и то, к чему они привели.
Чтобы потом, когда я поговорю про архитектуру LLVM-Snipe, наш генератор называется LLVM-Snipe, он выложен в OpenSource.net hub'е, мы смогли как-то сопоставить эти решения, как мы пользуемся архитектуре LLVM и как мы с ней боремся.
И давайте поговорим про runtime полиморфизм.
А виртуальная функция.
Здесь уже сегодня выступал уважаемый докладчик из ПВС-студия, который рассказывал, как бороться с виртуальными функциями.
Очень многие борются с виртуальными функциями.
Особенно, если вы пишете компилятор, у вас гигантский соблазн.
Потому что, смотрите, если вы пишете компилятор, то вот ваша иерархия, ну, представили эту иерархию базы, из нее что-то слева, что-то справа.
Дальше еще два класса слева, еще один класс справа.
У вас эти классы это, скорее всего, также члены какого-то инама, потому что это также узлы абстрактного синтаксического дерева.
Или чего-то в этом духе.
И у вас уже есть эти одишники.
И вам прям очень не хочется дублировать их вортитяй.
Вот прям очень не хочется.
Что сделали Valve?
Valve приняли изящное решение.
Заводим классов.
Что такое классов?
Начнем с самой верхушькой архии, бейс.
Классов берет BaseConst указатель, и для любого потомка Base, а BaseConst указатель от любой потомок Base.
Он вообще труп.
Да, все мы в этой иерархии Base.
Ну и GetValueID, какой-то ID-шник возвращает.
И дальше мы будем считать, что классов и GetValueID есть у каждого нашего класса.
И давайте немножко обобщим на производные классы.
Например, DriveTride.
Во-первых, если любой потомок дирайф трайт заходит через дирайф трайт указатель, то это труд.
А если заходит бейс указатель?
И если заходит бейс-указатель, то у нас есть так называемый динамический полиморфизм закрытого мира.
Потому что ЛВМАЯР вообще, говоря, изначально проектировали как даже с некой претензией на бинарную стабильность.
На бинарной стабильности в итоге не получилось.
Но считалось, что в ЛВМАЯРе мы знаем все ноды наперед.
И мы знаем все возможные айдишники.
Давайте их просто перечислим вот здесь в классов.
А все возможные айдишники, всех, кто ниже по эрархии, это true, а все остальные это false.
С одной стороны это решение кажется очень хорошим, но его поэтому и приняли.
У меня писали профессионалы.
Смотрите, из-за является ли аф на самом деле ту?
Мы вызываем ту, колонн, колонн, классов адрес f.
Все, нам не нужно больше RTTI-нода, нам не нужно больше вызов Type ID, нам не нужно платить за таблицы виртуальных функций для этого.
Мы реализовали два довольно простых метода.
А аналогично, DIN-Cast, DIN-Cast, он сознательно похож на Dynamic-Cast, он возвращает либо указатель, либо null-ptr, либо cast, cast либо возвращает указатель, либо abort.
Ужасто.
VLVM, кстати, от exceptionов тоже отказались.
Но тут многие из вас могут напрячься.
Мы же профессионалы, да?
У нас иерархия классов.
В иерархии классов нет одного виртуального деструктора.
Все отвечали на собеседованиях, зачем нужен виртуальный деструктор?
Чтобы удалять по указателю на базовый класс.
Как мы в такой иерархии будем удалять по указателю на базовый класс?
Свич сделал, спасибо, ответ ужасно.
Это ксерф настоящего кодда из LVM, и я не старался сделать его уродливее, а наоборот, я старался сделать так, чтобы он вслайт в лес.
И мало того, это даже не сам деструктор, это функция DeletValues, который деструктор взаимодействует сложным образом.
И если бы речь шла о маленькой иерархии, речь идёт о гигантской сложной иерархии, в которой basic block, SSOval, Instruction, SSOuser, например, Function это одновременно и ListNode, и также GlobalObject.
А вот и Instruction это не только SSOuser, это еще и ListNodeVisParent, к тому же парометризованная базовым блоком.
А базовый блок это также ListNodeVisParent, парометризованный Function,
Все это работало на механизме классов и все это довольно быстро перестало работать.
Почему?
Потому что компилятор устроен так.
У него есть много голов и много хвостов.
Компилятор берет много входных языков и он отдаёт их на много архитектур.
Но...
Все входные языки пользуются стабильным представлением LVMR.
Любой входной язык вы берете C, C++, Fortran, Rust, что угодно, и вы делаете один и тот же LVMR, и вот он как раз стабилен, это полиморфизм закрытого мира, но с бэкэндом компилятора это не работает.
Потому что в back-end компилятор вы хотите подать опцию в runtime и опцию, неизвестную в compile-time, скомпилируя мне для ARM.
И для этого ARM-овский back-end вместе с x86-ым back-end, вместе с RISC-5, должны держать внутри back-end компилятора.
И если вы хотите что-то диспетчить фронт-тайм, то у вас появляются виртуальные функции.
Посмотрите на эту прекрасную ирархию из LVEM.
LVEM NC EXPOR – machine code expression – это тот самый класс из классов и полиморфизмом закрытого мира.
А видишь, обведённый сознательно уродливым красным кружочком MC Target Expert.
Вот в нём появляются виртуальные функции, виртуальный деструктор, потому что он является общим динамически полиморфным интерфейсом для MIPSA, ARMA 8.6, NVPTX, PPCM.
Ну, короче, для всего, что может быть поддержанного лве.
И там появляются виртуальные функции, виртуальный деструктор.
И мы в нашем генераторе прямо очень на это наткнулись.
У нашей ошибки была гораздо сложнее.
Опять-таки, чтобы не усугублять, я приведу гораздо более простой пример.
Смотрите.
Представьте, что у вас есть база.
Например, Instant Info в LVM, в которой классов и все остальное.
И у вас есть ее наследник, в котором начинаются виртуальные функции.
Это как что-нибудь типа RISC-5 Gene Instant Info.
И дальше у вас есть функция Get something, которая возвращает Base-указатель, которая может быть Base-указатель или Direct-указатель.
Но как вы скажете?
Вы не можете использовать RTTI, потому что в Base ещё никакого RTTI нет.
Но вы также не можете использовать E-классов, потому что в Dirac тоже есть RTTI, там уже нет классов.
То есть люди начали с абсолютно чёткого проектного решения.
Давайте заменим неэффективный динамический полиморфизм.
К чему они в итоге пришли?
И вот это очень важный промежученный итог.
Давайте на нём остановимся.
Когда мы работаем над большими проектами, особенно когда мы работаем над большими проектами, мы должны принимать то, что я называю априорные решения.
Когда вы начинаете писать компилятор, у вас еще нет компилятора.
Вы еще не можете попробовать проверить.
Вы должны принять какое-то решение.
Будут ли в нем виртуальный функция или мы их чем-то заменим?
Будут ли в нем эксцепшен или мы их чем-то заменим?
И они могут быть ошибочными.
И эта ошибочность может проявиться поздно.
Например, вы уходили от РТТА, и вдруг вы пишете динкаст.
Или, например, вы уходили от эксэпшенов, уходили от эксэпшенов, уходили от эксэпшенов, и вдруг, поскольку у вас теперь всегда аборт, вы пишете специальную программу, которая ловит сигнал этого аборта, что-то там делает.
И работает еще в фоне.
И тогда вопрос, туда ли вы ушли.
Давайте про LVM.
Вот смотрите, давайте проголосуем.
А два варианта.
Оставить, как есть тщательно за документировать или передизайнить.
Кто за первый вариант?
Кто за второй?
Примерно пора.
Ну, кстати, вот.
А те реды, которые ближе ко мне, почему-то больше к передизайнить.
То есть люди с более новаторским мышлением садятся ближе ко ратору, да.
А в LWM оставили, как есть, и тщательно задокументировали.
И с тех пор все так и есть.
А теперь давайте поговорим о нашем генераторе.
Наш генератор является LWM-based инструментом.
Называется LWM Snippets.
Ссылка на GitHub внизу слайда.
Это довольно интересный, довольно живой проект.
У нас уже есть первые внешние пользователи, первые баги от внешних пользователей.
И в идеальном мире, в мире розовых пони, ЛВМ-Стипе должен работать так.
Он получает на вход гистограмму, то есть относительные веса инструкций.
Каждый вес, деленный на сумму весов, это вероятность появления инструкций в программе.
Кроме того, он получает описание секции, куда он кладет эти инструкции, и что он выдает на выход.
Он выдает на выход СНИПИД.
Ну, собственно, поэтому СНИПИД.
А СНИПИД этих инструкций плюс линкер-скрипт, чтобы прибить инструкции.
Но у нас есть проблема.
Вот эта схема, она ломается уже на доступах к памяти.
Посмотрите.
Вам сегодня уже рассказали, что у нас есть тек, у нас есть индекс, у нас есть offset.
Представьте, что вы хотите для доступа к памяти приколотить гвоздями, например, индекс и offset и при этом варьировать так.
То есть у вас должны быть какие-то биты, которые рандомизируются и какие-то биты, которые навсегда зафиксированы.
Или что вы хотите делать доступ страйдами, но чтобы каждый страйд при этом немножко плясал между first offset и last offset.
Опять же, вам сегодня показывали, какая может быть разница при страйде 256 и 257.
Гигантская.
Но какая вообще вероятность того, что такая инструкция сама появится в программе?
Она примерно нулевая.
И поэтому мы вынуждены пойти на вспомогательные инструкции.
И чтобы соответствовать схеме памяти, генератору нужно потратить несколько инструкций, а не на слайде такие серенькие.
потратить несколько инструкций на формирование адреса.
Окей.
Хорошо, мы потратили.
Но что стало с нашими вероятностями?
И мы приняли такой компромисс.
Хорошо.
Ансилы реинструкции не считаются.
Гистограмма — это гистограмма для основных инструкций.
Вот основная инструкция — это то, на чем работает генератор.
Вспомогательная инструкция — это то, что мы потратили на формирование адресов.
Ладно, компромисс как компромисс — все хорошо.
Пока все хорошо.
И это ломается на брончах.
Потому что представьте, в вашей гистограмме появляются условные переходы.
А условный переход — это хитрая штука.
Условный переход может вести вперед, но условный переход может вести и назад, и он часто ведет назад.
И когда вы делаете цикл, вы крутитесь в этом цикле, какая вероятность того, что у этого цикла случайно сформируется такое условие выхода, что вы из него выйдете?
Это примерно такая же вероятность, как выйти на улицу и встретить черепаху.
Ну, то есть не 50 на 50.
Поменьше.
И даже если это, даже если это бранч вперед, это вызывает определенные проблемы.
У вас некоторые блоки могут быть просто мертвыми.
И хуже всего это может быть вообще ни бранчи, ни цикла, это может быть произвольный регион, произвольный граф.
Хорошо, мы пошли на еще один компромисс.
Компромисс следующий.
Большинство программ не пишутся как случайные графы.
Ну, потому что программа, написанная как случайный граф, имела бы в среднем квадратичное число переходов относительно количества базовых блоков, это нереально.
Таких программ просто не существует.
Большинство программ пишутся в структурном виде, как завещал Дэйкстра.
И в структурном виде у них вложены друг в друга циклы, вложены в друг друга ифы, иф вложен в цикл, цикл вложен иф, у него вложены другие и так далее.
И в этом случае мы можем сделать...
инвариант структурности, то есть мы сначала генерируем структурный control flow, исходя из весов брончей в гистограмме, то есть мы знаем сколько должно быть переходов относительно стальных инструкций, мы нагенерировали сколько-то переходов, после чего мы начинаем забивать базовые блоки инструкциями один с другим.
И это дает нам pass manager, то есть генератор становится почти компилятор.
Первый pass, нагенерировать control flow, второй pass,
на генерировать инструкции внутри Control Flow.
Может быть еще PASS-рандомизировать Control Flow.
KF Generation PASS, KF Permutation PASS, Instruction Generator PASS.
И это ведет нас к проблеме.
Смотрите.
Представим пользователь, попросил какой-то режим.
Ну, например, пользователь говорит, я хочу, чтобы у меня лоды обязательно шли пачками по 30.
Вот мне нужно прям пробить кыши.
И всю эту пачку по 30 мы истратили на один из вот этих синеньких базовых блоков.
А дальше у нас сгенерировалось еще 30 циклов.
В гистограмме брончи лод равны вероятно.
Если мы потратили куда-то 30 лодов, у нас где-то должно быть 30 брончей.
И в этом случае на каждый цикл мы резервируем перегистру на индуктивную переменную.
На самом деле по два регистра, еще один, чтобы сравниваться.
И мы просто использовали все наши регистры.
И все то сложное состояние, которое было получено в синих блоках, белые блоки просто перетерли на тривиальное.
И верификатор ничего не получил с генератор.
Когда верификаторы к нам с этим пришли, мы поняли, что такой локальный подход не работает, и с этим надо что-то делать.
И мы сделали генплан.
А генплан или generation plan мне нравится это название за какой-то его советский дух.
У нас есть генераторы, в генераторе есть генплан.
Работает по концепции модальных режимов.
Каждый базовый блок.
Здесь показаны три базовых блока, которые как-то связаны друг с другом.
Может они участвуют в каких-то циклах, может быть в каких-то ифах.
Но в любом случае это базовые блоки.
И они размечены на модальные режимы.
То есть вот, например, серенький модальный режим означает генерацию, сцепленных друг с другом загрузок, чтобы каждая загрузка шла из адреса, там следующий.
А вот красненький режим означает большие пачки лодов.
Ну, а синенький режим означает обычную генерацию.
И у нас добавляется в нас в PassManager еще один Pass, BlockGenPlanningPass.
И если опять-таки вы работаете в виртуальном мире, не имеющем ничего общего с верификацией реальных процессоров, то это всё.
Но это не всё.
Потому что рано или поздно к вам приходит верификатор и говорит.
А мне не важно, сколько инструкций в базовом блоке.
Мне важно, чтобы базовый блок был 56 байт.
То есть, у вас могут быть запросы, а посмотрите сюда, я вернусь на этот слайд.
Здесь модальные режимы генплан, размеченные количеством инструкций в пачке.
Берешь пачку, пошел генерировать, берешь пачку, пошел генерировать.
Достаточно простой советской системы.
Эта простая советская система ломается, когда у нас возникает два вида запросов на генерацию.
Ну и, конечно, можно сделать свечи.
Тут уже из зала в другом контексте звучало идея сделать свечи.
Как вы поняли, я лично не очень хорошо отношусь к свечам.
И поэтому вместо свечей инженер, который работал над этой проблемой, сделал generation-request.
У нас может быть no-master generation-request, у нас может быть size generation-request.
У них у обоих может быть один интерфейс.
Давайте пока что оставим его статическим.
К сожалению, оставить его статическим не получилось.
Дело в том, что эту штуку
нужно уметь включать опции в runtime.
Если нужно уметь включать опции в runtime, она должна быть динамически полиморфной.
Мало того, запросы имеют тенденцию расти.
Смотрите, если свой запрос на генерацию есть у каждой функции, у каждого базового блока, у каждой группы инструкций внутри базового блока, и мало того некоторые запросы могут
с другими встречаться, а некоторые не могут.
Например, для бёрзг группы, для той самой группы из большого количества лодов.
Сайз моды не имеет смысла.
Там важно, чтобы было много лодов определенных количества.
Сайз не можем выдержать никак.
Сайз мод имеет смысл только для Plain Instruction Group.
И мы пришли.
в системе, но мы уже не боимся механизмов языка, мы не боимся виртуальных функций.
Мы не боимся также шаблонов.
Мы пришли к системе, которая выглядит логично и приятной.
Смотрите, у нас есть интерфейс и ИГГРР.
Мы их называем Игоремя.
А Instruction Group... А как бы второй называем, я не скажу?
Instruction Group Generation Request.
И этот Instruction Group Generation Request от него наследует от этого интерфейса настоящий Instragroup Generation Request, который при этом параметризован как шаблонным параметром, например, Number Instruction Generation Request или Size Generation Request.
А вот у нас есть также
Машин Basic Block Generation Request, который внутри содержит вектор Instruction Group Generation Request, но чтобы содержать Instruction Group Generation Request и проимитризованный другим шаблонным параметром, чем он сам, он должен содержать указатель на их базовый класс, который не шаблонный.
Иначе он не может.
Да, тут уже звучит так, все я сломался.
Вы думаете, это система сложная?
Нет, эта система не сложная.
Проблемы заключаются в следующем.
Представим себе, что у нас есть наш запрос на генерацию группы инструкций.
Этот запрос прометризован
А запросом, собственно, на генерацию по количеству, он наследует от этого запроса, чтобы переиспользовать функцию isCompleted.
Но при этом он также наследует от интерфейса.
Instruction group generation request.
Проблема заключается в том, что isCompleted должно быть вызвана, isCompleted, которая в nEGR должно быть вызвана из changepolicy.
Игоря.
То есть у нас должна быть делегация к сестринскому типу.
А делегация к сестринскому типу у нас означает, что виртуальные базовые классы, виртуальный базовый класс и комплитобал, и классическое ромбовидное наследование, частично с параметризацией как шаблонного параметра, тем, о чем наследуемся.
Как вы понимаете, я сейчас все это вымету из проектирования, но мы должны дойти до точки, мы должны запутать схему.
И у нас получается очень открытая и расширяемая схема.
Вот эти пунктирные стрелочки, знаете, есть выражение слаб язык человеческий, вот слаб язык UML.
Вот такая стрелочка пунктирная, как ведущая от NIGR к Type Instruction Group Generation Request, означает, что мы этим параметризованы, вернее, можем быть параметризованы.
И к тому же, если мы этим параметризованы, то мы должны от этого наследоваться.
В ЕМЛ такой стрелки просто нет, я ее придумал.
И у нас есть общий виртуальный интерфейс.
У нас есть Machine Basic Block Generation Request Interface для него, Machine Function Generation Request Interface для него, Instruction Group Generation Request Interface для него.
Теперь представьте, вы кто-то типа меня, вы архитектор проекта, вы приходите сюда, а вы давно сюда не заходили, там старшие инженеры ревьюют друг друга.
И что вы там видите?
Вы видите то, что так хорошо начиналось, как лапшу из виртуальных базовых классов.
Множественного наследования, полиси-классов, каких-то методов, делегирующихся через сестринский тип.
И оно все уже попало в open-source.
Если вы посмотрите историю проекта LVM-Snippy, мы уже успели за open-source-иться.
Когда все это начало возникать, вы все это можете в open-source посмотреть.
И у вас есть два варианта.
Оставить, как есть тщательно закомментировать или передизойнить.
Давайте кто за первый вариант?
Вау!
Есть те, кто зафирает!
Ура!
А кто за второй?
Ну, конечно.
Чем отличается так себе старший инженер от хорошего старшего инженера?
Когда приходишь к хорошему старшему инженеру, Юлия Тарасов, у которого проектировал хороший старший инженер, и говоришь ему, слушай, ну нам надо это передизайнить, он говорит, да, надо.
Давай думать, как передизайнить.
И мы начали думать.
Давайте применим wishful thinking и передизайним.
Function — это контейнер для базовых блоков.
По большому счету, Function Request должен быть контейнером для Basic Block Generation Request, который должен быть контейнером для Instruction Group Generation Requestов.
И единственное, что портит эту красивую картинку — это почти невидимое слово «полиси» внутри Instruction Group Generation Request.
Вот это полиси — это то, что должно быть на самом деле полиморфеном.
Они могут нести на себе странные, сложно параметризованные полиси.
Но если мы на секундочку выкинем полиси из головы, тогда проектирование становится простым приятным.
Смотрите.
Для того, чтобы сгенерировать базовый блок, все, что нам нужно, это пройтись по его Instruction Generation Request'ам и всех выполнить.
Приятно?
Правда, приятно.
а точно так же для функций.
И мы сделали вот так.
Давайте сделаем вот так.
Мы приватно унаследуемся от std-вектора, параметризованного Instruction Group Generation Request.
И приватно унаследуемся от std-map, поскольку Function Request — это что-то типа отображения из машины Basic Block указателя, это внутренняя структура LLVM, в собственный реквес для этого Basic Block.
Ну и тут эта машина-яркомпоратор, чудовищная вещь, но опять-таки абстрагируемся.
И дальше нам нужно решить одну маленькую проблему.
Смотрите, как бы мы хотели сгенерировать Instruction Group.
Что такое вообще генерация группы инструкций?
По большому счету, генерация группы инструкций ничем не отличается от генерации, которую у нас была, когда мы генерировали по плану.
Берем лопату, генерируем, генерируем, генерируем, закончили генерировать.
То есть это next, next, next, next, дошли до limit.
Я подсветил свиньям limit, next, limit, next.
К сожалению, здесь есть проблема.
То есть вот этот код, это реальный код, вы его прямо можете сейчас увидеть у нас в Никитхабе, но за счет чего он будет работать?
Почему?
Каким образом сюда проникнут все эти разные виды generation-request и так далее?
Куда делся весь полиморфизм?
Потому что смотрите, у нас теперь непараметризованные никакими шаблонными параметрами instruction-group-request, в которой просто хранится генполисия.
Кто уже догадался, к чему я клоню?
Ещё раз.
Какие типы?
Стирающие типы.
А скажите фамилию человека, который более всего известен своим продвижением такого рода архитектуры?
Шон Парнт.
Я клоню к Шону Парнт.
Смотрите, как мы могли бы спроектировать полиси?
Мы могли бы, я немного тут расширяю UML, делая все детали реализации, спрятанными внутрь генполиси.
Мы могли бы спрятать внутрь генполиси Next как концепт.
У нас есть ClassNext.
от которого наследует модель, тоже спрятанная внутрь генполисе, и храняющая полисе t, которым она параметризована.
И это может быть полисе генерации по количеству инструкций, по сайзу и так далее.
И у нас вот это наследование, это наследование настоящее, это виртуальные интерфейсы, там виртуальные функции, вы можете это выбирать в runtime, но оно скрыто, как деталь реализации generation-polis.
Мы использовали довольно неприятный механизм языка, а виртуальная функция довольно неприятный механизм языка.
Но мы использовали его хорошо его ограничив и ровно там, где нужно.
Дальше мы храним Unique Pointer на концепт, как Implementation, и Next просто делегирует Implementation Next, потому что это виртуальная функция.
И, конечно же, мы наделали кучу конструкторов.
20 перегрузок конструкторов, там 30 перегрузок конструкторов.
Нам не жалко.
Финальный вывод.
Да, подробнее про это можно посмотреть в докладе про динамический полиморфизм шоу-напарнта, это лучшее, что вы можете посмотреть.
А окончательный вывод.
Когда мы проектируем проекты и принимаем оприорные решения, в чем заключается роль человека, который принимает решение и контролирует их выполнение.
Во-первых, осознавать то, что вы не можете их не принимать.
Вы не можете взять и отложить оприорное решение.
Вы должны его принять, чтобы хоть что-то сдвинулось, чтобы что-то пошло.
Но вы при этом должны понимать, и мы это хорошо понимали, что они почти наверняка будут неправильными, будучи принятыми в первый раз.
И вы не должны давать им засохнуть в ваших проектах.
И в этом докладе мы видели два вида решений.
Я показал вам одно, которое было принято в LVM.
где постарались уйти от излишних виртуальных функций, ушли не туда, и оно увы засохло.
Я показал другое, которое было принято в нашем проекте.
И оно тоже было неправильно.
Мало того, оно было еще хуже.
Все это лапша.
Давайте я еще раз... Давайте вы еще раз насладитесь.
У нас осталась одна минута.
Когда я увидел эту штуку, а я увидел это в коде, я увидел это не такой красивой диаграммой.
Когда я увидел эту штуку в коде, а там как было?
Соответственно, какой-то джун начал туда что-то писать.
У него что-то развалилось, и он пришел ко мне за советом.
И тут я поглядел.
Это тоже было фундаментальное неправильное решение, которое, конечно, подумав, но если бы мы сразу его не приняли, мы бы не дошли до точки, где мы были способны это осознать.
И хороший программист не действует по идеологии, не действует по капризу и не действует по произволу.
Когда мы пишем наши программы, мы действуем по цели сообразности.
Закончить я бы хотел, маскотом нашего генератора.
Соответственно, я решил, что если генератор генерирует случайные программы, то у него вполне может быть случайно сгенерированный москот.
И мы всей командой, все выходные, отбирали разных москотов, в итоге выбрали эту.
Это называется снипетян.
Амо – это расширение в Risk5 и нейросеть сама его сгенерировала, я даже не просил.
Вот, это такое и обратите внимание сзади нее какие-то еще рандомные ассэмблерные строчки.
В общем, мне кажется, идеальный москот.
И пусть он на вас смотрит, пока вы будете задавать вопросы.
А у меня на этом все.
Всем спасибо.
Спасибо за доклад.
Поднимайся руки, задавайся вопросы.
Бижу руку в конце зала.
Раз, раз слышно?
Добрый день.
Да, большое спасибо за доклад.
Вопрос немного в сторону.
Видно, что проделал невероятный вопрос работы, но почему Open Source, а не коммерция, то есть почему?
Мы за Open Source или мягко, скажем, не все.
То есть, я очень долго пробивал руководство Open Source, и вот это как раз имеет отношение к тому, как меня представили.
Я все-таки преподаватель в университете.
У нас, как вы догадываетесь, гигантское количество пользователей на этот генератор и гигантский быклок на него.
И возможность в рамках лаборатории Risk5 в том же МФТИ привлекать студентов к развитию Open Source-ного генератора.
И мало того, возможность взаимодействовать с внешним Community через Open Source.
Она, во-первых, выдерживает идеологию Risk Five.
Risk Five сам по себе Open Source, правильно?
А во-вторых...
А мне очень хочется, чтобы люди перестали мучаться с отхочными генераторами.
И начали использовать компиляторные технологии для генераторов.
Это у меня прям вот такая вот идея.
Потому что я сам занимаюсь компиляторами, я люблю компиляторы.
Тестовый генератор — это обратная задача к компилятору.
То есть компилятор оптимизирует, а тестовый генератор, наоборот, деоптимизирует.
И у меня есть мечта, во-первых, создать вокруг этого генератора комьюнити, потому что синтакор не единственная компания, которая занимается RISC-5 Hardware, и мало того LVM-Snipe cross-платформленный генератор.
Вы можете написать для него backend для любого железа, для ARM, там для чего угодно.
Там просто еще не написано эти backend.
То есть я хочу развивать этот генератор, я хочу рано или поздно заобстримить его в LVM.
чтобы с одной стороны мы работали вместе со всем миром, но когда вы работаете вместе со всем миром, знаете, почему ЛЛВМ победил интелловский компилятор?
Потому что интелловский компилятор делала одна компания Intel, а ЛЛВМ делал весь мир, и в итоге Intel тоже стал делать ЛЛВМ.
И когда вы работаете вместе со всем миром, вы можете считать, что весь мир работает на вас.
И...
Я правильно понимаю, что сейчас к вам прилетают баги и так далее не только с российского рынка, но и вообще, в принципе, по миру.
По миру оно пока не очень распространено.
Я пока не сделал ни одного доклада и ни одной публикации на английском.
Я пока сконцентрировался на русскоязычном комьюнте.
Ну, по понятным причинам.
Ну что, зал?
Еще вопросы?
Вижу руку.
А передайте, пожалуйста, микрофон.
Если у кого-то взяли еще есть вопросы, поднимите руку, вам донесут микрофон.
Вот, вижу.
Там есть некоторые описания инструкции.
Для каждой инструкции все ее флаги, все атрибуты, все параметры, оно декларативно все.
Тейвиллген потом генерит все электроинструкции для этих штуки.
Для некоторых инструкций нередко нередко, а для некоторых надо ручками четко говорять.
Почему
Ой, сейчас надо собраться мыслями.
Я сразу вижу разработчика компиляторов.
Давайте пока туда, а Алексей пока соберется с мыслями.
Я уже собрался.
Почему не использовать?
Есть фазеры, которые были созданы для того, чтобы создать тестовое покрытие для кланга, грубо говоря.
Почему по аналогии нельзя было сделать так же?
Я понял мысли.
Смотрите, фазер для кланга генерируется исходный код на высокоуровнем языке.
Кланг — это воскоролевый компилятор.
От воскоролевого языка, когда мы хотим тестировать железо, это очень сложно.
Там мало что остается.
Соответственно, мало того, задача фазинга, она немножко другая.
Задача фазинга — это когда у вас есть код, вы его там модифицируете по границам API.
LVM-Sneeper многие тоже называют фазером, мне кажется, это в корне неправильно, потому что оно именно что генерирует инструкции по седу, оно не отталкивается от изначального состояния.
ЛВМ Снепи по большому счету генерирует машина-яр.
Существующего генератора машина-яра, ну такого рандомного генератора машина-яра, кроме ЛВМ Снепи, к сожалению, не было.
Пропускать его через Instruction Lowering это означает терять практически все свойства, которые нам интересны.
Если плюс-плюс развесистая дерева, а и ст, то его полное покрытие сгенерить очень сложно.
Просто ожичаясь способности, должна быть какая-то гигантская.
На нижнем уровне у нас 13 инструкция группы Горява и Львием, которые как-то понижаются.
Ну, просто интерпей не настолько большая, можно было сделать что-нибудь такое.
Нет, нет, нет.
А у нас довольно много инструкций.
Я же говорю.
РВВ, например.
РВВ — это 600 апкадов.
Из этих 600 апкадов LLVM генерирует, ну, сколько там?
Ну, максимум там.
Несколько десятков, остальные в нем просто описаны и доступны через билтыны.
Можно, конечно, поставить задачу так, что генерировать AST в нем, генерировать билтыны, и потом молиться, что ловеринг ничего не сломает, когда мы от этих билтынов будем к ассамблеру переходить.
Но зачем, если можно генерировать сразу машина Страхшина?
Спасибо.
Видела еще одну руку.
А вот тут же была рука, человек поднимает.
Раз, раз.
Здравствуйте.
Спасибо за доклад.
Вопрос такой большой бизнесовый.
А как вы продавали высшему менеджему эту идею о новом рефакторинге?
Обычно это, как бы, очень не любят.
Будет хотя бы больше фичей.
И как бы слово рефакторинг обычно очень сильно морозится.
И это да.
Ну как?
Это такой прям вопрос ко мне, как и начальнику отдела.
Но это было сложно.
Смотрите, мы на технической конференции.
Давайте я как-нибудь приду на темлицкую.
Спасибо.
Вот тут была рука.
Поднимите ее еще раз.
Вы же поднимали.
Спасибо за доклад.
У меня небольшой вопрос.
Просто мне немножко удивило.
Там, когда вы сделали новое решение, там было пару классиков, и они наследовались от один от вектора, другой от мапа.
А почему наследование, а не агрегация?
Почему приватное наследование, а не агрегация?
Чтобы иметь возможность сделать просто using bigin из 100 вектора.
И не переписывать его как в рэппер.
Приватное наследование, чем отличается от агрегации?
Возможностью сделать using.
методов того класса, от которого вы наследуете возможность ввести их в свой scope.
Если вы делаете агрегацию, то из агрегируемого класса в свой scope вы ничего ввести не можете.
Если вы посмотрите на наш github, то это там становится очевидным.
Мы делаем using там почти всех важных методов этого вектора, а иначе нам пришлось бы писать однообразные варпера.
То есть это сделано для того, чтобы... Сократить количество года.
Да-да-да.
Итерироваться как по массиву.
Да, конечно-конечно.
Мы на самом деле, то есть для нас тут вектор, как... Вот Бэзик, блок генеральный реквест, тут вектор, это деталь реализации.
Мы могли сделать композицию.
Это просто немножко больше кода.
И мне очень нравится идея вводить имена из приватных базовых классов в публичную область видимости.
Она пахнет си пласт-пласт.
Спасибо.
Так, вижу еще одну руку, передайте, пожалуйста, микрофон, а пока озвучиваю вопросы с чата.
Спрашивает, как вы определяете весаинструкции?
Откуда берется статистика?
Случайно или как-то по-другому?
Весаинструкции определяю не я, а гистограммы пишут пользователь, то есть верификатор.
Спасибо.
Раз, два, три.
Спасибо за доклад.
По сути весь доклад мы говорили о избавлении от виртуальных функций и ускорении происходящего.
В какой-то момент на слайде было наследование от стедмапа.
Это очень смутило в контексте производительности.
Это просто код со слайдов?
Или у вас реально красночерные деревья?
Смотрите, во-первых, избавление от виртуальных функций было не для ускорения происходящего.
Избавление виртуальных функций вообще не ускоряет происходящее.
Обычно.
Соответственно, виртуальные функции переоценены, чтобы избавление от них ускорило происходящее.
Вам нужно, чтобы у вас были очень короткие виртуальные функции, чтобы было много.
Если у вас большие виртуальные функции, то все расходы на виртуальность съедаются размером.
А почему наследование от STUDEMAP?
Опять-таки здесь мы избавлялись от всего этого для упрощения кода.
Мы хотим иметь простой, легко поддерживаемый код.
STUDEMAP-фанкшн-реквестия.
Когда вы делаете реквест на генерацию целой функции, вы представьте вот генерация функции, которая тестирует аппаратуру, сколько там всего внутри напихано и так далее.
Аверхит на студмэп, господи, да копейки.
Зато студмэп — это такой ван-фу-рол, ван-фу-рол штука.
Ее все знают.
И у нее прекрасные гарантии поэтераторам, которых нет ни одной из ее альтернатив.
Потому что все альтернативы студмэп быстрее не ее, как раз за счет того, что они ухудшают ее гарантии поэтераторам.
Правильно?
Ну вот.
Тогда напрашивается следующий вопрос.
А зачем изначально вот эта архитектура, которая вышла потом дурацкой с виртуальным наследованием?
Почему там появились шаблоны?
Это изображение красивости кода было сделано о непроизводительности?
Ну, смотрите, там появились шаблоны, потому что изначально мы хотели сделать эстетический интерфейс.
Я прошел этот момент.
То есть сначала хотелось, чтобы это был просто концепт, чтобы это был эстетический интерфейс.
А потом оказалось, что пользователь хочет ручку в ронтайме и пришлось наследоваться.
А шаблоны остались?
Так бывает.
Ну, то есть случайно вышло по сути, да?
Почему случайно?
Они там обоснованы.
Динамический интерфейс там тоже появился.
Но вы параметризуетесь с чем-то и сразу от него же наследуетесь.
То есть вы знаете, от кого вы от наследовались, потому что вы от наследовались от него и дальше используете его методы через его соответственный интерфейс.
То есть, вы отнаследовались на мысль «Generation Request», но вам неплохо бы, чтобы информация о том, что вы от него отнаследовались.
У вас была на этапе компиляции, и поэтому вы себя им же и параметризовали.
Ну, в общем, понятно, более-менее.
Спасибо.
На этой прекрасной ноте мы благодарим спикера за прекрасные доклады ответа на вопросы.
Спасибо.
Проблемы и решения в крупных проектах на примере LLVM-snippy, Константин Владимиров, C++ Zero Cost Conf | Целено