Около 5-6 месяцев назад я начал делать модернизированную версию советского языка программирования Рапира - Рапира26.
После перевода бэкенда на кастомную виртуальную машину и байткод, решил сделать что-то интерактивное: сделал песочницу, где можно посмотреть, позапускать разные демо программы на рапире26 (и конечно попробовать написать самому).
Бенчмаркая CSE: подстава на uint — деление считается дважды
Уважаемые читатели, в этом посте я хочу разобраться, что компилятор делает с парой / и %, и представить свои выводы.
Возьмём число 47 и делитель 10. Деление даёт 4, остаток — 7. Компилятор деление не выполняет: он умножает 47 на подобранное число и сдвигает, получая 4. Дальше для остатка хватает вычитания: 47 − 4 × 10 = 7. Одно умножение на оба ответа.
Так на int. На uint компилятор получает 4, умножает на 10, вычитает — а потом заново считает те же 4 из 47, вторым умножением.
Ответ верный и там, и там. CSE, common subexpression elimination, находит повторяющиеся вычисления и считает их один раз. Оба умножения в паре считают одно и то же, и на int проход их склеивает. На uint не склеивает — отсюда и обращение в трекер.
int value = ints[i];
total += value / 10 + value % 10; // одно умножение
uint value = uints[i];
total += value / 10 + value % 10; // три умножения
Замер на 1 024 значениях, .NET 10, три машины. Значения положительные: у знакового деления отрицательные идут другой веткой.
.NET 10. Пара к делению — во сколько раз пара медленнее одного деления того же типа
Из таблицы можно сделать выводы:
на int пара занимает столько же времени, сколько одно деление, на uint — вдвое больше;
беззнаковое деление быстрее знакового в 1,54–1,60 раза: знаковому нужна коррекция для отрицательных значений;
проседает не тип, а пара операций.
Вот как это выглядит в машинном коде, Xeon W-2255. У int одно умножение на всю пару:
mov edx, 0xD1FFAB1E ; подобранное число
imul edx:eax, r9d ; единственное умножение, вышло 4
sar edx, 2 ; деление готово
lea edx, [rax+4*rax] ; 4 x 5
add edx, edx ; ещё x2, вышло 40
sub r9d, edx ; 47 - 40 = 7, остаток
У uint то же самое, но в конце деление идёт второй раз:
mov r10d, 0xD1FFAB1E ; то же число
imul r10, r9 ; первое умножение, вышло 4
shr r10, 35 ; деление готово
imul r10d, r10d, 10 ; второе: 4 x 10 = 40
sub r9d, r10d ; 47 - 40 = 7, остаток
mov r10d, 0xD1FFAB1E ; снова оно
imul r8, r10 ; третье: те же 4 заново
shr r8, 35 ; и тот же сдвиг
Были проверены ещё три случая, разницы между int и uint в них нет. Делитель 16, степень двойки: деление сводится к сдвигу, остаток берётся из младших битов, умножений ноль у обоих. Делитель в переменной: работает машинная команда деления, она выдаёт оба ответа разом, 2 714 против 2 716 нс. Тип ulong: на .NET 8 и .NET 9 было три умножения, на .NET 10 осталось одно, а у uint три.
Что делать на практике:
в горячих циклах вроде разбора числа по цифрам, форматирования и хэшей пара идёт на каждом витке, а с ней и лишнее умножение;
Math.DivRem возвращает к одному умножению: быстрее пары в 1,13–1,76 раза. Внутри для uint то же вычитание:
// dotnet/runtime, Math.cs
public static (uint Quotient, uint Remainder) DivRem(uint left, uint right)
{
uint quotient = left / right;
return (quotient, left - (quotient * right));
}
вычитание, записанное явно, value - value / 10 * 10, быстрее пары в 1,23–1,80 раза — для тех, кому не нужен кортеж из Math.DivRem;
на int менять нечего: там деление с остатком уже собрано в одно умножение;
лишнее умножение забирает часть того, что uint даёт на делении, но не всё: на разборе числа по цифрам он остаётся быстрее int — 0,81–0,86.
Друзья, я просто обязан сказать огромное спасибо всему сообществу Хабра за вашу поддержку и активность под статьей о моем проекте Kakehashi!
Вдохновившись вашими отзывами, вчера вечером я опубликовал проект на Hacker News. Результат превзошел все ожидания: прямо сейчас тред держит 204 поинта, а репозиторий набрал более 220 звезд на GitHub.
Проект попал в радар к хардкорным системщикам со всего мира. Среди тех, кто дал звезду, оказались инженеры из команд Cursor, Fly.io, Astro, создатель пакетного менеджера Pixi, разработчик Redox OS и в дискуссии на HN был легендарный автор утилиты Cydia @saurik.
Но мне особенно приятно и дорого то, что самый первый импульс, первые звезды и конструктивный фидбэк проект получил именно здесь. Вы дали Kakehashi тот самый стартовый заряд, благодаря которому он смог громко заявить о себе на международной арене.
Огромное вам спасибо!
P.S. Хотел опубликовать в хаб «Я пиарюсь», но интерфейс не пропустил из-за нехватки кармы (нужно 30). Поэтому публикую в профильные хабы как апдейт к прошлой статье. Надеюсь на понимание!
Первая версия генератора кода готова. Сделал я его через IIncrementalGenerator и она очень сильно не оптимизирована, но да ладно он мне все ровно нужен был только ради следующей статьи про диагностику dsl
Что интересного в архитектуре и компиляторе «Эльбруса» — узнаем в новом выпуске «Битовых масок»
Вы наверняка слышали не одну новость о российских процессорах «Эльбрус», но вряд ли могли удовлетворить профессиональное любопытство в теме. Вам поможет подкаст «Битовые маски»! В новом выпуске гостем стал Виктор Шампаров — разработчик компилятора LCC для весьма специфической линейки процессоров «Эльбрус» на базе архитектуры VLIW (Very Long Instruction Word).
Виктор провел подробную экскурсию по особенностям «Эльбруса» и архитектуры VLIW с точки зрения системного программирования. Кроме того, Виктор как опытный преподаватель оценил, как сегодня осваивают компиляторы студенты технических вузов.
Среди тем подкаста:
почему сложно сделать хороший компилятор под VLIW;
в чем разница между советским и российским «Эльбрусом»;
что в компиляторе «Эльбруса» написано с нуля;
какие особенности есть у VLIW-компилятора;
почему в архитектуре «Эльбруса» сравнительно больше регистров;
зачем в «Эльбрусе» санитайзер оптимизации;
в каких вузах стоит учиться работе с компиляторами.
Смотрите и слушайте подкаст на любой удобной платформе — и присоединяйтесь к каналу «Битовых масок», чтобы не пропустить новые выпуски!
Недавно я работал над встраиваемой системой, где на FPGA крутится EKF и немного управляющей логики. Кодировать такое на RTL — занятие в лучшем случае неблагодарное, поэтому я обратился к HLS (high-level synthesis) и стал смотреть, что предлагает индустрия.
У меня уже была довольно обширная обвязка для моделирования и верификации на питоне, поэтому в идеале хотелось чего-то, что принимает его напрямую, желательно с минимальной адаптацией: скормить инструменту нужные куски моих моделей и сразу получить рабочий RTL на выходе. Ещё хотелось плавающую точку. Добавлю, что у меня используется Lattice ECP5, а значит Vitis и вот это всё отпадает, так что я смотрел в сторону вендор-независимых инструментов.
Те, что умеют переваривать питон, есть, но на практике они, честно говоря, малопригодны — разве что для очень узкого набора задач. Я тестировал Polyphony, PyLog, Allo+XLS, Allo+Vitis (тоже мимо из-за Lattice) и Veriloggen. Они работают в том смысле, что переводят какой-то питон в какой-то RTL, но не в том смысле, что можно получить что-то практически пригодное, если нужно собрать, скажем, фильтр Калмана или хотя бы базовый ПИД-регулятор. Есть и мощные инструменты (XLS, Bambu и прочие), но они не поддерживают питон, плюс к ним есть ряд вопросов (особенно по части ECP5), о которых я как-нибудь расскажу отдельно.
Лично для меня это важная вещь, потому что она уже позволила сильно ускорить мою работу. Возможности сейчас в основном определяется моими насущными нуждами, но всё это расширяемо, и любой вклад приветствуются.
Подробное описание того, как оно устроено, есть по ссылке, но основная идея такая: парсим питон, строим граф потока управления, определяем, какие операторы нужны, конструируем минимальное специализированное VLIW-ядро, планируем микрокод (полностью статически, чтобы ядро оставалось простым) и генерируем Verilog вместе с дополнительными артефактами вроде Cocotb и отчётов. В комплекте есть примеры.
Я уже прогнал бенчмарки бок о бок с Bambu, XLS, Dynamatic и Vitis — результаты выглядят достойно; напишу об этом отдельно, если будет интерес (пока ещё в работе).
Нейросети, компиляторы и гонка AI-железа — новый выпуск «Битовых масок»
В новом выпуске «Битовых масок» говорим о нейросетях, больших языковых моделях и аппаратной разработке для ИИ. В гостях — Андрей Камаев, эксперт по разработке ПО искусственного интеллекта, который начинал карьеру еще во времена независимой OpenCV-компании Itseez, а позже работал в Intel. Вместе с Еленой Лепилкиной и Антоном Афанасьевым он обсудил, как индустрия пришла к современным LLM и что сегодня происходит на рынке AI-железа.
Андрей рассказал, как развивались современные нейросети и как сегодня устроен рынок AI-железа. Также обсудили, что происходит «внутри» LLM, чем разработка нейросетей отличается от классического программирования и какие навыки помогут начинающим разработчикам строить карьеру в эпоху ИИ.
Выпуск получился одновременно историческим и практическим. Вы узнаете:
как индустрия пришла к современным LLM и что происходит на рынке AI-железа;
почему NVIDIA лидирует, а AMD пока не смогла догнать конкурентов;
чем отличаются компиляторы для нейросетей и почему сложно создать специализированный AI-ускоритель;
как устроены LLM «изнутри» и почему нейросети можно назвать «вредным джинном»;
какие навыки помогут начинающим разработчикам строить карьеру и конкурировать с ИИ.
РБПО по ГОСТ Р 56939—2024: вебинар №12 из 30 – Использование безопасной системы сборки программного обеспечения
Команда ООО "ПВС" совместно с Виталием Пиковым из учебного центра "Маском" провела цикл вебинаров, посвящённых разработке безопасного программного обеспечения (РБПО). Совместно с приглашёнными экспертами различных компаний мы рассмотрели 25 процессов, приведённых в ГОСТ Р 56939—2024.
Обеспечение безопасности при сборке ПО, недопущение привнесения в код ошибок, обусловленных небезопасными преобразованиями кода.
Общее количество вебинаров — 30: каждому из 25 процессов ГОСТа посвящено по одному вебинару и 5 записано дополнительно на смежные темы. Запись всех вебинаров и подборка дополнительной информации, подготовленная Андреем Карповым, доступна по ссылке: ГОСТ56939.РФ.
P.S. Мы регулярно проводим вебинары на различные темы, не обязательно связанные с РБПО. У нас появился подкаст «Разбаговка»! Приглашаем слушать и принять участие в качестве гостя.
Мы каждый день пишем код, но часто воспринимаем компилятор как "чёрный ящик". Сегодня приоткроем завесу тайны над работой компилятора, расскажем о его жизненном цикле и объясним, на каком этапе в игру вступают деревья.
Под капотом скрывается целый конвейер, который включает в себя построение дерева, оптимизацию и генерацию кода. Мы разобрали все этапы на конкретных примерах и написали статью для тех, кто хочет понять, как работает компилятор.
Разработчик Александр Гомес Гайгалас (Alexandre Gomes Gaigalas), автор библиотеки coral для создания переносимых shell-скриптов, опубликовал проект C89cc.sh. Это компилятор для языка C, написанный целиком на Shell.
Компилятор поддерживает стандарт C89 и может генерировать исполняемые файлы в формате ELF64 для систем x86-64. Исходный код проекта содержит около восьми тысяч строк и открыт под лицензией ISC.
Применяем кодогенерацию в Java для решения алгоритмических задач
В прошлый раз мы разобрались, как решается задача трансляции деревьев. И остановились на том, что в случае с AST от компилятора TypeScript, придётся руками обрабатывать 263 типов узлов. Тысячи строк однотипного boilerplate-кода: приведения типов, аннотации, объявления методов — всё это нужно не просто написать, но ещё и поддерживать. А если требования к архитектуре поменяются — переписывать заново.
Однако в случае с Java у нас есть способ упростить себе жизнь — кодогенерация. Нет, не та, что при помощи ИИ-агентов, хотя это мы тоже затронем. Вместо тысяч строк Java кода можно использовать лаконичный конфиг, в котором описывается соответствие узлов и их связи, а всю рутину берёт на себя генератор. Изоморфные преобразования, декомпозиция — всё это описывается там.
Как реализовать это с помощью JavaPoet, что умеет эта библиотека, а также как встроить в процесс нормализацию можно узнать в новом материале, посвящённом использованию кодогенерации для трансляции деревьев.
Слышу уже от второго человека, что язык Rust не дает нормально работать с указателями в связанных списках, деревьях и графах (в моей вселенной ЯП без этого - это как свадьба без невесты). Взял ChatGPT, задал промпт: "write a code to insert a node into a doubly linked list in rust". Оно сгенерило нечто с кучей дополнительных слов, которых не было ни в Си, ни в Паскале 40 лет назад: borrow, as_ref, and_then, upgrade, map, downgrade, Some, clone, borrow_mut. Это все реально нужно или они там совсем озверели?
Стоматологические услуги для компиляторов на примере LLVM 21
Многие слышали миф о маленьких птичках, "чистящих зубы" крокодилам. И пусть в живой природе этого не найти, но зато в мире программ есть свои герои, способные помочь ещё более могучим ящерам — драконам в лице компиляторов. Ну или в нашем случае виверне, ведь именно она на логотипе LLVM, чья очередная версия попала под чистку от багов.
Среди сегодняшних процедур: рытьё истории коммитов, чтение технических спецификаций и краткий румтур по совершенно разным уголкам проекта LLVM — от принтеров дебаг информации до оптимизатора и работы с регистрами.
Например, коснёмся инструкции CPUID:
Предупреждение PVS-Studio: V560 A part of conditional expression is always false: AVX10Ver >= 2. Host.cpp 2177
В выражении HasLeaf24 && (EBX & 0xff) сперва оба операнда && приведутся к типу bool, вычислится логическое "И", а затем результат снова расширится до типа int. На выходе получаем значение 0 или 1, и выражение AVX10Ver >= 2 всегда будет вычисляться как false.
Опечатка, ошибка в логике или кривой мёрж? Git blame и спецификация помогут ответить на этот вопрос, как и на многие другие, если вас заинтересовало – продолжение читайте в статье.
Слышали ли вы про то, что злобные C и C++ компиляторы могут удалить вызов memset в конце функции во время оптимизаций? У нас даже про это есть диагностика V597.
Это давно известная, но при этом живучая потенциальная уязвимость CWE-14: Compiler Removal of Code to Clear Buffers. В следующем коде компилятор удалит заполнение памяти нулями (вызов memset), так как после этого буфер не используется. Раз не используется, то заполнение буфера с точки зрения языка C++ не имеет каких-либо наблюдаемых эффектов и, следовательно, является лишим. Т. е. его можно и нужно удалить с целью оптимизации.
Код позаимствован из статьи про проверку проекта PPSSPP.
Проблема насущная, и для её решения в стандарт C23 внесли новую функцию memset_explicit, которая теперь обязательна для реализации в стандартной библиотеке вместо memset_s. Так вот, автор предложения (Miguel Ojeda, P1315) в своём документе сослался на нашу диагностику (ссылка N3).
И похоже, что он давно про нас знает, т. к. умудрился вставить ссылку ещё аж на старый сайт viva64.
Не зря столько лет говорим про memset. Приятно, что нас уже в proposal-ы затаскивают :)
Питон - предмет обожания секты питонистов, которые ходят по домам и всем говорят "Как, вы еще не выучили Питон? Он же учится за две недели!"
Допустим, но вот два практически идентичных репозитория (1, 2), которые я только что приготовил как форки от двух других практически идентичных репозиториев. Один для создания чипа на немецкой фабрике IHP (The Leibniz Institute for High Performance Microelectronics), а другой для создания чипа на американской фабрике SkyWater (аналог зеленоградского Микрона для военных).
clock = Clock(dut.clk, 10, unit="us")
assert not dut.uio_out.value [4];
Если во втором написать не "unit", а "units", оно пожалуется:
DeprecationWarning: The 'units' argument has been renamed to 'unit'.
DeprecationWarning: The 'units' argument has been renamed to 'unit'.
И типы данных поменялись:
unsupported operand type(s) for >>: 'LogicArray' and 'int'
А все почему? У питониcтов все время меняются версии, и в их коммьюнити не принято поддерживать обратную совместимость:
"Просто используй другую версию!", "Просто поставь виртуальные среды!", "Как, ты еще не используешь Докер? С ним это решается элементарно!" - "Ты просто не pythonian!"
Так можно две недели колупаться, после того как за две недели выучить питон.
Основа Kotlin K2 компилятора — это FIR‑дерево (Frontend Intermediate Representation).
Вкратце: FIR — это AST (абстрактное синтаксическое дерево), обогащённое семантической (смысловой) информацией. Оказывается, что у этой основополагающей технологии есть своя небольшая документация: fir‑basics.md и в той части, где написано про контракты указано (в моём вольном переводе), что:
Компилятор разрешает использовать контракты в свойствах, функциях и конструкторах классов
Вот это поворот! Ведь ранее было замечено их использование только внутри тела функций. В доке написано, что для свойств должно работать, но на практике получаем ошибку.
А где находится то самое ограничение на использование контрактов вне функций описал Android‑разработчик Виталий Перятин в новой статье о Kotlin Contracts, где он поделился любопытными моментами, которые удалось накопать самостоятельно, потому что как парсится список эффектов, как работает новый Contracts API изнутри, и почему, чёрт возьми, на уровне компилятора можно использовать контракты не только на уровне функций, в доках не пишут.
Устройство компилятора (кратко) на LLVM Компилятор - инструмент конвертации исходного кода, написанного на высокоуровневом языке программирования в машинный код, который может исполнять компьютер.
Компилятор делится на 3 этапа:
FRONTEND - анализирует текст исходного кода и преобразует его в IR.
MIDDLE - анализирует и оптимизирует этот сгенерированный код IR.
BACKEND - преобразует IR в машинный код.
Сам компилятор, и собственно язык программирования состоит из нескольких частей.
Lexer - лексер
Лексер сканирует и превращает сырой текст в токены. То есть сам исходный код разбиваеты на набор токенов (такие как литералы, идентификаторы, ключевые слова, операторы, разделители) Лексер читает исходный код символ за символом и идентифицирует последовательности символов, соответствующие определённым правилам языка.
Парсинг
Парсинг немного сложнее чем лексический анализ. Существует множество паресров и парсеров-генераторов.
Парсеры в компиляторах обычно принимают входные данные в форме токенов и строят определенное дерево - AST или дерево парсинга.
Обычно компиляторы строятся из множества маленьких компонентов, которые берут входные данные, меняют их или преобразуют их в различные выходные данные. Это одна из причин, по которым функциональные языки хорошо подходят для создания компиляторов. Другие причины — прекрасное сопоставление с эталоном и довольно обширные стандартные библиотеки. Прикольный факт: первая реализация компилятора Rust была на Ocaml.
Советую держать эти компоненты как можно более простыми и автономными — модульность сильно облегчит процесс. По-моему, то же можно сказать и о многих других аспектах разработки ПО.
AST
AST - абстрактное синтаксическое дерево. Это структурированное представление исходного кода программы в виде дерева, где каждый узел дерева представляет собой синтаксическую конструк языка программирования. Это дерево предоставляет абстракцию, которая позволяет анализировать и манипулировать программным кодом на высоком уровне.
IR
Эта часть занимается созданием [[1.2 IR]]. Через примитивы LLVM мы можем сгенерировать промежуточное представление. Каждому типу в AST дается метод, называемый codegen, который всегда возвращает объект значение LLVM, используемый для представления одного регистра присваивания (single assignment register), который является переменной для компилятора, которая может быть назначена только один раз. Интересно, что в этих примитивах IR то, что в отличии от ассемблера, они не зависят от какой-либо конкретной архитектуры машины, и это значительно упрощает работу для разработчиков языков, которым больше не нужно сопоставлять вывод в набор инструкций процессора. Теперь, когда фронтенд может генерировать IR, инструмент LLVM Optimizer используется для анализа и оптимизации сгенерированного кода. Он выполняет несколько проходов по IR и выполняет такие действия как устранение мертвого кода и скалярная замена агрегатов, и, наконец, это приводит нас к бекенду, где мы пишем модуль, который принимает IR в качестве входных данных, который выдает объектный код, который может работать на любой архитектуре.
Главной особенностью LLVM является промежуточное представление кода (англ. Intermediate Representation, IR), форма, которую использует LLVM для представления кода в компиляторе. LLVM IR был разработан для выполнения функций промежуточного анализа и преобразований внутри оптимизатора компилятора. Ее создание имело целью решение множества специализированных задач, включая поддержку легковесных оптимизаций среды выполнения, кроссфункциональные и межпроцедурные оптимизации, полный анализ программы и агрессивные реструктурирующие преобразования. Промежуточное представление кода определено как язык первого порядка с четкой семантикой.
IR (Intermediate Representation) в контексте LLVM — это промежуточное представление кода. Это низкоуровневое, независимое от платформы и типобезопасное представление программного кода, которое используется в качестве промежуточного языка между интерфейсной частью и серверной частью компилятора.
Этот код LLVM IR соответствует следующему коду на языке C, обеспечивающему возможность сложения целых чисел двумя разными способами:
unsigned add1(unsigned a, unsigned b) {
return a+b;
}
// возможно не самый лучший способ сложения двух чисел
unsigned add2(unsigned a, unsigned b) {
if (a == 0) return b;
return add2(a-1, b+1);
}
Как видно из этого примера, LLVM IR — низкоуровневый RISC-подобный набор виртуальных инструкций. Как и настоящий набор инструкций RISC, он поддерживает линейные последовательности простых инструкций (сложение, вычитание, сравнение и ветвление). Эти инструкции имеют трехадресную форму. Это значит, что они берут некоторое количество входных данных и вычисляют результат в другом регистре. LLVM IR поддерживает метки и в целом выглядит как необычная форма языка ассемблера.
Строго говоря, промежуточное представление LLVM является четко определенным и единственным интерфейсом оптимизатора. Это означает, что всё, что необходимо знать, чтобы писать фронтенды для LLVM, это: что такое LLVM IR, как он работает и какие инварианты ему необходимы. Так как LLVM IR имеет текстовую форму, то имеет смысл создавать фронтенд, который выводит LLVM IR в виде текста, а затем отправляет его на оптимизатор и необходимый генератор кода при помощи каналов Unix.