"Наш путь извилист, но перспективы светлые" Мао Цзедун

Здравствуйте, уважаемые хабровчане. Меня зовут Вадим Писаревский, я являлся лидером OpenCV (Open Source Computer Vision Library) на протяжении примерно 20 лет, и продолжаю участие в этом замечательном проекте. В этой статье я рад представить вашему вниманию результат другого своего проекта, над которым в фоне работаю уже много лет, а последние пару лет как минимум половину своего рабочего времени.

Краткая информация о языке

Ficus/Фикус это мульти-парадигменный язык программирования общего назначения, прежде всего функциональный, но также поддерживается императивная модель и некоторые объектно-ориентированные возможности. Главная "фишка" языка – это заточенность на обработку числовой и символьной информации. По сути, это язык для программирования разнообразных алгоритмов, от которых требуется высокая скорость работы и надежность при этом (например, алгоритмы компьютерного зрения и искусственного интеллекта по идее должны работать на самом разном железе, часто в условиях ограниченности вычислительных ресурсов. И не должны ломаться, желательно никогда). Компилятор Фикуса выдает на выходе код на языке Си неплохого качества, из которого можно получить готовое приложение, либо же скомпилировать в составе некоего более крупного проекта. Компилятор изначально был написан на OCaml, но уже почти как год переведен на сам Фикус. Все вместе, компилятор, стандартная библиотека, примеры, тесты и т.д. распространяются по лицензии Apache 2. В репозитории имеется небольшой учебник, распространяемый по лицензии Creative Common License.

Немного истории, или как меня угораздило взяться за новый язык

"Какой самый живучий паразит? Бактерия? Вирус? Кишечный глист? Идея. Она живуча и крайне заразна. Стоит идее завладеть мозгом, избавиться от неё уже практически невозможно." Кобб, фильм Inception

Считаю нужным с самого начала ответить на неминуемый вопрос "зачем?", ибо полностью согласен с Бобом Дэвисом, одним из создателей Intel VTune, а также автором самого первого инсталлятора OpenCV, который мне как-то сказал: "знаешь, если есть возможность обойтись без написания программы, лучше не писать – и ошибок не будет, и сопровождать не надо".

Началось проектирование языка примерно в 2011 году, когда мы готовили предложение в комитет Кронос по стандарту API компьютерного зрения OpenVX на базе OpenCV. Многие компании были заинтересованы в таком стандарте, чтобы потом предложить свои решения, прежде всего аппаратные, которые были бы дружественны пользователям OpenCV и более менее согласованы между собой по программным интерфейсам. С самого начала было понятно что OpenCV слишком большой и динамичный проект для стандартизации, поэтому искали некий базис операций минимального размера и при этом обладающий максимальными выразительными возможностями. Проблема была не только и не столько в выборе конкретного набора примитивных операций, но в определении способа составления из них более сложных алгоритмов обработки изображений и компьютерного зрения. Рассматривались следующие подходы:

  • Библиотечный или последовательный — самый примитивный, т.е. тот, который предоставляет классический интерфейс OpenCV. Последовательно вызываем функции из предоставляемого набора. Если нужные функции отсутствуют, реализуем их самостоятельно и вставляем в середину. Этот подход сочетает необыкновенную простоту и большую гибкость с очень низкой эффективностью, поскольку каждая операция прокачивает все данные через кэши.

  • Графовый — тот, который в итоге был выбран комитетом OpenVX, который в OpenCV реализуется в модуле G-API (graph API), и который сегодня также реализован в различных движках по запуску нейронных сеток. Подход заключается в том что реализуемый алгоритм представляется в виде направленного ациклического графа (DAG), вершины которого являются примитивными операциями. Такой граф может быть сформирован вручную или автоматически с помощью метода ленивых вычислений (который как раз и реализован в OpenVX и в OpenCV G-API). Коль скоро граф построен, его можно проанализировать, объединить некоторые операции вместе (т.е. осуществить слияние циклов), разбить массивы на блоки чтобы задействовать несколько ядер, возможно выполнить часть или все вычисления на GPU и прочих ускорителях. Эффективность качественных реализаций графового подхода в разы выше библиотечного варианта. Существенный недостаток такого подхода - невысокая гибкость. Как только алгоритм перестает выражаться с помощью доступного базиса операций, как только появляется динамическая логика, мы вместо одного графа получаем много мелких и эффективность подхода резко снижается. При этом очень много усилий тратится и ухищрений используется в попытках выразить нетривиальный алгоритм в виде такого графа.

  • Наконец, языковой подход. Проектируется язык, либо domain-specific, либо общего назначения, который бы особенно эффективно работал с массивами. Пишется компилятор такого языка, как правило, JIT компилятор, который переводит алгоритмы в бинарный код, возможно с вызовом функций из runtime, выполняющих примитивные операции над блоками из обрабатываемых массивов, так что данные остаются в кеше. Эффективность максимальна, гибкость при правильном выборе операций и конструкций языка также достаточна. Недостатки - самая высокая сложность реализации среди представленных подходов, а также потенциально высокий накладной расход, особенно в случае JIT-компилятора, ведь со своим приложением нужно поставлять и компилятор, и если он например базируется на LLVM - это десятки мегабайт. Тем не менее, популярные решения на базе этого подхода также имеются. Например это язык APL для вычислений вообще, до сих пор применяющийся в финансовой сфере, язык Halide для обработки изображений или фреймвок tvm для машинного обучения, созданный на основе кода Halide.

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

C++ и Python для OpenCV и что с ними не так

Возникает вопрос — зачем создавать новый язык, если есть быстрый C++ и удобный Python?

OpenCV изначально была написана на C, в 2009 году интерфейс был кардинально переделан на C++ по мотивам Matlab, прежде всего его всемогущих массивов. Прежде всего хотелось удобства обращения с массивами почти как в Matlab, чего в итоге и добились. В то же время, по мере роста размера и сложности библиотеки, по мере портирования алгоритмов на различные платформы, по мере повышения требований к производительности стали все больше проявляться следующие очевидные недостатки языка C++ как языка для реализации алгоритмов компьютерного зрения:

  • отсутствие встроенного типа многомерных плотных массивов, из-за чего:

    • каждая библиотека реализует свой тип массивов (ну кроме std::vector<>, std::array<>), поэтому при совместном использовании нескольких библиотек нужно перегонять данные из одного формата в другой, желательно эффективно.

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

    • проверка на диапазон либо выполняется всегда (обычно в debug-режиме), либо никогда (как правило в релизной сборке). Такой подход как-то сравнили с ситуацией когда человек надевает спасательный жилет во время обучения плаванию в мелком бассейне, но потом отправляясь в открытое море жилет благополучно оставляет дома.

  • до появления стандарта C++ 20 отсутствие полноценной поддержки модулей. Даже сейчас поддержка модулей только-только появляется в компиляторах и есть различия от версии к версии.

  • эффективный CPU код писать вручную кропотливо, хотя часто используются одни и те же приемы. Например, в одном из своих докладов я приводил пример, когда референсная реализация некоего фильтра на 70 строчек кода, работающая со скоростью 640x480@1FPS, была превращена в оптимизированную версию на 350 строчек кода работающую со скоростью 640x480@30FPS. То есть, не выходя за рамки C++ получилось разогнать код в 30 раз, но ценой усложнения реализации примерно в 5 раз. Особенно заметно бессилие 'оптимизирующих' и 'векторизующих' компиляторов C++ становится заметно в целочисленных вычислениях с пониженной точностью (int8, int16)

  • отсутствие поддержки GPU на уровне языка. Экспериментальная поддержка есть в стандартах OpenMP и OpenACC, но о возможности реализовать надежные кросс-платформенные решения речи пока не идет. Есть OpenCL и CUDA, но их использование за рамками простых книжных примеров требует поистине экспертного уровня. При этом тренды в вычислениях очевидны, компьютерное зрение и машинное обучение скорее рано чем поздно полностью мигрируют на GPU и специализированные ускорители:

Взято из https://www.nextplatform.com/2019/07/10/a-decade-of-accelerated-computing-augurs-well-for-gpus/
Взято из https://www.nextplatform.com/2019/07/10/a-decade-of-accelerated-computing-augurs-well-for-gpus/

Таким образом, потребность в полноценном языке или хотя бы Domain-Specific Language (DSL) для компьютерного зрения и машинного обучения становится очевидной, хотя бы даже для упрощения запуска алгоритмов на GPU.

Часто выбирают вариант с DSL, который встраивают в один из распространенных языков, типа C++, C# или Python. Компилятор для DSL и сделать проще и "продать" проще, т.к. не надо пересаживать людей с основного языка который используется для продукта, но:

  1. Гибридный код сложнее отлаживать.

  2. В случае JIT компилятора появляется накладной расход при распространении конечных приложений, а возможно и лицензионные вопросы.

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

Конкретизация идеи языка

Таким образом, после некоторых размышлений было решено сделать:

  • полноценный язык, чтобы не было необходимости использовать два языка в программе. Вместо постепенного расширения синтаксиса и семантики DSL языка (а в подобных расширениях необходимость возникает примерно в 100% случаев) было решено сразу сделать универсальное решение, пусть и не самое быстрое, и по мере необходимости добавлять распознавание особых паттернов в компилятор (то есть, делать его умнее) и добавлять новые интринзики для увеличения скорости полученного кода.

  • с автоматическим управлением памятью

  • с полноценной поддержкой массивов

  • с полноценной поддержкой модулей

  • настолько же быстрый как C++, в потенциале быстрее, за счет автоматического портирования некоторых циклов на GPU

  • настолько же удобный и выразительный как Python, хоть пока без интерактивного режима, к сожалению

  • со строгой статической типизацией, чтобы одновременно устранить две главные проблемы Python

  • с генерацией на выходе кода на C/C++, чтобы с одной стороны упростить себе жизнь и переиспользовать все существующие компиляторные технологии, а с другой стороны не зависеть полностью от такого монстра как LLVM. Дополнительное преимущество получилось в том что полученный код можно включить в состав более крупного проекта на C/C++.

  • с легким runtime, в отличие от C#, Java и даже Python. В частности поэтому отказался от полноценного сборщика мусора в пользу механизма подсчета ссылок.

Это были общие, вполне логичные решения. Оставалось определиться, как это все должно выглядеть, какие будут языковые конструкции, какой синтаксис. Изначально идея была взять за основу любимый Python, а возможно и одну из его вариаций, типа Cython, встроить туда поддержку многомерных массивов, и дело сделано! Я придерживался этой идеи пока случайно не познакомился с языками OCaml и StandardML, а также вообще с функциональным программированием, про которое я конечно слышал, и даже в свое время немного изучал Lisp, но глубоко не копал. Теоретически эти языки обладали всеми нужными свойствами кроме первоклассной поддержки многомерных массивов (выходной язык компилятора и метод сборки мусора относится скорее к деталям реализации, так что они не считались фундаментальными недостатками), и кроме этого синтаксис был слишком непривычен для меня как для пользователя C++ и Python, хотя автоматический вывод типов позволяет писать строго-типизированные программы с минимумом явных аннотаций, тем самым немного приближая код к Python-стилю.

Синтаксис, как оказалось впоследствии, довольно несложно переделать на что-нибудь более современное, и даже по мере развития проекта менять один синтаксис на другой, даже особо не затрагивая т.н. Abstract Syntax Tree (абстрактное древовидное представление синтаксиса, генерируемое парсером).

Оставался вопрос с поддержкой массивов.

Array comprehensions (буду благодарен за перевод термина)

Одна из ключевых особенностей функциональных языков - это повсеместное использование неизменяемых значений вместо переменных и неизменяемых типов данных, таких как односвязные списки с добавлением в начало, рекурсивные и нерекурсивные типы-суммы/варианты и т.д. В том числе благодаря этой особенности обеспечивается магия функциональных языков, когда программы начинают работать правильно почти сразу после того как их наконец получилось скомпилировать (как кто-то когда-то пошутил).

К сожалению, с массивами так не получается. В компьютерном зрении и в обработке изображений должна быть возможность быстро, очень быстро прочитать и поменять значения отдельных элементов массива, отдельных пикселей на изображении и т.д.

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

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

val A = load_gray_image(imagename)
val (h, w) = size(A)
val B: uint8 [,] =
    make_array_uninit((h-2, w-2)) // гипотетическая функция
                                // создания неинициализированного
                                // массива
// применяем фильтр [|0, 1, 0; 1, 4, 1; 0, 1, 0|]/8
for y <- 1:h-1
  for x <- 1:w-1 {
     B[y-1, x-1] = uint8((A[y-1,x]+A[y,x-1]+A[y,x]*4+
                          A[y,x+1]+A[y+1,x])>>3)
  }

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

Изучение вопроса привело к языкам SISAL и SAC (Single-Assignment C). В этих языках присутствует понятие array comprehensions, которые выглядят как циклы for, но тело цикла вычисляет отдельный элемент массива результирующего вектора. Прямо как list comprehensions в Python, но только многомерные. В результате, тот же самый код теперь можно записать как:

val A = load_gray_image(imagename)
val (h, w) = size(A)
// применяем фильтр [|0, 1, 0; 1, 4, 1; 0, 1, 0|]/8
val B = [| for y <- 1:h-1 for x <- 1:w-1 {
     uint8((A[y-1,x]+A[y,x-1]+A[y,x]*4+
            A[y,x+1]+A[y+1,x])>>3)
      }|]

Т.е. array comprehension выглядят как обыкновенные циклы for, но только они завернуты в скобки [| |], что позволяет их правильно классифицировать и обработать. С одной стороны, массивы по-прежнему остаются изменяемыми (мутабельными) структурами данных. С другой стороны, благодаря comprehensions некоторые операции работы над массивами можно выразить в функциональном стиле, используя принцип "single-assignment" - то что так обожают все оптимизирующие компиляторы. Также уменьшается простор для возможных ошибок и опечаток. В частности, тут B создается нужной размерности, размера и типа, элементы вычисляются гарантированно последовательно и независимо, сама конструкция синтаксически гарантирует инициализацию каждого элемента без пропусков и повторений. Для автоматического векторизатора такую нотацию гораздо проще обработать.

Заодно, упомянем про еще одно интересное свойство Фикуса. С одной стороны, Фикус гарантирует проверку на диапазон при доступе к элементам массива. С другой стороны, при анализе этого цикла компилятор увидит что доступ к массиву A обеспечивается с помощью т.н. "афинных" (т.е. линейных) операций над счетчиками циклов, поэтому он просто возьмет и для каждой из 5 операций доступа возьмет крайние значения индексов и до цикла проверит что они в пределах массива. Если нет, будет брошено исключение. При этом проверки на диапазон внутри цикла будут полностью удалены. Это уже реализованная оптимизация, которая стала реальной поскольку компилятор знает про многомерные массивы. В современном C++ такая оптимизация невозможна.

Оптимизирующий компилятор Фикуса. Пример со слиянием циклов

Вообще, несмотря на младенческий возраст, компилятор Фикуса гораздо более продвинут чем компилятор Python и даже местами умнее C++.

Промежуточный код проходит через несколько полных циклов оптимизации, каждый из которых включает следующие стадии:

  • устранение неиспользуемого кода

  • замена хвостовой рекурсии на циклы

  • вынос инвариантов цикла за его пределы

  • замена некоторых операций над матрицами, например A'*B+С, на специальные интринзики (gemm в данном случае)

  • подстановка тел небольших функций вместо их вызова

  • раскрытие блоков кода, вложенных в другие линейные блоки кода (flatten). Часто полезно после подстановки функций.

  • слияние циклов

  • устранение избыточных проверок на диапазон при последовательном обращении к элементам массива внутри цикла (см. описание выше)

  • замена константных выражений на их результаты, устранение недостижимых веток в условных операторах

отдельно после всех этих оптимизаций применяется вынос вложенных и lambda-функций наверх и остальная подготовка к генерации финального С кода.

Давайте рассмотрим на примере как работает такая многостадийная оптимизация:

/* обобщенная реализация операции поэлементного сложения
   для массивов произвольной размерности из стандартной библиотеки.
   здесь 't1 [+] означает
      "массив какой-то (но определенной)
       размерности из элементов типа 't1",
   't1 означает обобщенную переменную типа, по аналогии с
       "typename t1" в C++.
*/   
operator + (A: 't1 [+], B: 't2 [+]) =
  [| for a <- A, b <- B {a+b} |]

// обобщенная реализация операции поэлементного умножения
// элементов массива слева на скаляр из стандартной библиотеки
operator .* (a: 't, B: 't [+]) =
  [| for b <- B {a*b} |]

// инициализируем массивы явным перечислением их элементов
val A = [| 1., 2., 3. |]
val B = [| 5., 6., 7. |]
// используем операции над массивами
val C = A + 0.5.*B

Как мы видим, некоторые даже самые простые операции работы над массивами не спрятаны внутри компилятора, а реализованы на самом языке в стандартной библиотеке.

Компилятор преобразует код следующим образом (некоторые преобразования здесь сгруппированы вместе для краткости изложения):

  • инстанциируем обобщенные реализации для нашего случая, вводим имена для промежуточных результатов

operator + (A: double [], B: double []) = ... // тела не изменяются
operator .* (a: double, B: double []) = ...

val A = ...
val B = ...
val temp1 = (.*)(0.5, B)
val C = (+)(A, temp1)
  • подставляем функции inline. Поскольку функции после подстановки больше не используются, мы их удаляем

val A = ...
val B = ...
val temp1 = [| for b <- B {0.5*b} |]
val C = [| for a <- A, t <- temp1 {a+t} |]
  • сливаем циклы. Компилятор замечает что:

    • temp1 это временное значение, которое является результатом array comprehension. Тело array comprehension при этом — 'чистое выражение' без побочных эффектов.

    • temp1 используется лишь один раз, причем внутри другого comprehension

    Тогда можно заменить итерацию по temp1 на итерацию по массивам из которых получается temp1, а в тело цикла подставить выражение для вычисления элементов temp1.

val A = ...
val B = ...
val C = [| for a <- A, b <- B {val t = 0.5*b; a+t} |]

Основные свойства языка

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

  1. Синтаксис языка являет собой гремучую смесь C/C++, Python, Ocaml, Scala, Swift, Rust и возможно других языков, тем не менее, если рассматривать язык по слоям, то:

    • на уровне лексики, операций и выражений мы имеем С-подобный язык.

    • также есть препроцессор, напоминающий таковой в C/C++, но более простой и думается более безопасный. Задачу условной компиляции он вроде как решает.

    • на уровне управляющих конструкций, стиля описания типов, стиля определения функций, исключений и т.д. язык идеологически напоминает Ocaml и StandardML, с той большой разницей что вместо let-in цепочек мы используем блоки, внутри которых можно свободно чередовать декларации с выражениями. Но фундаментально Фикус - это функциональный язык, то есть, любая управляющая конструкция, включая 'if' и 'match', это выражение, которое может быть вложено в другие выражения.

    • блоки оформляются как в Swift, Rust и Go разве что нет ограничений где писать '{' - где хотите, можно переносить на новую строчку, можно не переносить.

    • система модулей построена по лекалам Python, т.е. она сильно отличается от Ocaml и StandardML.

  2. Массивы, списки, вектора, строки, структуры, кортежи, типы-суммы - first-class citizens. Компилятор все про них знает и адекватным образом конструирует, копирует, разрушает, передает в функции и хранит в контейнерах. Копирование любой структуры данных (operator =), передача ее в функцию или возвращение из функции, независимо от количества элементов это O(1) операции. Для некоторых структур, например массивов, доступно также глубокое копирование с помощью функции copy(). Пользователь не может переопределять поведение '=' или деструкторов.

  3. Почти нигде не используется boxing/unboxing типов. Есть пара небольших исключений, но они никак не влияют на объем занимаемой памяти или скорость обработки данных. Практически можно считать что boxing/unboxing не используется.

  4. Реализована довольно большая часть семантики языков OCaml и StandardML, за исключением их системы модулей. Также OOP реализовано немного по-другому чем в OCaml.

  5. Подобно C++ и Java, и в отличие от Python, OCaml и StandardML доступно полноценное переопределение функций, т.н. ad-hoc полиморфизм. Благодаря этому не нужно придумывать 100 вариантов названия функции print() и прочих базовых операций, как и не нужно устраивать в подобных функциях гирлянду из проверок if typeof(a) == t1 {...} else if ... (и не получится, ввиду отсутствия boxing/unboxing типов).

  6. Кстати о print(), string() и прочих. Для практически всех определенных пользователем типов доступны из коробки функции печати, преобразования в строку и сравнения. Исключение составляют варианты (и то сравнивать на равенство/неравенство их можно). Если автоматически генерируемая реализация не устраивает, можно переопределить.

  7. Для массивов доступны многие базовые операции из Матлаба и Python+numpy. При этом, в отличие от этих языков, циклы по массивам очень быстры. Так что, если чего-то не хватает, гораздо проще это эффективно реализовать.

  8. Для строк, списков и векторов также доступны многие базовые операции из OCaml, Python и остальных подобных языков.

  9. Строки используют Unicode, идентификаторы в программе могут включать Unicode-символы категорий 'буква' и 'цифра'.

  10. На выходе генерируется C или опционально C++ код. Из программ на Фикусе можно вызывать функции на C/C++, и соответственно наоборот (например, можно писать на Фикусе callback'и для C библиотек).

  11. Отдельная фишка - можно реализовывать тело Фикус-функции на C/C++. То есть, по аналогии с C/C++, где есть встроенный ассемблер, здесь есть встроенный C/C++. Это еще больше упрощает подключение сторонних библиотек и реализацию некоторых низкоуровневых функций стандартной библиотеки.

  12. Реализованы многие, но не все фичи функциональных языков типа Ocaml: неизменяемые значения и структуры данных, вложенные и безымянные функции, автоматический вывод типов, pattern-matching. Не реализованы ленивые вычисления.

  13. Реализованы многие, но не все фичи императивных языков. Есть циклы for, while, есть операторы break, continue, return. Есть изменяемые значения, они же переменные (var), можно отдельные поля структур объявлять изменяемыми (что снижает эффективность, но иногда оно того стоит).

  14. Отдельной строкой: каждое значение или переменную в Фикусе нужно обязательно явно инициализировать.

  15. Реализована обработка исключений, как в Ocaml/StandardML.

  16. Реализованы некоторые фичи объектно-ориентированных языков. Есть классы и есть интерфейсы. Интерфейсы можно наследовать друг от друга, классы нельзя (то есть, каждый класс - это final класс в терминологии Java). Классы построены на базе структур. Классы могут реализовывать ноль или более интерфейсов. В последнем случае можно делать преобразование класса к интерфейсу и обратно, можно преобразовывать один интерфейс к другому (аналог dynamic_cast<>() в C++ или QueryInterface() в COM).

  17. Доступны обобщенные функции и типы.

  18. На уровне синтаксиса, компилятора и стандартной библиотеки есть поддержка многопоточности. Просто нужно поставить @parallel перед for, и @sync перед блоками, которые нужно исполнить атомарно и эксклюзивно. Более сложные концепции типа корутин, агентных моделей, асинхронного выполнения и прочего пока не реализованы.

Стандартная библиотека постепенно пополняется, в настоящий момент она включает:

  1. базовые операции над числами, кортежами, структурами, строками, списками, векторами, массивами.

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

  3. чуть менее базовые операции над массивами, в частности матрицами, включая qr декомпозицию, подсчет детерминанта, обратной матрицы, решение линейных систем

  4. базовые математические операции, генерация случайных чисел

  5. функциональные и императивные ассоциативные контейнеры (на базе деревьев и хэш-таблиц, соответственно)

  6. операции над файлами, системные вызовы

  7. чтение/запись .json файлов

  8. простой движок для реализации unit-тестов

  9. частичные обертки для библиотеки OpenCV

Производительность

Вот небольшое сравнение текущей версии Фикуса c C/С++ и Python на нескольких бенчмарках из проекта https://benchmarksgame-team.pages.debian.net/benchmarksgame/index.html. Исходный код всех этих бенчмарок на Фикусе можно найти в репозитории по ссылке ниже.

Сталкиваем Фикус, C/C++ и Питон в Benchmark Game!
Сталкиваем Фикус, C/C++ и Питон в Benchmark Game!

В строке C/C++ для бенчмарок приведены две цифры. Первая цифра - для самой быстрой доступной реализации на данном языке, а в этом проекте разработчики заходят достаточно далеко в погоне за максимальной скоростью. Вторая цифра приведена для достаточно эффективной, но все еще читаемой реализации без использования встроенного ассемблера и специальных интринзиков (хотя и с распараллеливанием, когда это имеет эффект). Для Python приведено время самой быстрой реализации.

Статус и планы

"Мы строили-строили, и наконец построили!" Че

"После сборки обработать напильником." Из инструкции по сборке экспортного советского танка

  1. Язык спроектирован, компилятор реализован и самораскручен. Есть компактный runtime, небольшая стандартная библиотека, написан учебник. На момент написания статьи версия языка - 1.0-alpha.

  2. Одна из основных киллер-фич языка - продвинутые array comprehensions. Благодаря им нотация получается гораздо более компактной и удобной для анализа, а различные оптимизирующие преобразования над такими конструкциями можно осуществлять проще и безопаснее.

  3. Реализовано около сотни unit-тестов, также при начальной сборке компилятор собирает сам себя. Таким образом, есть некий sanity-check и он успешно проходится.

  4. Генерируемый код получается достаточно эффективным, хотя пока и отстает от вручную оптимизированного С кода, иногда заметно, что впрочем ожидаемо.

  5. Обернута часть библиотеки OpenCV, таким образом язык можно понемногу начинать использовать для написания небольших программ, для экспериментов с компьютерным зрением.

  6. Благодаря встраиваемым фрагментам на C/C++ подключать новые библиотеки, особенно после небольшой тренировки, достаточно несложно.

Вместе с тем, определенно есть куда расти:

  1. Есть определенные шероховатости в реализации компилятора, например обработка частичных специализаций обобщенных/шаблонных функций, работа с вложенными модулями, компиляция некоторых проектов на Windows.

  2. Обработка различных особых случаев (corner cases) в компиляторе и в стандартной библиотеке возможно недостаточна и определенно не тестируется должным образом.

  3. Вообще, покрытие тестами достаточно слабое

  4. Стандартная библиотека далека от завершенности. На этот счет имеется отдельный тикет в багтрекере.

  5. Было бы неплохо реализовать более совершенный пакетный менеджер, например как в Python или как в Rust.

  6. Скорость генерируемого кода пока далека от идеала. Можно улучшать скорость для CPU, можно пробовать реализовать экспериментальный запуск циклов на GPU. По крайней мере, бенчмарки mandelbrot.fx и spectralnorm.fx можно попробовать серьезно разогнать.

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

  8. Инструментальная поддержка языка пока очень слабая.

    • Было бы неплохо реализовать Language Server Protocol для полноценной поддержки языка внутри Visual Studio Code, VIM и других совместимых с этой технологией редакторов.

    • Необходимо как-то решить вопрос с отладкой программ на Фикусе. Пока предлагается два решения: 1) использовать отладочную печать, 2) с помощью cmake сгенерировать проект из сгенерированных Фикусом .c файлов, открыть его в IDE и запустить отладчик. Пример скрипта для такого способа прилагается.

  9. В настоящий момент реализована только сборка приложений (режим 'app'). Возможно следует переработать систему сборки и обеспечить полноценный режим компиляции отдельных модулей. Особенно это будет полезно и удобно если модули большие и зависят от сторонних C/C++ библиотек.

Благодарности

Благодарю:

  • Shenzhen Institute of Artificial Intelligence and Robotics for Society (AIRS), где мне посчастливилось некоторое время поработать и где мне дали достаточно свободы для реализации данного проекта.

  • Отдельная благодарность Yu Shiqi за приглашение меня в AIRS, в OpenCV China и всестороннюю поддержку

  • Моему двоюродному брату Петру за реализацию некоторых частей компилятора и стандартной библиотеки, а также помощь в тестировании

  • Ребят с прежней работы (из компании Itseez, ныне ставшей частью Нижегородского центра Intel) за обсуждение деталей дизайна и помощь в некоторых моментах на ранних стадиях работы над проектом.

  • Авторов различных open-source проектов, чьи результаты были с удовольствием использованы (некий частичный список помещен в конце головного README.md в корне проекта на github).

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

Ссылки:

  1. Репозиторий Ficus: https://github.com/vpisarev/ficus. В подкаталоге doc есть учебник в формате PDF. В подкаталоге examples можно найти примеры.

  2. MinCaml: https://github.com/esumii/min-caml. Микро-компилятор для очень маленького подмножества Ocaml, который вдохновил на написание компилятора Фикуса

  3. OpenCV: https://github.com/opencv/opencv/wiki

  4. Halide: https://halide-lang.org

  5. TVM: https://tvm.apache.org

  6. "Image Algebra: An Overview". G. X. Ritter et al https://www.researchgate.net/publication/222760582\_Image\_algebra\_An\_overview.О том как можно выразить различные алгоритмы обработки изображений с помощью некоей формальной нотации. Есть дальнейшая работа этого же автора на тему алгоритмов компьютерного зрения.

  7. Functional Approach To Programming. Guy Cousineau, Michel Mauny. https://www.cambridge.org/core/books/functional-approach-to-programming/27F7047FBF0962D02161E79B90BD7FD2. Выдающаяся книжка о том что такое функциональное программирование, и как начать думать и писать в таком стиле.

Комментарии (34)


  1. tzlom
    03.12.2021 01:51
    +1

    Зачем сравнивать производительность с питоном если есть джулия?

    Почему фикус?


    1. vpisarev Автор
      03.12.2021 03:08
      +5

      Сравнение с Питоном проводилось поскольку это флагманский язык для ML/AI, на нем сейчас пишут практически все в этой индустрии.

      С Джулией (Julia) интересно было бы сравниться, в следующей статье надо будет обязательно добавить. Это очень неплохой язык, где также реализована поддержка массивов на уровне Matlab, правда, насколько я понимаю, там выбрана динамическая типизация как вариант по-умолчанию, соответственно чисто теоретически эффективность кода будет скорее всего ниже чем в C++ и в Фикусе. На практике же и язык и компилятор и стандартная библиотека находятся в уже очень продвинутом состоянии, в компиляторе реализован вывод типов, есть возможность использовать SIMD-интринзики, есть возможность для конкретных случаев тонко настраивать код, например отключать проверку на диапазон при доступе к массиву, поэтому есть возможность при желании получить довольно быстрый код.

      По поводу названия. Ну как-то так получилось, рассматривал названия, начинающиеся на F/Ф, чтобы подчеркнуть функциональность. Случайно узнал что Фикусовые - это целое семейство, включающее большое количество известных растений, от комнатных до совершенно эпических, используемых для совершенно разных целей. Меня такая универсальность и "масштабируемость" очень порадовала.


      1. tzlom
        03.12.2021 03:54
        +1

        Но вы сравнивали же производительность, а удав инвалид по скорости это всем и так ясно.

        "поддержка массивов на уровне Matlab" уровень Матлаба это очень низкая планка.
        Джулия скорее компилируемая, я бы ожидаю что джулия будет в районе фикуса по производительности.

        Мне вот интересно, в ваших тестах фикус отстаёт от С++, а понятно из за чего?


        1. vpisarev Автор
          03.12.2021 06:33
          +1

          Да, понятно. Быстрейшие реализации часто используют SIMD-интринзики, иногда довольно нетривиальным образом. Иногда реализуется специальные аллокаторы памяти, иногда реализуются специализированные структуры данных под каждый бенчмарк

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

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

          Например бенчмарка btree значительно ускорилась после: 1) изменения представления рекурсивных вариантов, когда "пустые" листья стали представляться нулевыми указателями вместо выделения под них памяти, 2) подключения rpmalloc, который сейчас основной аллокатор в Фикусе и 3) распараллеливания бенчмарки по деревьям (@parallel for). Это общие полезные оптимизации. Дальнейшее ускорение скорее всего возможно при переходе с обычного подсчета ссылок на базе атомарных операций на т.н. biased reference counting, когда в большинстве случаев атомарных операций можно избежать. Опять же, это общая оптимизация которая будет полезна для многих программ.

          Бенчмарки spectralnorm и mandelbrot должны хорошо ускориться после автоматического портирования некоторых циклов под GPU. Это интереснее и полезнее (хотя и гораздо сложнее) чем тьюнить код одной конкретной бенчмарки. Для mandelbrot также можно в одном месте чуть подкрутить компилятор. Возможно это позволит догнать суб-оптимальную/стандартную версию на C++.

          k-nucleotide нуждается в более эффективной реализации хеш-таблицы, хотя может и что-то другое можно придумать

          Для n-body также можно в одном месте чуть подкрутить компилятор, но в-целом код неплохой. В реальности n-body это довольно игрушечная симуляция. Будет интереснее попробовать Фикус на гораздо более тяжелых симуляциях, и тогда опять же портирование на GPU должно сыграть.


          1. forthuser
            03.12.2021 07:29
            +1

            А, нет мысли порешать какие то задачи на Ficus ресурса http://rosettacode.org/ в сравнении с возможностями других языков?

            P.S. В i-net есть, для каких то языков, возможность проверки Online решений на них, а для Ficus предполагается такой же опционал?


            1. vpisarev Автор
              03.12.2021 12:38

              Это было бы полезно, и для пополнения unit-тестов нетривиальными примерами, и для продвижения языка. Один из примеров, ycomb.fx как раз был взят из rosettacode, потому что засомневался что оно вообще в Фикусе заработает. Вопрос только в ресурсах. Это был бы полезный студенческий проект, а может и не один. Надо будет это записать и подумать, какими силами это сделать.

              Насчет доступа к фикусу онлайн - также полезная возможность, но пока сам это делать не планирую.


      1. worldmind
        03.12.2021 17:28

        Так питон там как клей для всяких си/фортран либ используется, у них должно быть норм с производительностью.


  1. kivicode
    03.12.2021 02:20

    Очень интересно было бы попробовать, но, к сожалению, гитхабовская ссылка не работает :(


    1. vpisarev Автор
      03.12.2021 02:22

      Прошу прощения, в конце ссылки магическим образом добавился лишний символ. Правильная ссылка https://github.com/vpisarev/ficus


  1. rg_software
    03.12.2021 05:52
    +5

    Вообще, конечно, очень интересно -- всё-таки полноценный DSL язык с достаточно глубокой проработкой темы и явно не "на коленке" сделанный. Но штука специализированная, поэтому со стороны трудно предложить толковую реплику кроме "спасибо за статью". Но всё же слегка есть ощущение "начали за здравие..." Например, есть рассуждения о том, что в C++ с массивами не очень, нельзя выполнить глубокую оптимизацию вложенных циклов и т.п. Но в конечном итоге всё равно по бенчмаркам не выиграли же.


    1. vpisarev Автор
      03.12.2021 12:45

      Вопрос вполне справедливый: где деньги, т.е. где обещанная скорость? Дайте немного времени. Считаю что проектные решения, заложенные в языке правильные, но нужно время чтобы их превратить в реальную скорость. Это будет один из главных приоритетов на ближайший период.

      Ну и, справедливости ради, это не совсем DSL. Скорее полноценный язык (на котором можно написать компилятор например) с хорошей поддержкой массивов.


  1. AndreyDmitriev
    03.12.2021 10:45
    +1

    Спасибо, я попробовал, всё собралось и тестовые примерчики компиляются.

    У меня есть практический вопрос. Как использовать Фикус из кода на других языках, ну скажем из С/С++? Я почитал документацию - там у вас пример, когда Си используется в Фикусе, а мне хочется обратного. В идеале получить просто DLL, куда передать указатели на массивы и получить обратно результат. Если у вас там Си используется, то это должно быть совсем несложно. Я, конечно, могу взять сгенерированнный код на Си, добавить там экспорт, а прямо из Фикуса можно DLL получить?

    Ну вот, к примеру есть у меня примитивный код, который гонит 16 бит картинку в 8 бит для отображения на экране с линейной LUT между min и max:

    __declspec(dllexport) void __cdecl  Mapping16to8(
    		unsigned short *src_ptr_16bit, unsigned char *dst_ptr_8bit,
    		int src_bytesPerLine, int dst_bytesPerLine,
    		int width, int height, int min, int max)
    //...опуская всякие проверки  
    for (i = 0; i < imin; i++) LUT16[i] = 0;
    for (i = imin; i < imax; i++) LUT16[i] = (int)(double(i - imin) / (imax - imin) * 255.0);
    for (i = imax; i < 65536; i++) LUT16[i] = 255;
        
    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            *dst_ptr_8bit = LUT16[*src_ptr_16bit];
            src_ptr_16bit++; dst_ptr_8bit++;
        		}
        //учитываем выравнивание
        src_ptr_16bit += (src_bytesPerLine >> 1) - width;
        dst_ptr_8bit += dst_bytesPerLine - width; 
    }

    Тут надо учесть ещё то, что массивы у меня выровненные (на 64 байта, что в общем стандартная практика для быстрой обработки изображений). Вот как это на Фикусе будет выглядеть?


    1. cepera_ang
      03.12.2021 14:27
      +1

      Может вам всё-таки на упомянутые Halide/TVM глянуть? Всё-таки промышленные языки, с серьезными пользователями за спиной, автотюнинг всякий в наличии...


    1. vpisarev Автор
      03.12.2021 15:55

      Теоретически вызвать Фикус из C/C++ должно быть несложно. Практически в данный момент это не так просто поскольку пока компилятор работает только в режиме генерации кода для приложений.

      Вот пример, который воспроизводит ваш C++ код:

      fun make_lut_16to8(imin: int, imax: int)
      {
          assert(0 <= imin <= imax < 65536)
          [| for i <- 0:65536 {sat_uint8((i - imin)*255./(imax - imin))} |]
      }
      
      fun apply_lut(img: uint16 [,], lut: uint8 []) =
          [| for pix <- img {lut[pix]} |]
      
      val L = make_lut_16to8(16, 4096)
      val img8 = apply_lut([| for i <- 0:6 for j <- 0:6 {uint16((i+j)*500)}|], L)
      println(img8)

      Проблема в том что если собрать его с -O3 или -O1, то эти функции подставятся и исчезнут сами по себе. Пока нужно компилировать только с -O0, но тогда скорость будет ниже. Но тем не менее, для проверки можно это сделать. Допустим, если сохранить этот код в файле lutop.fx и собрать, то вы получите каталог ficus/__fxbuild__/lutop/ с кучей .с файлов, включая lutop.c, там будет функция FX_EXTERN_C int _fx_M5lutopFM9apply_lutA2b2A2wA1b(fx_arr_t* img_0, fx_arr_t* lut_0, fx_arr_t* fx_result, void* fx_fv), которую можно вызвать, если соответствующим образом подготовить входные массивы:

      _fx_arr_t img = {0, 0, 0, 0, 2, (char*)src_ptr,
         {{height, src_stride}, {width, sizeof(uint16_t)}};
      fx_arr_t lut = {0, 0, 0, 0, 1, (char*)lut_ptr, {{65536, 1}};
      fx_arr_t result = {0};
      int errcode=_fx_M5lutopFM9apply_lutA2b2A2wA1b(&img, &lut, &result, 0);
      ...

      Подобная обертка может располагаться в .с/.cpp файле и она как раз может быть dllexport. После чего останется включить все сгенерированные .c файлы, вместе с runtime, в свой проект. Но конечно это пока не очень простое и не удовлетворительное решение.


    1. adeshere
      03.12.2021 18:45
      +2

      Как использовать Фикус из кода на других языках, ну скажем из С/С++? 

      Простите, что влезаю в чужой разговор, но Вы уверены, что эти вызываемые процедуры оптимально писать именно на Фикусе? В общем, я бы тоже предложил глянуть в сторону других вариантов - а конкретно фортрана. Модульность там реализована уже довольно давно (начиная с фортрана-95), а поддержка массивов просто близка к идеалу. Язык позволяет удобно писать практически любые расчеты. Начиная от простейших массивных операций (это когда в арифметическом выражении вместо имени переменной можно указать имя массива) и задания почти произвольных сечений массива, и кончая такими операторами, как FORALL и WHERE. Если использовать только чистые функции (которые тоже появились в стандарте фортран-95), то циклы для обработки массивов вообще практически не нужны. А главное, все эти операторы очень эффективно оптимизируются компилятором. Скорость исполнения получается примерно такая же, как на Си, при том, что высокоэффективный код писать значительно проще. (Только не забудьте при тестах производительности включать в обоих языках оптимизацию максимально, иначе неоптимизированная программа кратно проиграет оптимизированной).

      Впрочем, насчет легкости кодирования это наверно уже вопрос привычки и вкуса. К примеру, в фортране ввод-вывод - это часть языка, и поэтому есть целый зоопарк операторов ввода-вывода с кучей спецификаторов для работы с разными типами файлов (а в Си это вынесено в библиотеки). Еще один минус фортрана - он "тащит" совместимость со старым кодом, из-за этого в языке до сих пор сохраняются устаревшие операторы и возможности (вроде Include, goto, common и др.). Использовать их никто не заставляет, но документацию это слегка усложняет.

      С другой стороны, как ни сравнивай фломастеры вкусом и цветом, а мне все-таки кажется, что написать:

        real(16) ::        a(100), d(100)
        ...
      d=sin(a**2)

      явно проще, чем "крутить цикл". Который компилятор потом будут оптимизировать на свое усмотрение (и если он сложный - то может случиться, что поймет его не совсем так, как хотел программист). А приведенная выше запись просто не оставляет простора для толкований.

      Но и плюсов (простите за каламбур) у фортрана тоже не мало. Я бы сюда отнес развитые инструменты для контроля точности вычислений с плавающей точкой и написания переносимых программ, а также средства для мультиязычного программирования (с помощью интерфейсов можно как вызывать функции из других языков, так и писать свои функции, которые будут передавать параметры в вызывающую программу "по ее правилам"). Или такой нюанс: проверка использования неинициализированных переменных, выхода индекса за границы массива и т.д. и т.п. - это отдельные опции компилятора, и их можно включать/выключать вне зависимости от того, собираю ли я debug-версию или release. (Да, это свойство компилятора, а не языка, но подозреваю, что все основные компиляторы это умеют). Кстати, для желающих в языке сейчас есть минимальные элементы ООП. Но для тех, кто занимается вычислениями, одно из основных преимуществ - это наличие мощных библиотек для всевозможных расчетов, которые обычно прилагаются к компилятору, поэтому их использование выглядит, как использование "стандартных библиотек языка". Само собой, операции с матрицами и линейная алгебра там тоже есть ;-) Причем обычно эти библиотеки имеют версии для расчетов с разной разрядностью (не обязательно все считать в double), и т.д.

      В общем, если кому-то будут интересны подробности - пишите, расскажу что смогу (с примерами кода).


      1. cepera_ang
        03.12.2021 18:57

        А позволяет ли фортран делать такие фокусы: вот у нас есть картинка для обработки (WxHx3) и надо её, например, по-блюрить. Делаем функцию блюр, которая усредняет соседние пиксели, а язык сам разберется как побить картинку на блоки оптимального размера (чтобы в L1-кеш влезали), порежет на потоки по количеству ядер и выберет, где оптимально из памяти уже вычисленные значения достать, а где быстрее будет просто пересчитать?


        1. adeshere
          03.12.2021 19:55
          +1

          Есть ли такие функции в стандартной библиотеке - сомневаюсь, скорее всего нету. Надо искать специализированные либы для этого (и тут я, увы, не помощник, - я только временные ряды обрабатываю).

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

          Понятно, что потом все равно надо будет тестировать скорость и сравнивать. Априори у меня есть маленькое подозрение, что скорость будет не очень плохая, так как включение в компиляторе опции "/Qparallel" мою программу на моем компьютере (6 ядер) в несколько раз ускоряет. Без каких-либо телодвижений с моей стороны. Но это именно домыслы - твердых знаний по этой части у меня нет.

          В принципе, в языке есть возможность получить количество ядер и распараллелить расчеты вручную. Но если честно, я с этим вопросом не разбирался пока - хватает оптимизатора. У меня ведь временные ряды достаточно небольшие - обычно не больше нескольких миллионов значений. Для случая двух измерений это получится картинка со стороной 1-10K. Ну и свертка у меня всегда идет по одному измерению, а не по двум.

          Вообще, я думаю, что метеорологи в этих вопросах лучше меня шарят. У них ведь там трехмерные модели и сетки. Только вот на хабре я таких пока не встречал. Если у Вас с языком нет проблем, то в англоязычных форумах наверняка можно более точный ответ получить. Либо могу попытаться через знакомых такие контакты в РФ найти. Я сам давно собирался этим поинтересоваться. Но это дело очень небыстрое, надо искать седьмую воду на киселе....


      1. AndreyDmitriev
        03.12.2021 20:15
        +1

        Спасибо за такой развёрнутый ответ. Об оптимальности речи не идёт вовсе, да и не собираюсь я с Фикусом идти в продакшн. Я просто интересуюсь всякими новыми (и местами эзотерическими) штуками - просто ради любопытства. Вот неспешно пилю "пет-проект" для души - это программка для работы с 16-ти битными картинками (типа ImageJ, но чуть удобнее). Основной язык - C#, интерфейс на Avalonia, основная библиотека OpenCV через opencvsharp, ну и самописные функции, в которых я могу использовать любой стек технологий, какой заблагорассудится - тут я ничем не ограничен.

        Что касается работы с массивами без циклов - то по работе я использую LabVIEW и там примерно тоже самое:

        Ну а мощнейшая математическая библиотека вкупе с весьма приличной библиотекой обработки изображений покрывают в общем все нужды с лихвой. Но "эзотерики" в LabVIEW конечно хватает.

        К Фортрану я как-нибудь вернусь. У нас было два семестра численных методов (один на Фортране, второй на Паскале) и я верю, что современный Фортран далеко ушёл от того, что я учил тридцать лет назад.


      1. vpisarev Автор
        03.12.2021 20:23
        +1

        Фортран это очень мощный язык, и он конечно еще всех переживет (как и Лисп). Тем не менее, если взять свежую версию Фикуса, и запустить вот такой примерчик:

        val a = [| for i <- 0:100 {i/100.*M_PI} |]
        println(sin(a.**2))

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

        for (int_ i_0 = 0; i_0 < 100; i_0++, dstptr_0++) {
           *dstptr_0 = sin(pow(i_0 / 100.0 * 3.141592653589793, 2));
        }

        То есть, компиляторы Фортрана круты, а библиотеки на Фортране бесценны, но и мы уже кое-что можем.


  1. Druj
    03.12.2021 11:02
    +3

    Синтаксис просто прекрасен, OCaml обернутый в Julia. Без шуток, очень приятно


    1. vpisarev Автор
      03.12.2021 12:52
      +1

      Спасибо. Cтиль Julia немного отличается, мне кажется, скорее здесь параллели можно провести со Swift, Scala, Rust. Насчет Ocaml все справедливо - это основной источник влияния.

      В качестве лайфхака - если взять редактор с поддержкой лигатур (например, Sublime или VSCode) и соответствующий шрифт, то будет еще красивее. В репозитории Фикуса можно найти один из таких шрифтов.


  1. domix32
    03.12.2021 11:04

    также есть препроцессор, напоминающий таковой в C/C++, но более простой и думается более безопасный. Задачу условной компиляции он вроде как решает.

    А чем гарантируется безопасность?
    А что с автовекторизацией и раскручиванием циклов?
    Есть ли compile time выражения?
    Вообще перечисляется довольно немало языковых фич, но при этом крайне малое количество примеров кода. У фикуса есть здоровенная документация? Что там с генерацией доков кстати?


    1. vpisarev Автор
      03.12.2021 16:07

      В препроцессоре Фикуса нет директивы include. Вместо этого есть настоящие модули. Тем самым, локально объявленные в модуле символы не влияют на остальные модули.

      Во-вторых, символы препроцессора содержатся в отдельной таблице, и не конфликтуют с символами в коде.

      // preprocess_test.fx
      @IFNDEF UI
      @DEFINE UI "CLI"
      @ENDIF
      val UI = @{UI} // публикуем значение символа препроцессора в настоящем коде,
                     // сохраняем в неизменяемую переменную с таким же именем
      println(f"selected UI: {UI}")

      запускаем чтобы напечаталось "GUI"

      `bin/ficus -run -D UI=GUI preprocess_test.fx`

      Автовекторизация/раскрутка циклов в настоящий момент осуществляется только компилятором C/C++, который подхватывает код сгенерированный Фикусом. Наша первая задача - сгенерировать максимально дружественный для векторизации код, не отягощенный динамической типизацией, boxing/unboxing и т.д. Для простых циклов эта задача решается неплохо.

      Compile-time выражения не имеют отдельного синтаксиса, но во время компиляции константные выражения подхватываются и заменяются их значениями. Например, при компиляции примера выше про LUT с -O3 (imax - imin) благополучно заменилось на 4080.

      Примеров много и в тьюториале на 100 страниц и в подкаталоге examples. Собираюсь написать еще статью, может и не одну, с более подробным обзором фич языка с примерами. Помести я это все в эту статью, объем бы превысил все разумные пределы.

      Генерации доков пока нет, это в планах, хотя и не краткосрочных


      1. domix32
        03.12.2021 23:39

        Ну то есть фактически только избавление от scope clashing. Я думал маленько про другую безопасность, но ладно.


        @IFNDEF UI
        @DEFINE UI "CLI"
        @ENDIF

        выглядит довольно громоздко. не планировали взять синтаксис макросов как в расте или поменять такие штуки на какие-нибудь пространства имён? что-нибудь типа


        @scope UI {
        @define UI "CLI"
        } 
        или
        #[!defined(UI)]
        @define UI "CLI"

        За f-strings отдельный респект.


  1. alien308
    03.12.2021 13:24
    +1

    Нативная поддержка полноценной линейной алгебры. Этого везде страшно не хватает. NumPy/SciPy, Julia не предлагать.

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

    Или хотя бы гуманный интерфейс в форматы BLAS и возможность полноценного вызова MKL с гуманным интерфейсом, включая драйвера и pardiso.

    Я понимаю, что это очень сверзадачная сверхзадача :).


    1. Druj
      03.12.2021 14:06

      В Julia BLAS не нативный(если я правильно вас понял), это всунутая в stdlib обёртка над LAPACK


      1. alien308
        03.12.2021 14:13

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

        Как реализовано под капотом это уже вопрос реализации.


    1. vpisarev Автор
      03.12.2021 16:35

      Обертки над BLAS и LAPACK в самых ближайших планах. Функций там много, но попробуем обернуть хотя бы часть для generic double-precision матриц (ленточные, верхне/нижнетреугольные пока можно отложить)


  1. worldmind
    03.12.2021 17:33

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


    1. vpisarev Автор
      03.12.2021 18:04

      Именно так. В Фикусе реализованы иммутабельные вектора, добавленные под влиянием доклада Хуана Пуэнте: https://www.youtube.com/watch?v=dbFfpTp3EhA. Для представления потоковых одномерных данных типа звука, текста, финансовых данных и прочее они я думаю вполне бы подошли. Но для изображений, тензоров все-таки пока это не очень эффективно.


  1. Color
    03.12.2021 18:10

    Интересный проект, спасибо. Буду следить


  1. Soukhinov
    03.12.2021 18:52

    По поводу ускорения вычислений на GPU мне вспомнился периодически возникающий диалог:
    — Ну что, разобрались, почему нейросеть так тормозила?
    — Да. Там число каналов было не кратно четырём, сеть стала вычисляться на GPU, вот и тормоза.

    (Речь о том, что «правильный» способ вычислений — использование Apple Neural Engine, а вычисления на GPU мы называем «отсутствием аппаратной акселерации» :) ).


  1. iShrimp
    04.12.2021 17:49

    Скажите, пожалуйста, поддерживаются ли массивы структур?

    У меня есть небольшой проект по обработке изображений на Java, где нужно преобразовать массив пикселей в массив градиентов (структур типа {magnitude; x_direction; y_direction}). Я использую один массив типа int, в котором последовательно хранятся тройки значений в формате fixed point. Это неудобно, т.к. надо вручную манипулировать индексами, и все данные должны быть одного типа. А вариант с тремя отдельными массивами работает медленнее. Есть ли в вашем языке инструменты для работы с такими данными?


    1. vpisarev Автор
      04.12.2021 23:34
      +1

      да, конечно. См. пример nbody.fx, там используются похожие структуры и определяются над ними операторы: https://github.com/vpisarev/ficus/blob/master/examples/nbody.fx