Сравнение одинакового проекта в Rust, Haskell, C++, Python, Scala и OCaml |
||
МЕНЮ Искусственный интеллект Поиск Регистрация на сайте Помощь проекту ТЕМЫ Новости ИИ Искусственный интеллект Разработка ИИГолосовой помощник Городские сумасшедшие ИИ в медицине ИИ проекты Искусственные нейросети Слежка за людьми Угроза ИИ ИИ теория Внедрение ИИКомпьютерные науки Машинное обуч. (Ошибки) Машинное обучение Машинный перевод Реализация ИИ Реализация нейросетей Создание беспилотных авто Трезво про ИИ Философия ИИ Big data Работа разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика
Генетические алгоритмы Капсульные нейросети Основы нейронных сетей Распознавание лиц Распознавание образов Распознавание речи Техническое зрение Чат-боты Авторизация |
2019-06-24 14:26 В последнем семестре университета я выбрал курс компиляторов CS444. Там каждая группа из 1-3 человек должна была написать компилятор из существенного подмножества Java в x86. Язык на выбор группы. Это была редкая возможность сравнить реализации больших программ одинаковой функциональности, написанных очень компетентными программистами на разных языках, и сравнить разницу в дизайне и выборе языка. Такое сравнение породило массу интересных мыслей. Редко можно встретить такое контролируемое сравнение языков. Оно не идеально, но намного лучше, чем большинство субъективных историй, на которых основано мнение людей о языках программирования. Мы сделали наш компилятор на Rust, и сначала я сравнил его с проектом команды на Haskell. Я ожидал, что их программа будет намного короче, но она оказалась того же размера или больше. То же самое для OCaml. Затем сравнил с компилятором на C++, и там вполне ожидаемо компилятор был примерно на 30% больше, в основном, из-за заголовков, отсутствия типов sum и сопоставлений с образцом. Следующее сравнение было с моей подругой, которая сделала компилятор самостоятельно на Python и использовала менее половины кода, по сравнению с нами, из-за мощности метапрограммирования и динамических типов. У другого товарища программа на Scala тоже была меньше нашей. Больше всего меня удивило сравнение с другой командой, которая тоже использовала Rust, но у них оказалось в три раза больше кода из-за разных дизайнерских решений. В конце концов, самая большая разница в количестве кода оказалась в пределах одного языка! Содержание
Почему я считаю это содержательным Прежде чем вы скажете, что количество кода (я сравнил как строки, так и байты) — ужасная метрика, хочу заметить, что в данном случае она может обеспечить хорошее понимание. По крайней мере, это наиболее хорошо контролируемый пример, когда разные команды пишут одну и ту же большую программу, о котором я слышал или читал.
Таким образом, я думаю, что количество кода обеспечивает приличное понимание того, сколько усилий потребуется для поддержки каждого проекта, если бы он был долгосрочным. Думаю, что не слишком большая разница между проектами также позволяет опровергнуть некоторых экстраординарные утверждения, которые я читал, например, что компилятор на Haskell будет более чем вдвое меньше C++ в силу языка. Rust (основа для сравнения) Я и один из моих товарищей раньше каждый написал более 10k строк на Rust, а третий коллега написал, возможно, 500 строк на каких-то хакатонах. Наш компилятор вышел в 6806 строк Haskell В команду Haskell входило два моих друга, которые написали, возможно, пару тысяч строк Haskell каждый, плюс прочитали много онлайн-контента о Haskell и других подобных функциональных языках, таких как OCaml и Lean. У них был ещё один товарищ по команде, которого я не очень хорошо знал, но, похоже, сильный программист и раньше использовал Haskell.
Эти различия плюс тесты объясняют все различия в объёме. На самом деле наши файлы для свёртывания констант и разрешения контекста очень близки по размеру. Но всё равно остаётся некоторая разница в байтах из-за более длинных строк: вероятно, потому что требуется больше кода для перезаписи целого дерева на каждом проходе. В итоге, отложив решения в области дизайна, по-моему, Rust и Haskell одинаково выразительны, возможно, с небольшим преимуществом Rust из-за способности легко использовать мутацию, когда это удобно. Было также интересно узнать, что мой выбор метода рекурсивного спуска и рукописный лексический анализатор окупились: это был риск, который противоречил рекомендациям и указаниям профессора, но я решил, что так легче и это правильно. Поклонники Haskell возразят, что та команда, вероятно, не использовала в полной мере возможности Haskell, и если бы они лучше знали язык, то могли бы сделать проект с меньшим количеством кода. Согласен, кто-то вроде Эдварда Кметта может написать тот же компилятор в значительно меньшем объёме. Действительно, команда моего друга не использовала много причудливых суперпродвинутых абстракций и причудливых библиотек комбинаторов, таких как lens. Однако всё это сказывается на читаемости кода. Все люди в команде — опытные программисты, они знали, что Haskell способен на очень причудливые вещи, но решили не использовать их, потому что решили, что на их понимание потребуется больше времени, чем они сэкономят, и сделает код сложнее для понимания другими. Это кажется мне реальным компромиссом, а утверждение, что Haskell волшебно подходит для компиляторов, переходит в нечто вроде «Haskell требует чрезвычайно высокой квалификации для написания компиляторов, если вы не заботитесь о поддержке кода людьми, которые также не очень искусны в Haskell». Ещё интересно отметить, что в начале каждого проекта профессор говорит, что студенты могут использовать любой язык, который работает на университетском сервере, но предупреждает, что команды на Haskell отличаются от остальных: у них самый большой разброс в оценках. Многие переоценивают свои способности и у команд на Haskell больше всего плохих оценок, хотя другие справляются отлично, как мои друзья. С++ Затем я поговорил с моим другом из команды C++. Я знал только одного человека в этой команде, но C++ используется на нескольких курсах в нашем университете, поэтому, вероятно, у всех в команде был опыт C++.
Ещё мы сравнивали время компиляции. На моём ноутбуке чистая отладочная сборка нашего компилятора занимает 9,7 с, чистый релиз 12,5 с, а инкрементная отладочная сборка — 3,5 с. У моего друга не было таймингов под рукой для их билда C++ (используя parallel make), но он сказал, что цифры похожие, с оговоркой, что они помещают реализации множества небольших функций в заголовочные файлы, чтобы уменьшить дублирование сигнатур ценой более длительного времени (именно поэтому я не могу измерить чистые накладные расходы на строки в заголовочных файлах). Python Мой друг, очень хороший программист, решила сделать проект в одиночку на Python. Она также реализовала больше дополнительных функций (для удовольствия), чем любая другая команда, включая промежуточное представление SSA с распределением регистров и другими оптимизациями. С другой стороны, поскольку она работала в одиночку и реализовывала множество дополнительных функций, она уделяла наименьшее внимание качеству кода, например, бросая недифференцированные исключения для всех ошибок (полагаясь на бэктрейсы для отладки) вместо того, чтобы реализовать типы ошибок и соответствующие сообщения, как у нас. Rust (другая группа) Cамое интересное для меня сравнение было с моим другом, который тоже делал проект в Rust с одним товарищем по команде (которого я не знал). У моего друга был хороший опыт Rust. Он внёс вклад в разработку компилятора Rust и много читал. О его товарище ничего не знаю.
Я не смотрел на код проходов анализа их компилятора, но они тоже велики. Я поговорил со своим другом, и, похоже, они не реализовали ничего похожего на инфраструктуру визиторов, как у нас. Предполагаю, что это, наряду с некоторыми другими меньшими различиями в дизайне, объясняет разницу в размере этой части. Визитор позволяет нашим проходам анализа обращать внимание только на части AST, в которых они нуждались, вместо того, чтобы сопоставлять шаблон по всей структуре AST. Это экономит много кода. Их часть для генерации кода состоит из 3594 строк, а наша — 1560. Я посмотрел на их код и кажется, что почти вся разница в том, что они выбрали промежуточную структуру данных для инструкций ассемблера, где мы просто использовали форматирование строк для прямого вывода ассемблера. Им пришлось определять типы и функции вывода для всех используемых инструкций и типов операндов. Это также означало, что построение инструкций ассемблера заняло больше кода. Где у нас был оператор форматирования с короткими инструкциями, такими как mov ecx, [edx] , им нужен был гигантский оператор rustfmt , разбитый на 6 строк, которые строили инструкцию с кучей промежуточных вложенных типов для операндов, включающих до 6 уровней вложенных скобок. Мы также могли выводить блоки связанных инструкций, таких как преамбула функции, в одном операторе форматирования, где им пришлось указывать полную конструкцию для каждой инструкции.Наша команда рассматривала возможность использования такой абстракции, как у них. Было проще иметь возможность либо выводить текстовую сборку, либо непосредственно выдавать машинный код, однако это не являлось требованием курса. То же самое можно было сделать с меньшим количеством кода и лучшей производительностью, используя трейт X86Writer с методами вроде push(reg: Register) . Мы учитывали также, что это могло бы упростить отладку и тестирование, но мы поняли, что просмотр сгенерированного текстового ассемблера на самом деле легче читать и тестировать с помощью тестирования моментальных снимков, если либерально вставлять комментарии. Но мы (видимо, правильно) предсказали, что это займёт много дополнительного кода, и не было никакой реальной выгоды, учитывая наши реальные потребности, поэтому мы не беспокоились. Хорошо сравнить это с промежуточным представлением, которое команда C++ использовала в качестве дополнительной функции, что отняло у них всего 500 дополнительных строк. Они использовали очень простую структуру (для простых определений типов и кода построения), которая использовала операции, близкие к тому, что требовала Java. Это означало, что их промежуточное представление было намного меньше (и, следовательно, требовало меньше кода построения), чем результирующий ассемблер, поскольку многие языковые операции, такие как вызовы и приведения, расширялись во многие ассемблерные инструкции. Они также говорят, что это действительно помогло отладке, так как вырезало много мусора и улучшала читаемость. Представление более высокого уровня также позволило сделать некоторые простые оптимизации на их промежуточном представлении. Команда C++ придумала действительно хороший дизайн, который принёс им гораздо больше пользы с гораздо меньшим количеством кода.В целом, похоже, что общая причина трёхкратной разницы в объёме обусловлен последовательным принятием различных проектных решений как больших, так и малых в направлении большего количества кода. Они реализовали ряд абстракций, которые мы не сделали — они добавили больше кода, и пропустили некоторые из наших абстракций, которые уменьшают количество кода. Этот результат действительно удивил меня. Я знал, что дизайнерские решения имеют значение, но я бы не догадался заранее, что они приведут к каким-либо различиям такого размера, учитывая, что я только обследовал людей, которых я считаю сильными компетентными программистами. Из всех результатов сравнения это самый значительный для меня. Наверное, мне помогло, что я много читал о том, как писать компиляторы, прежде чем пошёл на этот курс, поэтому мог воспользоваться умными проектами, которые другие люди придумали и нашли хорошо работающими, вроде визиторов AST и метода рекурсивного спуска, хотя их не преподавали у нас на курсе. Что действительно заставило меня задуматься — это стоимость абстракции. Абстракции могут облегчить расширение в будущем или защитить от некоторых типов ошибок, но их нужно учитывать с учётом того, что вы можете получить в три раза больше кода для понимания и рефакторинга, в три раза больше возможных мест для ошибок и меньше времени на тестирование и дальнейшую разработку. Наш учебный курс отличался от реального мира: мы точно знали, что никогда не коснёмся кода после разработки, это исключает преимущества упреждающей абстракции. Однако, если мне пришлось бы выбрать, какой компилятор расширить произвольной функцией, которую вы скажете позже, я бы выбрал наш, даже не учитывая своё знакомство с ним. Просто потому что в нём намного меньше кода, который нужно понять, и я мог бы потенциально выбрать лучшую абстракцию для требований (например, промежуточное представление команды C++), когда буду знать конкретные требования. Также в моём представлении укрепилась таксономия абстракций: есть те, которые сокращают код, учитывая только текущие требования, как наш шаблон визитора, а есть абстракции, которые добавляют код, но обеспечивают преимущества расширяемости, отладочности или корректности. Scala Я также поговорил с другом, который в предыдущем семестре делал проект на Scala, но проект и тесты были точно такими же. Их компилятор состоял из 4141 строки и ~160 КБ кода, не считая тестов. Они прошли 8 из 10 секретных тестов и 100% открытых тестов и не реализовали никаких дополнительных функций. Таким образом, по сравнению с нашими 5906 строками без дополнительных функций и тестов, их компилятор меньше на 30%. OCaml Поскольку все члены нашей команды проходят стажировку в Jane Street (технологическая компания вычислительного трейдинга — прим. пер.), мне было особенно интересно посмотреть на результат других бывших стажёров Jane Street, которые выбрали для написания компилятора язык OCaml. Их компилятор составлял 10914 строк и 377 КБ, включая небольшое количество тестового кода и никаких дополнительных функций. Они прошли 9/10 секретных тестов и все публичные тесты. Заключение В целом я очень рад, что сделал это сравнение, я многому научился и много раз удивлялся. Думаю, общий вывод заключается в том, что дизайнерские решения имеют гораздо большее значение, чем язык, но язык имеет значение, поскольку даёт вам инструменты для реализации разного дизайна. Источник: habr.com Комментарии: |
|