Русский
Русский
English
Статистика
Реклама

Компиляторы

Начинаем писать под stm8, выбираем среды разработки и стартуем

28.04.2021 12:07:29 | Автор: admin
image

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

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

Первый установка ST Visual Develop и выбор в качестве компилятора COSMIC Бывший платный, а ныне бесплатный, но со своими заморочками; регистрация, получение ключа, и прочие танцы с бубном.

Второй же вариант, более простой VS Code + PlatformIO и компилятор SDCC полностью свободный. И опять же не все так просто. Sdcc не умеет исключать не используемые функции. Я решил этот вопрос хоть и успешно, но не без дополнительных действий при написании кода.

Первая среда, для любителей всё делать правильно


Для начала нам нужен ST Visual Develop. Устанавливаем и ставим запуск ярлыка всегда от администратора. В придачу к нему нам дают ST Visual Programmer, полезный инструмент, особенно когда стоит защита от записи и надо разблокировать микроконтроллер, а ведь китайские blue pill всегда приходят заблокированными. Китайцы бояться что мы украдём их круто оптимизированный Blink.

Вот так выглядит STVD
image
Я так понял её создали, когда в моде были 16 битные цвета...

Дальше нужно будет получить компилятор COSMIC и его лицензионный ключ. Заполняем то что просят, получаем дистрибутив и делаем запрос ключа по электронной почте (тут рулетка кому то сразу придёт, кому то придётся подождать).

После пройденного лабиринта из форм и запросов, когда всё уже установлено, начнём
image
image
При первом запуске нужно указать расположение ключа, его лучше поместить в директорию компилятора. После создания нажимаем F7, ошибок быть не должно. Если писать на чистых регистрах, то всё готово, но я такой хардкор не люблю, поэтому продолжим и добавим SPL библиотеку.

Распакуем куда-нибудь и заходим в папку STM8S_StdPeriph_Lib\Project\STM8S_StdPeriph_Template. Тут у нас шаблон проекта stm8s_conf.h это конфигурационный файл библиотеки, через него выбирается контроллер. Зайдём в main тут сразу с первых строк #include "stm8s.h" это ссылка на основную библиотеку, а так же кусок кода отладки который начинается с #ifdef USE_FULL_ASSERT, без отладочного кода будут сыпаться ошибки.

Теперь когда мы прошлись по верхам давайте пробовать запускать библиотеку. Добавляем в проект из шаблона main и конфигурационный файл в. В include files добавляем всё из STM8S_StdPeriph_Lib\Libraries\STM8S_StdPeriph_Driver\inc.
image
Теперь всё должно собраться.

Добавим stm8s_gpio.c и соберём простецкую мигалку. У меня один из светодиодов висит на D3, конфигурация ноги на выход выглядит так:

GPIO_DeInit(GPIOD);GPIO_Init(GPIOD, GPIO_PIN_3, GPIO_MODE_OUT_PP_LOW_SLOW);

Её вписываем в main до бесконечного цикла.

А вот функция смены состояния. GPIO_WriteReverse(GPIOD, GPIO_PIN_3); вписываем её в бесконечный цикл.

Но вот незадача, в SPL нету функций задержки, скопипастим из примера GPIO в библиотеке. Там она выглядит следующим образом.

void Delay(uint16_t nCount){  /* Decrement nCount value */  while (nCount != 0)  {    nCount--;  }}

Впишем её в конец перед #ifdef USE_FULL_ASSERT. Так же впишем её прототип в начало, где под это выделено место в шаблонном main.

/* Private function prototypes -----------------------------------------------*/void Delay (uint16_t nCount);

Ну и наконец впишем функцию со значением в бесконечный цикл после функции смены состояния: Delay(0xFFFF);

Подключаем ST-Link и прошиваем, для этого нажимаем Start Debugging и Run. Светодиод моргает значит всё хорошо.

У кого не получилось вот полный main.c
/**  ******************************************************************************  * @file    Project/main.c   * @author  MCD Application Team  * @version V2.3.0  * @date    16-June-2017  * @brief   Main program body   ******************************************************************************  * @attention  *  * <h2><center> COPYRIGHT 2014 STMicroelectronics</center></h2>  *  * Licensed under MCD-ST Liberty SW License Agreement V2, (the "License");  * You may not use this file except in compliance with the License.  * You may obtain a copy of the License at:  *  *        http://www.st.com/software_license_agreement_liberty_v2  *  * Unless required by applicable law or agreed to in writing, software   * distributed under the License is distributed on an "AS IS" BASIS,   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  * See the License for the specific language governing permissions and  * limitations under the License.  *  ******************************************************************************  */ /* Includes ------------------------------------------------------------------*/#include "stm8s.h"/* Private defines -----------------------------------------------------------*//* Private function prototypes -----------------------------------------------*/void Delay (uint16_t nCount);/* Private functions ---------------------------------------------------------*/void main(void){GPIO_DeInit(GPIOD);GPIO_Init(GPIOD, GPIO_PIN_3, GPIO_MODE_OUT_PP_LOW_SLOW);  /* Infinite loop */  while (1)  {GPIO_WriteReverse(GPIOD, GPIO_PIN_3);    Delay(0xFFFF);  }  }void Delay(uint16_t nCount){  /* Decrement nCount value */  while (nCount != 0)  {    nCount--;  }}#ifdef USE_FULL_ASSERT/**  * @brief  Reports the name of the source file and the source line number  *   where the assert_param error has occurred.  * @param file: pointer to the source file name  * @param line: assert_param error line source number  * @retval : None  */void assert_failed(u8* file, u32 line){   /* User can add his own implementation to report the file name and line number,     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */  /* Infinite loop */  while (1)  {  }}#endif/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/


Теперь посмотрим со стороны на эту среду.

По плюсам; у неё есть полноценная отладка, поддержка SPL из коробки. По минусам; она старая из этого вытекает, не очень приятный интерфейс и старый диалект си, на который многие свежие библиотеки придётся переписывать, в частности ставить переменные только вначале функций. Вес кода оставляет желать лучшего. Ну последний камень, этот инструмент может просто взять и вылететь, такое бывает, нечасто, но всё же.

Вторая среда, для тех кто не любит заморачиваться.


Второй подход это свободный и обновляемый компилятор SDCC, а так же среда PlatformIO.

Для начала установим VS Code и станем рабами Microsoft, далее найдём расширение PlatformIO.

image

Ждём пока миллион ползунков пройдёт до конца и перезапускаем программу. Открываем расширение (может быть и само откроется). Создаём новый проект и выбираем ближайшую плату с stm8s (микроконтроллер можно будет изменить в конфигурационном файле). В качестве фреймворка выбираем SPL мы же не ардуинщики, нас интересует хардкор.

Не удивляйтесь, проект будет создаваться прилично долго, потому как он на ходу будет подгружать компилятор, библиотеки и тд. И вот перед нами возник девственно голый проект, закрываем его он нам больше не нужен. В меню где мы создавали проект, открываем пример spl-blink, суть в том что blink генерирует неплохой шаблон в котором можно сразу писать. Правим пример под себя и прошиваем, всё моргает.

Разберем поподробнее среду разработки. Во первых весь проект должен лежать в src, иначе среда ведёт себя неадекватно. Во вторых открываем stm8s_conf.h и видим что все библиотеки кроме GPIO закомментированы, если этого не сделать то у мк не хватит памяти что бы поместить весь SPL в микроконтроллер (помните в начале я говорил что он загружает все функции что видит в код?).

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

/* Private define ------------------------------------------------------------*/#define _GPIO_DeInit#define _GPIO_Init#define _GPIO_WriteReverse#define _CLK_DeInit#define _CLK_SYSCLKConfig#define _TIM4_DeInit#define _TIM4_ITConfig#define _TIM4_ClearITPendingBit#define _TIM4_Cmd#define _TIM4_TimeBaseInit#define _TIM4_ClearFlag#define _HAL_GPIO_WritePin#define _GPIO_WriteLow#define _GPIO_WriteHigh//#define STM8S003/* Includes ------------------------------------------------------------------*/#include "stm8s.h"/* Uncomment the line below to enable peripheral header file inclusion */#if defined(STM8S105) || defined(STM8S005) || defined(STM8S103) || defined(STM8S003) ||\    defined(STM8S001) || defined(STM8S903) || defined (STM8AF626x) || defined (STM8AF622x)// #include "stm8s_adc1.h" #endif /* (STM8S105) ||(STM8S103) || (STM8S001) || (STM8S903) || (STM8AF626x) */#if defined(STM8S208) || defined(STM8S207) || defined(STM8S007) || defined (STM8AF52Ax) ||\    defined (STM8AF62Ax)// #include "stm8s_adc2.h"


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

Пройдёмся по преимуществам: работает почти из коробки, полностью бесплатно без попрошайничества, редакции языка обновляются и не придётся учить и переписывать код под си 80-90г. Сам интерфейс настраиваемый и намного приятнее. Для тех кто не любит win есть linux версия.

По минусам: нету полноценной отладки (над ней активно работают, но пока ничего приближённого к cosmic я не видел), в проекте небольшой хаос, раскидать по папкам пока нельзя.

Послевкусие.


Ну и на последок есть ещё среды, варианты и компиляторы, но либо это тот же SDCC вкрученный силой в Eclipse или ещё куда и работающий хуже чем в VS Code, либо это платные варианты IAR, Raisonance. Я лично пользуюсь и тем и тем, но чаще VS Code. Рекомендовать ничего не буду каждому своё, увидимся в комментариях)

Подробнее..

Recovery mode Сборка ядра Linux 5.12.10 c LLVM 12 Clang и LTO оптимизацией

14.06.2021 18:13:31 | Автор: admin

Технический прогресс не стоит на месте, появляются новые компьютерные архитектуры, компиляторы становятся умнее и генерируют более быстрый машинный код. Современные задачи требуют все более креативного и эффективного решения. В данной статье пойдет речь, на мой взгляд, про один из самых прогрессивных тулчейнов LLVM и компиляторы на его основе Clang и Clang++, для языков программирования С и C++ соответственно. Хоть GCC конкурент Clang, может агрессивнее оптимизировать циклы и рекурсию, Clang дает на выходе более корректный машинный код, и чаще всего не ломает поведение приложений. Плюс оптимизация программ не заканчивается только оптимизацией циклов, поэтому Clang местами дает лучшую производительность. В GCC же за счет переоптимизации вероятность получить unpredictable behavior значительно выше. По этой причине на многих ресурсах не рекомендуют использовать -O3 и LTO(Link Time Optimization) оптимизации для сборки программ. Плюс в случае агрессивной оптимизации, размер исполняемых файлов может сильно увеличиться и программы на практике будут работать даже медленнее. Поэтому мы остановились на Clang не просто так и опции компиляции -O3 и LTO работают в нем более корректно. Плюс современные компиляторы более зрелые, и сейчас уже нет тех детских болячек переоптимизации и LTO.

Что меня побудило написать эту статью? В первую очередь это несколько фактов:

  1. Впервые прочел про сборку ядра Linux с LTO оптимизацией и Clang из новостей, где упоминалась компания Google. Она использует Clang и LTO оптимизацию для сборки ядра Linux и получения лучшей производительности. Компания Google для меня является синонимом инноваций, лучших программистов в мире и поэтому для меня ее опыт является самым авторитетным. Плюс она привнесла очень много в развитие open source, и ее наработками пользуются тысячи компаний во всем мире.
  2. Хоть компания Google начала использовать Clang и LTO оптимизацию раньше, только с выходом ядра Linux 5.12.6 и 5.12.7 было закрыто большое количество багов, и сборка ядра c LTO оптимизаций стала доступна многим. До этого при сборке ядра с LTO оптимизацией многие драйвера давали сбой.
  3. Мною уже протестирована работа ядра с LTO на Ryzen 9 3900x + AMD Radeon 5700 XT. Плюс уже давно использую LLVM 12 и Clang для сборки системных программ. Инструментарий LLVM12 и Clang стали основными в моей системе по причине лучшей поддержки моего процессора и нужные мне программы работают быстрее при сборке с помощью Clang. Для программистов Clang дает лучший контроль ошибок, оптимизации и unpredictable behavior. -fdebug-macro, -fsanitize=address, -fsanitize=memory, -fsanitize=undefined, -fsanitize=thread, -fsanitize=cfi, -fstack-protector, -fstack-protector-strong, -fstack-protector-all, -Rpass=inline, -Rpass=unroll, -Rpass=loop-vectorize, -Rpass-missed=loop-vectorize, -Rpass-analysis=loop-vectorize и т.д.
  4. Данная возможность толком нигде не была описана в связи с п.2 и есть подводные моменты, которые будут рассмотрены в данной статье.


В этой статье будет описана сборка ядра Linux 5.12.10 c LLVM 12 + Clang и LTO оптимизацией. Но так как статья получилась бы короткой, то так же бонусом будет рассмотрен вопрос как сделать утилиты LLVM 12 и Clang сборочным инструментарием по умолчанию, и какие программы и библиотеки имеет смысл собрать вручную, чтобы получить лучший отклик и производительность от системы. GCC имеет более лояльную лицензию на использование, и поэтому он установлен во многих дистрибутивах по умолчанию.

Так как в новом ядре фиксится немалое количество багов для работы с моим оборудованием(Ryzen 9 3900x + AMD Radeon 5700 XT) будет рассмотрен вопрос автоматизации сборки и установки нового ядра, чтобы это сильно не отвлекало и занимало минимум времени. Думаю многим это будет полезно. Будет рассмотрен принцип работы моего сборочного скрипта. Все действия будут проводиться в Arch Linux. Если статья будет хорошо оценена, то она станет вводной частью в серию статей про оптимизацию Linux, где будут рассмотрены внутренние механизмы ОС, и как оптимизировать их работу, будут рассмотрены вредные советы и ошибки оптимизации, и будет дан ответ на вопрос оптимизации системы Что для русского хорошо, то для немца смерть!.

Хоть тема оптимизации описывалась многократно, не мало где дают вредные советы, и некоторые механизмы ОС описаны с ошибками. Чаще всего это происходит из-за сложностей перевода или минимальной документации в интернете к компонентам ядра Linux. Где-то информация вовсе устарела. Плюс некоторые вещи понимают программисты, но не понимают системные администраторы, и наоборот. Изначально после установки Linux работает относительно медленно, но благодаря оптимизации и гибкой настройке, можно добиться более высокой производительности и значительно улучшить отклик системы. Arch Linux у меня используется как основная система, и отклик системы, производительность лучше, чем в Windows 10.
Внимание, автор статьи не несет ответственность за причиненный вред в следствии использования данной статьи! Все действия вы выполняете на свой страх и риск! Все действия должны выполнять только профессионалы!


Немного теории



LTO или Link Time Optimization это оптимизация на этапе линковки(компоновки). Чтобы понять, что такое LTO рассмотрим как работают компиляторы. В большинстве компиляторов используется двух этапная модель: этап компиляции и этап линковки.

На этапе компиляции:

Парсятся исходные тексты программ, строится AST Абстрактное Синтаксическое Дерево.

  • Оптимизируется Абстрактное Синтаксическое Дерево. Оптимизируются циклы, удаляется мертвый код, результат которого нигде не используется. Раскрываются выражения, например 2+5 можно заменить на 7, чтобы при работе приложения не вычислять его значение каждый раз и тем самым сделать его быстрее и т.д.
  • Оптимизированное дерево может быть преобразовано в машинный псевдокод понятный компилятору. Псевдокод используется для дополнительной оптимизации, упрощает разработку универсального компилятора для разных архитектур процессора, например для x86-64 и ARMv7\. Так же как ASM листинг, этот псевдокод еще используется, чтобы понять, как компилятор генерирует машинный код, и служит для понимания работы компилятора, поиска ошибок, например, ошибок оптимизации и unpredictable behavior. Стоит заметить этот этап не является обязательным и в некоторых компиляторах отсутствует.
  • Происходит векторизация. Векторизация ,Automatic Vectorization, SIMD
  • Генерируется объектный файл. Объектный файл содержит в себе машинный код для компьютера, и специальные служебные структуры, в которых все еще есть неизвестные адреса функций и данных, поэтому этот файл все еще не может быть запущен на исполнение. Чтобы разрешить неизвестные адреса, был добавлен этап линковки.


Компьютер работает только с бинарными данными, и может оперировать только адресам, поэтому имя функции и переменной ему ничего не говорят. Имя это лишь формальность для программистов, чтобы удобнее было читать исходный код. Во многих компилируемых языках программирования невозможно вычислить адрес функций на этапе компиляции, поэтому в них был придуман механизм описания функций.
Если мы используем функцию и она реализована в каком-то другом файле, то мы должны описать ее имя, параметры и возвращаемое значение. Тем самым мы скажем компилятору, что не надо искать ее в этом файле и она будет добавлена на этапе линковки. Так же, это упрощает парсинг исходных файлов. Нам больше не надо для компиляции и разрешения адресов читать все файлы за один раз. Представьте себе, что у вас исходный код программы занимает несколько гигабайт, такой размер нередко встречается в серьезных программах, тогда оперативной памяти большинства компьютеров не хватило бы, чтобы хранить все служебные структуры компилятора, что значительно повысило бы стоимость разработки и самих программ.

На этапе линковки:

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


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

В Clang используется два вида LTO Оптимизации: Full LTO и Thin LTO. Full LTO это классическая реализация LTO оптимизации, которая обрабатывает конечный исполняемый файл за раз целиком и использует много оперативной памяти. Отсюда эта оптимизация занимает много времени, но дает на выходе самый быстрый код. Thin LTO это развитие LTO оптимизации, в которой нет оптимизации всего файла целиком, а вместо этого вместе с объектными файлами записывают дополнительные метаданные, и LTO оптимизатор работает с этими данными, что дает более высокую скорость получения оптимизированного исполняемого файла (скорость сравнима с линковкой файла без LTO оптимизации) и код сравнимый или чуть уступающий в производительности Full LTO. Но самое главное Full LTO может значительно увеличить размер файла, и код наоборот может из-за этого работать медленнее. Thin LTO лишен этого недостатка и в некоторых приложениях на практике мы можем получить лучшую производительность! Поэтому наш выбор будет сборка ядра Linux с Thin LTO.

Дополнительная информация:



Установка LLVM 12 и Clang



Поставить llvm и clang можно выполнив в консоли под root команду:

pacman -Syu base-devel llvm clang lld vim

Это самый простой вариант установки, но лично предпочитают новые версии ПО и git версия закрыла часть багов компилятора и даже стабильнее релиза. Так как за время написания статьи многое поменялось, вышел официальный пакет llvm 12, то чтобы понять ход мыслей, рекомендуется к прочтению прошлая версия по установке.

Прошлая версия
На момент написания статьи, в дистрибутиве Arch Linux используются LLVM и Clang версии 11\. А LLVM и Clang версии 12 находятся в staging репозитории Arch Linux [LLVM](http://personeltest.ru/aways/archlinux.org/packages/staging/x86_64/llvm/). Staging репозиторий это репозиторий, где находятся версии пакетов, которые ломают приложения, зависящие от прошлой версии. Он используется для компиляции всех зависящих программ, и когда все они будут собраны, все пакеты за раз переходит в общий репозиторий. Например, в Arch Linux от LLVM и Clang версии 11 зависят blender, rust и qt creator и т.д. Если мы поставим LLVM и Clang версии 12, то они перестанут работать.
Upd. Пакет уже перешел в основной репозиторий. Так как мною одним из первых была произведена миграция на LLVM и Clang 12, то было придумано простое решение, создать пакет [llvm11-libs](http://personeltest.ru/aways/aur.archlinux.org/packages/llvm11-libs-bin/) с необходимыми библиотеками для обратной совместимости, который позволяет оставить зависимые программы рабочими. Но данный пакет работает только с моим сборочным пакетом [llvm12-git](http://personeltest.ru/aways/aur.archlinux.org/packages/llvm12-git/). Поэтому мы будем собирать LLVM и Clang 12 из исходников. Но вы можете дождаться, когда LLVM и Clang 12 появятся в основном репозитории Arch Linux или использовать 11 версию. Лично предпочитают новые версии ПО, и LLVM и Clang 12 лучше поддерживают мой процессор Ryzen 9 3900X. Плюс git версия закрыла часть багов компилятора и даже стабильнее релиза. Релизный архив с официального сайта у меня не проходит больше тестов при сборке чем git версия. Не стоит пугаться того, что часть тестов компилятор провалил, там нет критических багов для x84-64 архитектуры, и большая часть затрагивают другие компоненты, например openmp и lldb. За очень долгое время тестирования llvm и clang 12 мною не было замечено ни одного бага влияющего на работу системы. Стоит заметить, на данный момент 13 версия является очень сырой и нам не подходит!

Поставим llvm и clang 11 версии(Если 12 версия появилась в основном репозитории, то поставится 12я версия) можно выполнив в консоли под root команду:

pacman -Syu base-devel llvm clang lld libclc vim

Обновить Arch Linux и поставить новые версии программ можно командой(это будет полезно тем кто будет ждать официального выхода 12 версии, думаю это произойдет уже через пару дней):

pacman -Syu

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


Cборка LLVM 12 из Arch User Repository



Для сборки нам понадобиться git и нам надо будет собрать программу yay.

Поставим необходимые зависимости, для этого нам будут нужны права root: pacman -Syu base-devel git go vim

Если вы хотите собрать llvm 12 с помощью clang 11, то надо поставить еще их: pacman -S llvm clang

Отредактируем конфигурационный файл сборщика пакетов makepkg в Arch Linux и увеличим количество потоков для сборки программ. Это ускорит скорость сборки. Под root выполним: vim /etc/makepkg.conf

Найдем строки MAKEFLAGS и NINJAFLAGS. Нажмем латинскую букву A. Нам после -j надо указать количество потоков для сборки. Рекомендуется ставить ваше количество ядер или потоков процессора, если ядер 4, то ставим 4 или 8\. У меня это 20, 12 ядер 24 потока, 4 остаются запасными для других задач. Или используем автоматическое определение $(nproc).

В итоге получим:

MAKEFLAGS="-j20"NINJAFLAGS="-j20"

или

MAKEFLAGS="-j$(nproc)"NINJAFLAGS="-j$(nproc)"


Нажмем ESC, дальше SHIFT + :(буква Ж). Внизу появится : строка для ввода команд, вводим wq. w write, записать изменения в файл. q quit, выйти из vim. q! выход из vim без сохранения файла. Кому сложно разобраться с vim, в Linux есть замечательная программа, называется она vimtutor. Если у вас настроена правильно локаль, то vimtutor будет на русском, запустить его можно командой vimtutor. Стоит заметить, вопреки распространенному мнению, обучение у вас не займет много времени. Обычно новичков пугают мифом: vi и vim люди изучают очень долго, и осилить их могут только единицы. На самом деле это не так и там нет ничего сложного.

Под обычным пользователем клонируем репозиторий yay, собираем и устанавливаем:
git clone https://aur.archlinux.org/yay.git && cd yay && makepkg -cfi

Импортирует открытый gpg ключ, он необходим для проверки подписи llvm12-git:
gpg --keyserver pgp.mit.edu --recv-keys 33ED753E14757D79FA17E57DC4C1F715B2B66B95

Поставим LLVM 12 и библиотеки совместимости с 11 версией. Стоит заметить, мой пакет LLVM 12 уже содержит все необходимые утилиты, включая Clang и LLD и их не надо ставить отдельно. Под обычным пользователем выполним команду: yay -Syu llvm12-git. Если llvm 12 есть в официальном репозитории, то llvm11-libs-bin не нужно ставить. Команда yay задаст вам несколько вопросов, нажмите Enter в ответ на все. Сборщик LLVM задаст 3 вопроса:

  • Build with clang and llvm toolchain? Собрать с помощью llvm и clang? Отвечаем Y или Enter если да, и N если нет. Рекомендую собирать LLVM с помощью Clang.
  • Skip build tests? Пропустить сборку тестов? Отвечаем Y или Enter. Так как во время сборки, не все тесты проходят проверку, то сборка будет прекращена. Поэтому мы пропускаем сборку тестов, и на самом деле сборка будет идти даже быстрее.
  • Skip build documentation? Пропустить сборку документации? Отвечаем Y или Enter если да, и N если нет. Если вам не нужна документация, то можно пропустить, это ускорит сборку. Лучше читать документацию на официальном сайте, это удобнее.
  • Skip build OCaml and Go bindings? Пропустить сборку OCaml и Go биндингов? Отвечаем Y или Enter если да, и N если нет. Для большинства ответ Y и их сборку можно смело пропустить в угоду скорости сборки. Для тех кому они нужны, а это очень маленькое количество людей могут ответить N.


Сборка может занять от 20 минут до пары часов. Ждете и в конце отвечаете Y на вопрос: хотите ли вы поставить собранные пакеты?

После установка LLVM надо собрать libclc12-git yay -S libclc12-git. libclc необходим для компиляции opencl и для сборки mesa.

Делаем LLVM и Clang сборочным тулчейном по умолчанию в Arch Linux



Большинство программ в Arch Linux собираются с помощью команды makepkg: man makepkg и PKGBUILD файлов. Поэтому в первую очередь внесем изменения в конфигурационный файл /etc/makepkg.conf. Выполним под root в консоли команду: vim /etc/makepkg.conf. Перейдем к строке CHOST="x86_64-pc-linux-gnu" поставим курсор на следующей пустой строке и нажмем латинскую букву A, и вставим после строки:

export CC=clangexport CXX=clang++export LD=ld.lldexport CC_LD=lldexport CXX_LD=lldexport AR=llvm-arexport NM=llvm-nmexport STRIP=llvm-stripexport OBJCOPY=llvm-objcopyexport OBJDUMP=llvm-objdumpexport READELF=llvm-readelfexport RANLIB=llvm-ranlibexport HOSTCC=clangexport HOSTCXX=clang++export HOSTAR=llvm-arexport HOSTLD=ld.lld

Дальше заменим строки CPPFLAGS, CXXFLAGS, LDFLAGS на содержимое ниже:

CFLAGS="-fdiagnostics-color=always -pipe -O2 -march=native -fstack-protector-strong"CXXFLAGS="-fdiagnostics-color=always -pipe -O2 -march=native -fstack-protector-strong"LDFLAGS="-Wl,-O1,--sort-common,--as-needed,-z,relro,-z,now"

Если вкратце мы используем -O2 оптимизацию для всех программ, -fstack-protector-strong используем улучшенную защиту стека, что снижает вероятность потенциально опасных ошибок при работе со стеком в программах, она же включена у меня в ядре. Плюс на моем процессоре при сборке с Clang с -fstack-protector-strong код при работе с целыми числами работает чуть быстрее, при работе с числами с плавающей запятой есть небольшой оверхед. В GCC наоборот есть более заметный оверхед и производительность снижается. -march=native есть смысл заменить на ваш, у меня это -march=znver2 gcc.gnu.org/onlinedocs/gcc/x86-Options.html.

Изменим количество потоков в MAKEFLAGS и NINJAFLAGS для сборки программ. Это помогает ускорить сборку программ. После -j надо указать количество потоков для сборки. Рекомендуется ставить ваше количество ядер или потоков процессора, если ядер 4, то ставим 4 или 8\. У меня это 20, 12 ядер, 24 потока, 4 остаются запасными для других задач. Или используем автоматическое определение $(nproc).

В итоге получим:

MAKEFLAGS="-j20"
NINJAFLAGS="-j20"


или

MAKEFLAGS="-j$(nproc)"
NINJAFLAGS="-j$(nproc)"


Из DEBUG_CFLAGS и DEBUG_CXXFLAGS надо удалить -fvar-tracking-assignments. LLVM не поддерживает данный параметр.

Файл должен будет принять примерно такой вид:

CARCH="x86_64"CHOST="x86_64-pc-linux-gnu"CARCH="x86_64"CHOST="x86_64-pc-linux-gnu"#-- Compiler and Linker Flagsexport CC=clangexport CXX=clang++export LD=ld.lldexport CC_LD=lldexport CXX_LD=lldexport AR=llvm-arexport NM=llvm-nmexport STRIP=llvm-stripexport OBJCOPY=llvm-objcopyexport OBJDUMP=llvm-objdumpexport READELF=llvm-readelfexport RANLIB=llvm-ranlibexport HOSTCC=clangexport HOSTCXX=clang++export HOSTAR=llvm-arexport HOSTLD=ld.lldCPPFLAGS="-D_FORTIFY_SOURCE=2"CFLAGS="-fdiagnostics-color=always -pipe -O2 -march=native -fstack-protector-strong"CXXFLAGS="-fdiagnostics-color=always -pipe -O2 -march=native -fstack-protector-strong"LDFLAGS="-Wl,-O1,--sort-common,--as-needed,-z,relro,-z,now"RUSTFLAGS="-C opt-level=2"#-- Make Flags: change this for DistCC/SMP systemsMAKEFLAGS="-j20"NINJAFLAGS="-j20"#-- Debugging flagsDEBUG_CFLAGS="-g"DEBUG_CXXFLAGS="-g"#DEBUG_CFLAGS="-g -fvar-tracking-assignments"#DEBUG_CXXFLAGS="-g -fvar-tracking-assignments"#DEBUG_RUSTFLAGS="-C debuginfo=2"

Нажмем ESC, дальше SHIFT + :(буква Ж). Внизу появится: строка для ввода команд, вводим wq. w write, записать изменения в файл. q quit, выйти из vim. q! выход из vim без сохранения файла. Кому сложно разобраться с vim, в Linux есть замечательная программа, называется она vimtutor. Если у вас настроена правильно локаль, то vimtutor будет на русском, запустить его можно командой `vimtutor`. Стоит заметить, вопреки распространенному мнению, обучение у вас не займет много времени. Обычно новичков пугают мифом: vi и vim люди изучают очень долго, и осилить их могут только единицы. На самом деле это не так и там нет ничего сложного.

Следующим этапом можно добавить настройки в файл .bashrc текущего пользователя. Не root, сборка программ под root очень плохая идея! Это относительно вредный совет и с помощью clang будут собираться все программы! Поэтому делайте это только если хорошо понимаете зачем это вам. Это можно сделать командой:

cat << 'EOF' >> "${HOME}/.bashrc"export CARCH="x86_64"export CHOST="x86_64-pc-linux-gnu"export CC=clangexport CXX=clang++export LD=ld.lldexport CC_LD=lldexport CXX_LD=lldexport AR=llvm-arexport NM=llvm-nmexport STRIP=llvm-stripexport OBJCOPY=llvm-objcopyexport OBJDUMP=llvm-objdumpexport READELF=llvm-readelfexport RANLIB=llvm-ranlibexport HOSTCC=clangexport HOSTCXX=clang++export HOSTAR=llvm-arexport HOSTLD=ld.lldexport CPPFLAGS="-D_FORTIFY_SOURCE=2"export CFLAGS="-fdiagnostics-color=always -pipe -O2 -march=native -fstack-protector-strong"export CXXFLAGS="-fdiagnostics-color=always -pipe -O2 -march=native -fstack-protector-strong"export LDFLAGS="-Wl,-O1,--sort-common,--as-needed,-z,relro,-z,now"export RUSTFLAGS="-C opt-level=2"export MAKEFLAGS="-j20"export NINJAFLAGS="-j20"export DEBUG_CFLAGS="-g"export DEBUG_CXXFLAGS="-g"EOF


Список системных библиотек и программ которые стоит собирать вручную


Внимание, сборка всех программ и все консольные команды надо выполнять под обычным пользователем, перед установкой у вас попросит пароль root. Сборка всех библиотек и программ из списка не занимает много времени. Все кроме Mesa у меня собирается в районе 1 минуты. Список дан в той в последовательности в которой рекомендуется сборка! К примеру от zlib-ng и zstd зависит Mesa, а от Mesa зависит xorg-server.

Самое первое, что надо сделать в Arch Linux это заменить zlib на zlib-ng. Это дает хороший выигрыш производительности в приложениях, которые зависят от zlib. Больше всего это заметно на веб браузерах и веб серверах, которые используют gzip сжатие для передачи данных. На высоко нагруженных серверах это дает очень значительную прибавку к производительности. Сборка довольно быстрая. Поставить можно командой(под обычным пользователем): yay -Syu zlib-ng. На вопрос хотите ли вы удалить zlib отвечайте Y. Не бойтесь библиотеки полностью взаимозаменяемы, и ничего не сломается!

Дальше у нас идет zstd это вторая по популярности библиотека используемая в ядре и в программах для сжатия данных. Поэтому имеет смысл собрать так же ее. Чтобы собрать, вам нужно скопировать содержимое zstd, создать директорию, например zstd, а в ней создать файл PKGBUILD и в него вставить содержимое по ссылке. Дальше в консоли перейти в директорию содержащую PKGBUILD, выполнить команду makepkg -cfi .

libjpeg-turbo Библиотека для работы c jpeg файлами. Ее очень часто используют браузеры и программы рабочего стола. libjpeg-turbo собранный с clang дает у меня лучшую производительность. Действия такие же, как в zstd. Создать директорию, и вставить в файл PKGBUILD содержимое по ссылке libjpeg-turbo. Дальше в консоли перейдите в директорию содержащую PKGBUILD, выполнить команду makepkg -cfi.

libpng Библиотека для работы с PNG файлами. По сборке и установке все то же самое. libpng. Для сборки вам понадобится патч: 72fa126446460347a504f3d9b90f24aed1365595.patch, его надо положить в одну директорию с файлом PKGBUILD. Для сборки надо внести изменения в PKGBUILD, заменить source и sha256sums на строки ниже, и добавить функцию prepare.

source=("https://downloads.sourceforge.net/sourceforge/$pkgname/$pkgname-$pkgver.tar.xz"  "72fa126446460347a504f3d9b90f24aed1365595.patch")sha256sums=('505e70834d35383537b6491e7ae8641f1a4bed1876dbfe361201fc80868d88ca'  '84298548e43976265f414c53dfda1b035882f2bdcacb96ed1bc0a795e430e6a8')prepare() {  cd $pkgname-$pkgver  patch --forward --strip=1 --input="${srcdir:?}/72fa126446460347a504f3d9b90f24aed1365595.patch"}


Mesa это святой грааль для всех графических приложений. Стоит собирать всегда вручную, дает хорошую прибавку в десктоп приложениях, улучшается отклик рабочего стола. Одно время сидел на git версии, чтобы получить лучшую поддержку новых видеокарт AMD. Вот мой PKGBUILD оптимизированный для сборки с помощью Clang.

Для сборки вам надо отредактировать файл mesa.conf и установить необходимые вам драйвера dri, gallium, vulkan для сборки. У меня сборка только под новые видеокарты AMD. Подглядеть можно тут: Mesa OpenGL, mesa-git package, Mesa Documentation. При выходе новой версии Mesa не забудьте сменить 21.1.2 на новую версию. А после смены версии обновите контрольные суммы файлов, выполнив в директории с PKGBUILD команду updpkgsums.

xorg-server X сервер с которым взаимодействуют почти все среды рабочего стола. Сборка дает заметное улучшение отклика рабочего стола. Сборка такая же через mapkepkg -cfi. Скачать необходимые файлы для сборки можно тут: xorg-server Сборочный пакет немного кривой и собирает пакет без оптимизаций. Поэтому его надо пропатчить. Для это после строки arch-meson ${pkgbase}-$pkgver build \ надо добавить строки:

  -D debug=false \  -D optimization=2 \  -D b_ndebug=true \  -D b_lto=true \  -D b_lto_mode=thin \  -D b_pie=true \

Полный список критических важных программ влияющих на производительность системы вы можете посмотреть в поем github репозитории arch-packages. Список был создан с помощью системного профилировщика perf. Все сборочные файлы оптимизированы для сборки с помощью llvm и сборка полностью автоматизирована. На моем ryzen 9 3900x сборка всего занимает около 20 минут. Единственный пакет который невозможно собрать с помощью clang и llvm это glibc. Его надо собирать вручную, и с оптимизацией -march= под ваш процессор, это самая часто вызываемая библиотека. Сборку glibc могут проводить только профессионалы, понимающие, что они делают. Не правильная сборка может сломать систему!

Для того, что бы воспользоваться автоматизированной сборкой надо выполнить(под обычным пользователем):
git clone https://github.com/h0tc0d3/arch-packages.git && cd arch-packages && chmod +x build.sh

Дальше нам надо установить все gpg сертификаты и зависимости необходимые для сборки, выполним ./build.sh --install-keys, а затем ./build.sh --install-deps

Для сборки программ достаточно просто запустить скрипт: ./build.sh --install, скрипт вам будет задавать вопросы, какие программы хотите собрать и поставить. На вопрос: хотите ли вы отправить все ваши деньги и пароли автору статьи? хотите ли вы заменить программы?(например, zlib-ng и zlib конфликтуют. Удалить zlib? [y/N] ) ответьте Y . Если вам нужна принудительная пересборка всех программ, то надо выполнить ./build.sh --install --force. По умолчанию, если пакет был уже собран и найден с нужной версией, то он не собирается, а просто устанавливается.

Для сборки mesa надо отредактировать файл mesa/mesa.conf и установить необходимые вам драйвера dri, gallium, vulkan для сборки.

С помощью команды ./build.sh --check можно проверить различия версий в моем репозитории и в официальном, помогает быстро адаптировать сборочные файлы и собрать актуальные версии программ. Слева версия в моем репозитории, справа от стрелки в официальном. Мой репозиторий может служить удобной тренировочной точкой на пути к созданию своего дистрибутива, создания LFS и развитию навыка пересборки ПО не ломая систему.

[+] zstd 1.5.0-1[+] libpng 1.6.37-3[+] libjpeg-turbo 2.1.0-1[+] mesa 21.1.2-1[+] pixman 0.40.0-1[-] glib2 2.68.3-1 -> 2.68.2-1[+] gtk2 2.24.33-2[+] gtk3 1:3.24.29-2[+] gtk4 1:4.2.1-2[+] qt5-base 5.15.2+kde+r196-1[+] icu 69.1-1[+] freetype2 2.10.4-1[+] pango 1:1.48.5-1[+] fontconfig 2:2.13.93-4[+] harfbuzz 2.8.1-1[+] cairo 1.17.4-5[+] wayland-protocols 1.21-1[+] egl-wayland 1.1.7-1[+] xorg-server 1.20.11-1[+] xorgproto 2021.4-1[+] xorg-xauth 1.1-2[+] xorg-util-macros 1.19.3-1[+] xorg-xkbcomp 1.4.5-1[+] xorg-setxkbmap 1.3.2-2[+] kwin 5.22.0-1[+] plasma-workspace 5.22.0-2[+] glibc 2.33-5


Сборка Ядра с помощью LLVM и Clang с LTO оптимизацией


Внимание! Сборку ядра необходимо выполнять под обычным пользователем. Перед установкой ядра у вас попросит sudo пароль. Не рекомендуется использовать патчи ядра linux-ck, linux-zen, MuQSS и т.д. Мною были протестированы все, при кажущемся увеличении производительности системы, происходят кратковременные лаги и снижается стабильность системы, некоторые подсистемы ядра работают не стабильно! С выходом ядра 5.11 стандартный планировщик работает не хуже и значительно стабильнее! Единственный патч который мною применяется это патч для применения оптимизации под процессор github.com/graysky2/kernel_gcc_patch Выбрать ваш процессор можно в меню конфигуратора ядра Processor type and features-->Processor family.

Сборка ядра с помощью LLVM описана в официальной документации Linux Kernel Build with LLVM. Но там есть несколько подводных моментов, которые не описаны. Первый подводный момент заключается в OBJDUMP=llvm-objdump, тут идет переопределение objdump, но так как параметры objdump в llvm имеет другой синтаксис, то при сборке будет пропущена часть тестов для проверки корректности сборки, и будет warning ругающийся на objdump. Правильно будет оставить родной objdump OBJDUMP=objdump

Неправильно:

make CC=clang LD=ld.lld AR=llvm-ar NM=llvm-nm STRIP=llvm-strip \  READELF=llvm-readelf HOSTCC=clang HOSTCXX=clang++ \  HOSTAR=llvm-ar HOSTLD=ld.lld OBJCOPY=llvm-objcopy OBJDUMP=llvm-objdump


Правильно:

make CC=clang LD=ld.lld AR=llvm-ar NM=llvm-nm STRIP=llvm-strip \  READELF=llvm-readelf HOSTCC=clang HOSTCXX=clang++ \  HOSTAR=llvm-ar HOSTLD=ld.lld OBJCOPY=llvm-objcopy OBJDUMP=objdump

Второй подводный момент заключается в том, что если мы не добавим LLVM_IAS=1 в строку make, то нам не будет доступна LTO оптимизация в конфигураторе ядра!

Поэтому полная строка для сборки с LTO будет:

export BUILD_FLAGS="LLVM=1 LLVM_IAS=1 CC=clang CXX=clang++ LD=ld.lld AR=llvm-ar NM=llvm-nm STRIP=llvm-strip READELF=llvm-readelf HOSTCC=clang HOSTCXX=clang++ HOSTAR=llvm-ar HOSTLD=ld.lld OBJCOPY=llvm-objcopy OBJDUMP=objdump"make ${BUILD_FLAGS} -j$(nproc)

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

export BUILD_FLAGS="LLVM=1 LLVM_IAS=1 CC=clang CXX=clang++ LD=ld.lld AR=llvm-ar NM=llvm-nm STRIP=llvm-strip READELF=llvm-readelf HOSTCC=clang HOSTCXX=clang++ HOSTAR=llvm-ar HOSTLD=ld.lld OBJCOPY=llvm-objcopy OBJDUMP=objdump"tar -xf linux-5.12.10.tar.xz -C /tmpcd /tmp/linux-5.12.10zcat /proc/config.gz > .config # Берем конфигурацию запущенного ядра из /proc/config.gz и используем ее для сборкиecho "-mykernel" > .scmversionmake ${BUILD_FLAGS} oldconfigmake ${BUILD_FLAGS} -j$(nproc) nconfig

C помощью oldconfig конфигурация адаптируется под новое ядро и запускается конфигуратор nconfig. Подробнее о конфигураторах ядра можно прочесть в официальной документации [Kernel configurator](http://personeltest.ru/aways/www.kernel.org/doc/html/latest/kbuild/kconfig.html).

В конфигураторе переходим в General architecture-dependent option --> Link Time Optimization (LTO) и выбираем Clang ThinLTO (EXPERIMENTAL). Для дополнительной защиты стека в General architecture-dependent options ставим \* напротив Stack Protector buffer overflow detection и Strong Stack Protector. Жмем F9 и сохраняем новый конфигурационный файл. Далее идет список команд для сборки и установки нового ядра.

make ${BUILD_FLAGS} -j$(nproc)make ${BUILD_FLAGS} -j$(nproc) modulessudo make ${BUILD_FLAGS} -j$(nproc) modules_installsudo cp -v arch/x86_64/boot/bzImage /boot/vmlinuz-mykernel

Следующий подводный момент заключается в DKMS, после установки ядра собранного с помощью Clang, DKMS пытается собрать модули ядра с помощью GCC. По этой причине сборка и установка DKMS модулей в новое ядро завершается ошибкой. Решение проблемы заключается в передаче DKMS компилятора Clang таким образом:

sudo ${BUILD_FLAGS} dkms install ${dkms_module} -k 5.12.10-mykernel


Автоматизация сборки ядра Linux



Для автоматизации сборки ядра мы будем использовать мой bash скрипт github.com/h0tc0d3/kbuild. Клонируем репозиторий и перейдем в рабочую директорию: git clone https://github.com/h0tc0d3/kbuild.git && cd kbuild && chmod +x kbuild.sh

Отредактируем файл build.sh или поместим содержимое ниже в файл ${HOME}/.kbuild. Рекомендуется второй способ vim "${HOME}/.kbuild" т.к. при обновлении скрипта наши настройки сохранятся. Если использовалось клонирование репозитория git, то в директории со скриптом можно выполнить команду git pull, чтобы обновить скрипт. Ниже даны параметры по умолчанию, они формируют поведение скрипта по умолчанию, если соответствующий параметр не был передан. Эти параметры в дальнейшем можно будет переопределить с помощью параметров командной строки для скрипта. Так же можно добавить команду в ваш .bashrc. Для этого в директории со скриптом kbuild.sh надо выполнить echo "alias kbuild='${PWD}/kbuild.sh" >> "${HOME}/.bashrc", ${PWD} автоматом заменит на текущую директорию. Или из любой другой директории можно указать полный пусть к скрипту echo "alias kbuild='полный-путь/kbuild.sh'" >> "${HOME}/.bashrc" После редактирования .bashrc необходимо перезапустить терминал! Теперь можно будет запускать скрипт командой kbuild --help .

KERNEL_VERSION='5.12.10'         # Версия Linux для сборки. Любая версия с официального сайта kernel.org, включая rc версии.KERNEL_POSTFIX='noname'         # Постфикс для названия ядра. Ядро будет иметь имя версия-постфикс, 5.12.10-noname, нужно для разделения в системе ядер с одной версией.KERNEL_CONFIG='/proc/config.gz' # Конфигурационный файл ядра. Поддерживает любые текстовые файлы и с жатые с расширением gz.KERNEL_CONFIGURATOR='nconfig'   # Конфигуратор ядра nconfig, menuconfig, xconfig.# Рекомендую использовать nconfig, он лучше menuconfig.# Можно писать полную строку, например MENUCONFIG_COLOR=blackbg menuconfig# Дополнительную информацию можно найти в документации к ядру https://www.kernel.org/doc/html/latest/kbuild/kconfig.htmlMKINITCPIO=1 # Запускать "mkinitcpio -p конфигурационный_файл" После сборки? 0 - Нет, 1 - Да.MKINITCPIO_CONFIG="${KERNEL_POSTFIX}" # Имя конфигурационного файла mkinitcpio, по умолчанию равно постфиксу.CONFIGURATOR=0      # Запускать конфигуратор ядра? 0 - Нет, 1 - Да. Если вам не нужно конфигурировать ядро, то можно поставить 0.LLVM=0              # Использовать LLVM Для сборки? 1 - Да, 0 - Нет(Будет использован GCC или другой системный компилятор по умолчанию)THREADS=8           # Количество поток для сборки. Ускоряет сборку. Для автоматического определения надо заменить на $(nproc)BUILD_DIR='/tmp'    # Директория в которой будет проходить сборки ядра. У меня 32gb оперативной памяти и сборка происходит в tmpfs.DOWNLOAD_DIR=${PWD} # Директория для сохранения архивных файлов с исходниками ядра. ${PWD} - в папке из которой запущен скрипт сборки.DIST_CLEAN=0    # Если директория с исходниками существует выполнять make disclean перед сборкой? 0 - Нет, 1 - ДаCLEAN_SOURCE=0  # Выполнять make clean после сборки ядра? 0 - Нет, 1 - ДаREMOVE_SOURCE=1 # Удалять директорию с исходными файлами ядра после сборки? 0 - Нет, 1 - Да.SYSTEM_MAP=0    # Копировать System.map в /boot После сборки? 0 - Нет, 1 - Да.PATCH_SOURCE=1                          # Применять патчи ядра? 0 - Нет, 1 - Да.PATCHES=("${HOME}/confstore/gcc.patch") # Список патчей ядра. Нельзя поменять с помощью параметров скрипта.DKMS_INSTALL=1                                        # Выполнять DKMS Install? 0 - Нет, 1 - Да.DKMS_UNINSTALL=1                                      # Выполнять DKMS Uninstall? 0 - Нет, 1 - Да.DKMS_MODULES=('openrazer-driver/3.0.1' 'digimend/10') # Список DKMS модулей, который нужно собрать и установить. Нельзя поменять с помощью параметров скрипта.

Внимание! Сборку ядра необходимо выполнять под обычным пользователем. Перед установкой ядра у вас попросит sudo пароль. Не рекомендуется использовать патчи ядра linux-ck, linux-zen, MuQSS и т.д. Мною были протестированы все, при кажущемся увеличении производительности системы, происходят кратковременные лаги и снижается стабильность системы, некоторые подсистемы ядра работают не стабильно. С выходом ядра 5.11 стандартный планировщик работает не хуже и значительно стабильнее! Единственный патч который мною применяется это патч для применения оптимизации под процессор github.com/graysky2/kernel_gcc_patch. Нас интересует файл more-uarches-for-kernel-5.8+.patch. Путь к нему имеет смысл указать в PATCHES. Выбрать ваш процессор можно в меню конфигуратора ядра Processor type and features-->Processor family.

Принцип работы скрипта:

1) set -euo pipefail скрипт переходит в строгий режим, в случае ошибок скрипт завершается с ошибкой. Является хорошим тоном, при написании bash скриптов. Скрипт проверяет запущен ли он под рут, если запущен под рут, то выдает ошибку и завершается. Загружается настройки пользователя из файла ${HOME}/.kbuild

2) Скрипт проверяет существование директории linux-версия в директории BUILD_DIR. Если существует, то исходники распакованы. Перед сборкой может выполняться команда make distclean, поведение задается переменной DIST_CLEAN. Если этой директории не существует, то проверяется существование файла linux-версия.tar.gz

или linux-версия.tar.xz. Если файл найден, то он распаковывается в BUILD_DIR. Иначе файл скачивается с kernel.org в директорию DOWNLOAD_DIR.

3) Скрипт применяет патчи ядра и устанавливает постфикс для версии ядра(записывает его в файл .scmversion ).

4) Скрипт копирует настройки ядра из файла KERNEL_CONFIG в .config и выполняет make oldcofig для адаптации настроек под новое ядро и запускает конфигуратор ядра.

5) Скрипт собирает ядро и модули.

6) Скрипт удаляет модули DKMS из ядра которое сейчас запущено, если это необходимо. Это необходимо, чтобы в списке dkms status не отображались мертвые ядра. Удаляет директорию `/lib/modules/версия-постфикс` если она существует. Она существует в том случае, если мы собираем одну и туже версию несколько раз. Это дополнительная защита от unpredictable behavior .

7) Скрипт устанавливает модули ядра, копирует ядро в /boot/vmlinuz-постфикс.

8) Скрипт собирает DKMS модули и устанавливает их. Копирует System.map в /boot/System-постфикс.map, если это необходимо.

9) Обновляет загрузочный img файл для ядра. Выполняет mkinitcpio -p конфиг.

10) Выполняет make clean если необходимо. Удаляет директорию linux-версия в директории BUILD_DIR, если это необходимо.

Собрать ядро с llvm можно командой ./kbuild.sh -v 5.12.10 --llvm --start или kbuild -v 5.12.10 --llvm --start, если был установлен alias. -v 5.12.10 указывает версию ядра для сборки, --llvm указывает собирать ядро с помощью llvm и clang. --start указывает, что надо запускать конфигуратор ядра. Получить справку по параметрам скрипта можно выполнив команду kbuild --help.

Русская справка
Параметры: Описание: Пример:
--version, -v Версия ядра для сборки --version 5.12.10 | -v 5.13-rc4
--postfix, -p Постфикс ядра --postfix noname | -p noname
--config, -c Файл конфигурации ядра --config /proc/config.gz | -c /proc/config.gz
--dir, -d Директории сборки --dir /tmp | -d /tmp
--download, -z Директория загрузки --download /tmp | -z /tmp
--threads, -t Количество потоков сборки --threads 8 | -t 8
--configurator, -x Конфигуратор ядра --configurator nconfig | -x "MENUCONFIG_COLOR=blackbg menuconfig"

--start, -s Запускать конфигуратор
--disable-start, -ds Не запускать конфигуратор

--mkinitcpio, -mk Запускать mkinitcpio после установки ядра
--disable-mkinitcpio, -dmk Не запускать mkinitcpio после установки ядра
--mkinitcpio-config, -mc Конфиг mkinitcpio --mkinitcpio-config noname | -mc noname

--llvm, -l Использовать LLVM
--disable-llvm, -dl Не использовать LLVM

--patch, -ps Применять патчи ядра
--disable-patch, -dp Не применять патчи ядра

--map, -m Копировать System.map в /boot/System-постфикс.map
--disable-map, -dm Не копировать System.map

--clean, -cs Чистить исходники после сборки. make clean
--disable-clean, -dc Не чистить исходники после сборки.
--distclean, -cd Чистить исходники перед сборкой. make distclean
--disable-distclean, -dd Не чистить исходники перед сборкой.
--remove, -r Удалять директорию с исходниками после сборки
--disable-remove, -dr Не удалять директорию с исходниками после сборки

--dkms-install, -di Устанавливать DKMS модули
--disable-dkms-install, -ddi Не устанавливать DKMS модули
--dkms-uninstall, -du Деинсталлировать DKMS модули перед их установкой
--disable-dkms-uninstall, -ddu Не деинсталлировать DKMS модули перед их установкой

Список параметров которые помогают отлавливать ошибки на разных этапах и продолжить вручную:

--stop-download, -sd Стоп посл загрузки файла
--stop-extract, -se Стоп после распаковки архива с исходниками
--stop-patch, -sp Стоп после применения патчей ядрей
--stop-config, -sc Стоп после конфигуратора ядра
--stop-build, -sb Стоп после сборки ядра
--stop-install, -si Стоп после установки нового ядра и модулей




Как можно понять из статьи сборка ядра с LLVM и Clang относительно простая. И самое главное можно автоматизировать сборку и установку ядра, и в дальнейшем не тратить много времени на сборку новых ядер.

Всем кто дочитал до конца, спасибо! Комментарии и замечания приветствуются!


Подробнее..

Перевод Sparkplug неоптимизирующий компилятор JavaScript в подробностях

09.06.2021 20:16:20 | Автор: admin

Создать компилятор JS с высокой производительностью означает сделать больше, чем разработать сильно оптимизированный компилятор, например TurboFan, особенно это касается коротких сессий, к примеру, загрузки сайта или инструментов командной строки, когда большая часть работы выполняется до того, как оптимизирующий компилятор получит хотя бы шанс на оптимизацию, не говоря уже о том, чтобы располагать временем на оптимизацию. Как решить эту проблему? К старту курса о Frontend-разработке делимся переводом статьи о Sparkplug свече зажигания под капотом Chrome 91.


Вот почему с 2016 года мы ушли от синтетических бенчмарков, таких как Octane, к измерению реальной производительности и почему старательно работали над производительностью JS вне оптимизирующих компиляторов. Для нас это означало работу над парсером, стримингом [этой поясняющей ссылки в оригинале нет], объектной моделью, конкурентностью, кешированием скомпилированного кода...

Впрочем, повернувшись лицом к улучшению производительности фактического, начального выполнения JS, мы столкнулись с ограничениями процесса оптимизации интерпретатора. Интерпретатор V8 сам по себе быстрый и сильно оптимизированный, но интерпретаторам как таковым свойственны накладные расходы, избавиться от которых мы не можем: например на декодирование байт-кода или диспетчеризацию неотъемлемые части функциональности интерпретатора.

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

Выход из положения Sparkplug: новый неоптимизирующий компилятор JavaScript, который мы выпустили вместе с V8 9.1, он работает между интерпретатором Ignition и компилятором TurboFan.

Новый процесс компиляцииНовый процесс компиляции

Быстрый компилятор

Sparkplug создан компилировать быстро. Очень быстро. Настолько, что мы всегда можем компилировать, когда захотим, повышая уровень кода SparkPlug намного агрессивнее кода TurboFan, [подробнее здесь, этой ссылки в оригинале нет].

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

Второй трюк в отличие от большинства компиляторов Sparkplug не генерирует промежуточное представление (IR). Вместо этого Sparkplug компилирует прямо в машинный код за один линейный проход по байт-коду и выдаёт соответствующий выполнению байт-кода машинный код. На самом деле компилятор это switch внутри цикла for, который занят диспетчеризацией, чтобы за каждой инструкцией закрепить генерирующие машинный код функции.

// The Sparkplug compiler (abridged).for (; !iterator.done(); iterator.Advance()) {  VisitSingleBytecode();}

Недочёт промежуточного представления (IR) заключается в том, что компилятор ограничен возможностями оптимизации. Поскольку промежуточной архитектурно независимой стадии нет, этот факт также означает, что всю реализацию нужно переносить на каждую поддерживаемую архитектуру отдельно. Но, оказывается, ни то, ни другое не является проблемой: портировать код довольно легко, вместе с тем Sparkplug не нужно выполнять тяжёлую оптимизацию.

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

Совместимые с интерпретатором фреймы

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

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

Стековый фрейм с указателями стека и фреймаСтековый фрейм с указателями стека и фрейма

Сейчас около половины читателей закричит: "Диаграмма не имеет смысла, стек направлен в другую сторону!" Ничего страшного, я сделал кнопку: думаю, стек направлен вниз.

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

Стековые фреймы для нескольких вызововСтековые фреймы для нескольких вызовов

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

Это основной макет стека для всех типов функций; затем идёт соглашение о передаче аргументов и о том, как в своём фрейме функция хранит значения. В V8 мы имеем соглашение для фреймов JS, что аргументы до вызова функции (включая приёмник) добавляются на стек в обратном порядке, а также о том, что первые несколько слотов стека это вызываемая функция, контекст, с которым она вызывается, и количество передаваемых аргументов. Вот наш стандартный фрейм JS.

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

В случае Ignition соглашение становится более явным. Ignition интерпретатор на основе регистров, это означает, что есть виртуальные регистры (не путайте их с машинными!), которые хранят текущее состояние интерпретатора. включая локальные переменные (объявления var, let, const) и временные значения. Эти регистры содержатся в стековом фрейме интерпретатора, вместе с указателем на выполняемый массив байт-кода и смещением текущего байт-кода в массиве.

Sparkplug намеренно создаёт и поддерживает соответствующий фрейму интерпретатора макет фрейма. Всякий раз, когда интерпретатор сохраняет значение регистра, SparkPlug также сохраняет его. Делает он это по нескольким причинам:

  1. Это упрощает компиляцию Sparkplug; новый компилятор может просто отражать поведение интерпретатора без необходимости сохранять какое-либо отображение из регистров интерпретатора в состояние Sparkplug.

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

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

  4. Тривиальной становится и замена на стеке (OSR). Замена на стеке это когда выполняемая функция заменяется в процессе выполнения; сейчас это происходит, когда интерпретированная функция находится в горячем цикле (в это время она поднимается до оптимизированного кода этого цикла) и где оптимизированный код деоптимизируется (когда он опускается и продолжает выполнение функции в интерпретаторе), любая работающая в интерпретаторе логика замены на стеке будет работать и для Sparkplug. Даже лучше: мы можем взаимозаменять код интерпретатора и SparkPlug почти без накладных расходов на переход фреймов.

Мы немного изменили стековый фрейм интерпретатора: во время выполнения кода Sparkplug не поддерживается актуальная позиция смещения. Вместо этого мы храним двустороннее отображение из диапазона адресов кода Sparkplug к соответствующему смещению. Для декодирования такое сопоставление относительно просто, поскольку код Sparklpug получается линейным проходом через байт-код. Всякий раз, когда стековый фрейм хочет узнать "смещение байт-кода" для фрейма Sparkplug, мы смотрим на текущую выполняемую инструкцию в отображении и возвращаем связанное смещение байт-кода. Аналогично, когда Sparkplug нужно узнать OSR из интерпретатора, мы смотрим на байт-код в смещении и перемещаемся к соответствующей инструкции Sparkplug.

Вы можете заметить, что теперь у нас есть неиспользуемый слот фрейма, где должно быть смещение байт-кода; избавиться от него мы не можем, поскольку хотим сохранить оставшуюся часть стека неизменной. Мы перепрофилируем этот слот стека, чтобы вместо него кешировать "вектор обратной связи" для текущей выполняющейся функции; это вектор, хранящий данные о форме объекта, и он должен быть загружен для большинства операций. Всё, что нам нужно делать, соблюдать осторожность с OSR, чтобы гарантировать, что мы подставляем либо правильное смещение байт-кода, либо правильный вектор обратной связи для этого слота. В итоге стековый фрейм Sparkplug выглядит так:

Полагаемся на встроенный код

На самом деле Sparkplug генерирует очень мало собственного кода. У JS сложная семантика, так что для выполнения даже самых простых операций требуется много кода. Принудительная повторная генерация такого кода при каждой компиляции оказалась бы плохим решениям по нескольким причинам:

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

  2. Этот подход увеличил бы потребление памяти кодом Sparkplug.

  3. Пришлось бы переписывать кодогенерацию для большого количества функциональности JS, что, вероятно, означало бы и больше ошибок, и большую поверхность атаки.

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

Вы можете подумать: "Ну и какой тогда смысл во всём этом? Разве Sparkplug не выполняет ту же работу, что и интерпретатор?" и во многом будете правы. Во многих отношениях Sparkplug является "просто" сериализацией выполнения интерпретатора, вызывая те же встроенные двоичные сниппеты и поддерживая тот же стековый фрейм. Тем не менее оно того стоит, потому что Sparkplug удаляет (или, точнее, предварительно компилирует) те самые неустранимые накладные расходы интерпретатора, такие как декодирование операндов и диспетчеризация следующего байт-кода.

Оказалось, что интерпретация эффективнее множества оптимизаций уровня центрального процессора: статические операнды динамически читаются из памяти интерпретатором, вынуждая процессор делать предположения о том, какими могут быть значения. Диспетчеризация к следующей инструкции байт-кода для сохранения производительности требует успешного прогнозирования ветви выполнения, и, даже если предположения и прогнозы верны, по-прежнему нужно выполнять декодирование и диспетчеризацию кода, а также занимать драгоценное пространство различных буферов и кешей. ЦП сам по себе эффективный интерпретатор, хотя он применяется к машинному коду. С этой точки зрения Sparkplug транспилятор из байт-кода Ignition в байт-код центрального процессора, перемещающий выполнение в "эмуляторе" к "нативному" выполнению.

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

Так как же Sparkplug работает на практике? Мы выполнили несколько бенчмарков Chrome на наших ботах для замера производительности со Sparkplug и без него. Спойлер: мы очень довольны.

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

Speedometer

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

Среднее улучшение показателей спидометра с помощью Sparkplug по нескольким ботам производительности. Полосы на диаграмме ошибок указывают на диапазон между квартилямиСреднее улучшение показателей спидометра с помощью Sparkplug по нескольким ботам производительности. Полосы на диаграмме ошибок указывают на диапазон между квартилями

Обзор бенчмарка

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

На этих тестах мы решили посмотреть метрику V8 Main-Thread Thread, измеряющую общее проведённое в V8 время (включая компиляцию и выполнение), в основном потоке (то есть исключая стриминговый парсинг или фоновую оптимизацию). Это лучший способ увидеть, насколько оправдан Sparkplug, без учёта других источников шума бенчмарка.

Результаты различны и сильно зависят от машины и веб-сайта, но в целом выглядят прекрасно: видно улучшение около 515 %.

Медианное улучшение времени работы V8 в основном потоке на наших бенчмарках для просмотра с 10 повторениями. Полосы на диаграмме указывают на диапазон между квартилямиМедианное улучшение времени работы V8 в основном потоке на наших бенчмарках для просмотра с 10 повторениями. Полосы на диаграмме указывают на диапазон между квартилями

Таким образом, V8 имеет новый сверхбыстрый неоптимизирующий компилятор, повышающий производительность V8 в реальных бенчмарках на 515 %. Он уже доступен в V8 v9.1 (укажите опцию --sparkplug), и мы выпустим его вместе с Chrome 91.

Вот что важно заметить: выделившись в отдельную область разработки, фронтенд не ограничивается одним только JavaScript, например есть множество тонкостей в том, каким образом браузер работает с CSS. Вместе с тем оптимизации на уровне браузера не означают, что можно больше не писать аккуратный, компактный и элегантный код. Скорее они означают, что теперь разработчики будут свободнее чувствовать себя, когда захотят написать сложную или тяжёлую функциональность. Если фронтенд вам интересен, вы можете обратить внимание на наш курс Frontend-разработчик, где получите комплексную подготовку, чтобы в дальнейшем писать и поддерживать приложения различного масштаба и уровня сложности.

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Чему равно выражение -33u3 на С? Не угадаете. Ответ -4. Приглашаю на небольшое расследование

04.05.2021 18:23:50 | Автор: admin

Вот пример для проверки:

#include <iostream>int main(){    std::cout << "-3/3u*3 = " << int(-3/3u*3) << "\n";}

Посмотреть результат можно тут.

Или попробуйте поиграться с этим примером здесь или здесь.

Вообще-то мне не удалось найти хоть какой-то компилятор С++, который бы выдавал результат отличный от -4. Даже старый GCC-4.1.2, clang-3.0.0 или Borland C 1992 года. Так же заметил, что результат одинаковый и для константы, вычисляемой в момент компиляции и для времени выполнения.

Предлагаю внимательно рассмотреть результат выражения -3/3u*3.

Если убрать приведение к типу intв примере выше, то получим 4294967292 или 0xFFFFFFFС(-4). Получается, что компилятор на самом деле считает результат беззнаковым и равным 4294967292. До этого момента я был свято уверен, что если в выражении используется знаковый тип, то и результат будет знаковым. Логично же это.

Если посмотреть откуда берется -4 вместо -3, посмотрим внимательней на ассемблерный код примера, например здесь.

Пример изменю, чтобы результат вычислялся не в момент компиляции, а в момент выполнения:

int main(){    volatile unsigned B = 3;    int A = -3/B*3;}

Для x86-64 clang 12.0.0 видим, что используется беззнаковое деление, хотя числитель откровенно отрицательное -3:

        mov     dword ptr [rbp - 4], 3    // B = 3        mov     ecx, dword ptr [rbp - 4]        mov     eax, 4294967293        xor     edx, edx        div     ecx                       // беззнаковое деление !!        imul    eax, eax, 3               // знаковое умножение        mov     dword ptr [rbp - 8], eax

Для x64 msvc v19.28 тот же подход к делению:

        mov     DWORD PTR B$[rsp], 3      // B = 3        mov     eax, DWORD PTR B$[rsp]        mov     DWORD PTR tv64[rsp], eax        xor     edx, edx        mov     eax, -3                             ; fffffffdH        mov     ecx, DWORD PTR tv64[rsp]        div     ecx        imul    eax, eax, 3        mov     DWORD PTR A$[rsp], eax

Получается, что для деления беззнакового числа на знаковое используется БЕЗЗНАКОВАЯ операция деления процессора div. Кстати, следующая команда процессора, это правильное знаковое умножение imul. Ну явный баг компилятора. Банальная логика же подсказывает, что знаковый тип выиграет в приведении типа результата выражения если оба знаковый и беззнаковый типы используются в выражении. И для знакового деления требуется знаковая команда деления процессора idiv, чтоб получить правильный результат со знаком.

Проблема еще и в том, что число 4294967293 не делится на 3 без остатка:4294967293 = 1431655764 * 3 + 1и при умножении 1431655764 обратно на 3, получаем 4294967292 или -4. Так что прикинуться веником и считать, что 4294967293 это то же -3, только вид сбоку, для операции деления не прокатит.

Двоично-дополнительное придставление отрицательных чисел.

Благодаря представлению чисел в двоично-дополнительном виде, операции сложения или вычитания над знаковыми и без-знаковыми числами выполняются одной и той же командой процессора (add для сложения и sub для вычитания). Процессор складывает (или вычитает) только знаковое со знаковым или только без-знаковое с без-знаковым. И для обоих этих операций используется одна команда add (или sub) и побитово результат будет одинаковый (если бы кто-то решил сделать раздельные операции сложения для знаковых и без-знаковых типов). Различие только во флагах процессора. Так что считать знаковое без-знаковым и складывать их оба как без-знаковых корректно и результат будет побитово правильным в обоих случаях. Но для деления и умножения этот подход в корне неправильный. Процессор внутри использует только без-знаковые числа для деления и умножения и результат приводит обратно в знаковое с правильным признаком знака. И для этого процессор использует разные команды для знакового (idiv) и без-знакового деления (div) и так же и для умножения (imul и соответственно mul).

Я когда обнаружил, что используется без-знаковое деление, решил, что это бага компилятора. Протестировал много компиляторов: msvc, gcc, clang. Все показали такой же результат, даже древние трудяги. Но мне довольно быстро подсказали, что это поведение описано и закреплено в самом стандарте.

Действительно, стандарт говорит об этом прямо:

Otherwise, if the unsigned operand's conversion rank is greater or equal to the conversion rank of" "the signed operand, the signed operand is converted to the unsigned operand's type.

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

Вот где оказывается собака зарыта: "the signed operand is converted to the unsigned operand's type"!! Ну почему, почему, Карл!! Логичнее наоборот: "the unsigned operand is converted to the signed operand's type", разумеется при соблюдении ранга преобразования. Ну вот как -3 представить без-знаковым числом?? Наоборот кстати можно.

Интересная получается сегрегация по знаковому признаку!

Заметил, что это правило почему-то работает только для операции деления, а операция умножения вычисляется правильно.

Проверим на ассемблере здесь этот пример:

int main(){    volatile unsigned B = 3;    int C = -3*B;}
Вот ассемблерный код:

mov dword ptr [rbp - 4], 3 mov eax, dword ptr [rbp - 4] imul eax, eax, 4294967293 mov dword ptr [rbp - 8], eax

Стандарт ничего не говорит о неприменимости этого правила для операции умножения. И деление и умножение должны быть БЕЗЗНАКОВМИ.

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

Ага! Наивный!

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

Хоть это исследование и было больше года назад, я до сих пор под впечатлением от многих вещей в этой истории:

  • Как я не натыкался на это раньше? Не один десяток лет интенсивно кодирую на С и С++ с погружением в ассемблер, но только сейчас споткнулся на неё. Хотя может и натыкался ранее, но не мог поверить что причина именно в этом.

  • Ассемблер я выучил раньше С и поэтому всегда считал С удобной заменой Ассемблера. Но правила работы процессоров неизменны и даже языки высокого уровня им следуют.

Интересно, что это правило всё же не работает и для операций с указателями. С ними нет такого поведения. Видать понимают разработчики компиляторов, что иначе рухнет очень многое. Посмотрите этот пример здесь:

int main(){    const unsigned a[] = {3,4,5,6,7};    unsigned p = (&a[0] - &a[3])/3u*3;    // -3    unsigned b = -3/3u*3;   // -4}

Хоть я и понимаю, что могу ошибаться в логике работы этого мира, но задумайтесь, в следующий раз садясь в современный, нашпигованный вычислительной логикой самолёт (или автомобиль), а не сработает ли вдруг не оттестированный кусок кода в какой-то редкой нештатной ситуации, и не выдаст ли он -4 вместо элементарных -3, и не постигнет ли его участь подобная Boeing 737 MAX?

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

Ошибка в команде просессора FDIV у Интела

Помните, в начале 2000-х была выявлена ошибка с вычислением в команде FDIV у Интела. Там было различие в 5 знаке после запятой в какой-то операции деления. Какой был шум тогда!!
Но исправили оперативно и быстро. В компиляторы добавили условный флаг для обхода этой команды. Интел срочно исправил логику в кристалле и выпустил новые чипы.

И это всего лишь 5-й знак после запятой! Многие его даже и не заметили, подумаешь, мелочь какая! А тут -4 вместо -3 и считаем знаковое без-знаковым и вместо -3 имеем еще и 4294967292! И тишина в ответ! И в этой тишине тихо падают Боинги.

Предвижу возражение, что есть рекомендация использовать преимущественно без-знаковый тип. Понимаю, с таким правилом Стандарта по другому никак. Но как -4 в беззнаковое перевести, и чтоб всё остальное работало? И почему на других языках и всех процессорах не надо этими танцами с бубном заниматься, а в С++ надо? И что делать с константными выражениями, вычисляемыми компилятором? Вместо элегантных выражений, в которых мы уверены, надо записывать их с оглядкой, чтоб у компилятора небыло изжоги!

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

Как хорошую подсказку познавательно добавить предупреждение в компилятор когда он применяет это правило из Стандарта:"Signed value is intentionally converted to unsigned value. Sorry for crashing one more airplane. Have a nice flight!" Вот удивимся тогда, как мало мы тестируем и как много нам открытий чудных приносит компилятор друг.

Можно еще исправить Стандарт, ограничив правило только операциями сложения и вычитания. Как компромис. Но это крайне маловероятно в этой Вселенной. Да и Боингов еще много летает.

Представьте студента (С) на экзамене у преподавателя по информатике (П) в ВУЗе.

- П: Хорошо, последний вопрос на 5. Можно ли привести знаковое число к беззнаковому типу?
- С: Хе, можно. НУЖНО! Обязательно НУЖНО! Ставте 5, я пойду.
- П: Как НУЖНО?? О_О. Подумайте. Как можно представить, например, -4 беззнаковым числом? - С: Чего тут думать! Стандарт С++ сказал, что НУЖНО, значит НУЖНО и точка. А то что -4 станет беззнаковым очень большим числом - это уже ни печалька Стандарта, ни моя. - П: Подумайте еще раз. Вы на экзамене по информатике и вас спрашивают о базовых вещах, которые общие для всех языков программирования и процессоров, а не только для С++. - С: Чего вы пристали к мне со своими языками и процессорами. У меня в билете вопрос про С++, вот я про С++ и отвечаю. А вы про какой то там Ассемблер, базовые вещи, языки программирования! Стандарт С++ сказал, компилятор сделал, я ответил! У вас есть вопросы к Стандарту про базовые вещи, вот ему их и задавайте, а я ответил правильно! - П: Да уж. Подстава конкретная.

Подробнее..

Перевод Rust 1.51.0 const generics MVP, новый распознаватель функциональности Cargo

26.03.2021 20:17:43 | Автор: admin

Команда Rust рада сообщить о выпуске новой версии 1.51.0. Rust это язык программирования, позволяющий каждому создавать надёжное и эффективное программное обеспечение.


Если вы установили предыдущую версию Rust средствами rustup, то для обновления до версии 1.51.0 вам достаточно выполнить следующую команду:


rustup update stable

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


Что было стабилизировано в 1.51.0


Этот выпуск представляет одно из наиболее крупных дополнений языка Rust и Cargo за долгое время, включающее в себя стабилизацию константных обобщений в минимально полезном варианте и новый распознаватель функциональности в Cargo. Давайте посмотрим подробнее!


Константные обобщения (Const Generics MVP)


До этого выпуска Rust позволял параметризовать типы по времени жизни или по типам. Например, если бы мы хотели получить struct, которая является обобщённой для массива, мы бы написали следующее:


struct FixedArray<T> {              // ^^^ Определение обобщённого типа.    list: [T; 32]        // ^ Где мы использовали его.}

Если затем мы используем FixedArray<u8>, компилятор создаст мономорфизированную версию FixedArray, которая выглядит так:


struct FixedArray<u8> {    list: [u8; 32]}

Этот полезный функционал позволяет писать повторно используемый код без дополнительных затрат во время выполнения. Однако до этого выпуска у нас не было возможности легко объединять значения таких типов. Это наиболее заметно в массивах, где длина указывается в определении типа ([T; N]). Теперь в версии 1.51.0 вы можете писать код, который будет обобщённым для значений любого числа, типа bool или char! (Использование значений struct и enum по-прежнему не стабилизировано.)


Это изменение позволяет создать массив, обобщённый по типу и длине. Давайте посмотрим на пример определения и то, как его можно использовать.


struct Array<T, const LENGTH: usize> {    //          ^^^^^^^^^^^^^^^^^^^ Определение константного обобщения.    list: [T; LENGTH]    //        ^^^^^^ Мы использовали его здесь.}

Теперь если мы используем Array<u8, 32>, компилятор создаст мономорфизированную версию Array, которая выглядит так:


struct Array<u8, 32> {    list: [u8; 32]}

Константные обобщения добавляют важный новый инструмент для разработчиков библиотек, чтобы создавать новые, мощные и безопасных API во время компиляции. Если вы хотите узнать больше о константных обобщениях, можете почитать статью в блоге Const Generics MVP Hits Beta для получения дополнительной информации об этой функции и её текущих ограничениях. Нам не терпится увидеть, какие новые библиотеки и API вы создадите!


Стабилизация array::IntoIter


Как часть стабилизации константных обобщений, мы также стабилизировали использующее их новое API std::array::IntoIter. IntoIter позволяет вам создать поверх массива итератор по значению. Ранее не было удобного способа итерироваться по самим значениям, только по ссылкам.


fn main() {  let array = [1, 2, 3, 4, 5];  // Раньше  for item in array.iter().copied() {      println!("{}", item);  }  // Теперь  for item in std::array::IntoIter::new(array) {      println!("{}", item);  }}

Обратите внимание, что это было добавлено вместо .into_iter() как отдельный метод, так как сейчас оно ломает текущее соглашение о том, что .into_iter() относится к срезам по ссылочному итератору. Мы изучаем возможности в будущем сделать такой код более эргономичным.


Новый распознаватель функциональности Cargo


Управление зависимостями сложная задача, и одна из самых сложных её частей выбор версии зависимости, когда от неё зависят два разных пакета. Здесь учитывается не только номер версии, но и то, какая функциональность была включена или выключена для пакета. По умолчанию Cargo объединяет функциональные флаги (features) для одного пакета, если он встречается в графе зависимостей несколько раз.


Например, у вас есть зависимость foo с функциональными флагами A и B, которые используются пакетами bar and baz, но bar зависит от foo+A, а baz от foo+B. Cargo объединит оба флага и соберёт foo как foo+AB. Выгода здесь в том, что foo будет собран только один раз и далее будет использован и для bar, и для baz.


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


Общим примером этого из экосистемы может служить опциональная функциональность std во многих #![no_std] пакетах, которая позволяет этим пакетам предоставить дополнительную функциональность, если она включена. Теперь представим, что вы хотите использовать #![no_std] версию foo в вашей #![no_std] программе и использовать foo во время сборки в build.rs. Так как во время сборки вы зависите от foo+std, то и ваша программа тоже зависит от foo+std, а значит более не может быть скомпилирована, так как std не доступна для вашей целевой платформы.


Это была давняя проблема в Cargo, и с этим выпуском появилась новая опция resolver в вашем Cargo.toml, где вы можете установить resolver="2", чтобы попробовать новый подход к разрешению функциональных флагов. Вы можете ознакомиться с RFC 2957 для получения подробного описания поведения, которое можно резюмировать следующим образом.


  • Dev dependencies когда пакет используется совместно как обычная зависимость и dev, возможности dev-зависимости включаются только в том случае, если текущая сборка включает dev-зависимости.
  • Host Dependencies когда пакет совместно используется как обычная зависимость и зависимость сборки или процедурный макрос, features для нормальной зависимости сохраняются независимо от зависимости сборки или процедурного макроса.
  • Target dependencies когда у пакета включены зависимые от платформы features, и он присутствует в графе сборки несколько раз, будут включены только features, подходящие текущей платформе сборки.

Хотя это может привести к компиляции некоторых пакетов более одного раза, это должно обеспечить гораздо более интуитивный опыт разработки при использовании функций с Cargo. Если вы хотите узнать больше, вы также можете прочитать раздел Feature Resolver в Cargo Book для получения дополнительной информации. Мы хотели бы поблагодарить команду Cargo и всех участников за их тяжёлую работу по разработке и внедрению нового механизма!


[package]resolver = "2"# Или если вы используете workspace[workspace]resolver = "2"

Разделение отладочной информации


Хоть это и нечасто освещается в релизах, команда Rust постоянно работает над сокращением времени компиляции. В этом выпуске вносится самое крупное улучшение за долгое время для Rust на macOS. Отладочная информация исходного кода содержится в собранном бинарнике, и за счет этого программа может дать больше информации о том, что происходит во время исполнения. Раньше в macOS отладочная информация собиралась в единую директорию .dSYM при помощи утилиты dsymutil, что могло занимать много времени и дискового пространства.


Сбор всей отладочной информации в эту директорию помогал найти её во время выполнения, особенно если бинарник перемещался. Однако у такого решения есть и обратная сторона: если вы сделали небольшое изменение в вашей программе, то dsymutil необходимо запустить над всем собранным бинарником, чтобы собрать директорию .dSYM. Иногда это могло сильно увеличить время сборки, особенно для крупных проектов, поскольку надо перебирать все зависимости, но это важный шаг, без которого стандартная библиотека Rust не знает, как загружать отладочную информацию на macOS.


Недавно обратная трассировка в Rust была переключена на другой бэкенд, который поддерживает загрузку отладочной информации без запуска dsymutil. Она была стабилизирована, чтобы убрать запуск dsymutil. Это может значительно ускорить сборки, которые включают отладочную информацию, и уменьшить размер занимаемого пространства. Мы не проводили обширных тестов, но у нас есть множество отчётов сборок, которые стали с таким поведением на macOS намного быстрее.


Вы можете включить новое поведение, установив флаг -Csplit-debuginfo=unpacked при запуске rustc или задав опцию split-debuginfo в unpacked раздела [profile] в Cargo. С опцией "unpacked" rustc будет оставлять объектные файлы (.o) в директории сборки вместо их удаления и пропустит запуск dsymutil. Поддержка бэктрейсов Rust достаточно умна, чтобы понять, как найти эти .o файлы. Такие инструменты, как lldb, также знают, как это делается. Это должно работать до тех пор, пока вам не понадобится переместить бинарники в другое место и сохранить отладочную информацию.


[profile.dev]split-debuginfo = "unpacked"

Стабилизированные API


Итого: в этом выпуске было стабилизировано 18 новых методов для разных типов, например slice и Peekable. Одним из примечательных дополнений является стабилизация ptr::addr_of! и ptr::addr_of_mut!, которая позволяет вам создавать сырые указатели для полей без выравнивания. Ранее это было невозможно, так как Rust требовал, чтобы &/&mut были выровнены и указывали на инициализированные данные. Из-за этого преобразование &addr as *const _ приводило к неопределённому поведению, так как &addr должно быть выровнено. Теперь эти два макроса позволяют вам безопасно создать невыровненные указатели.


use std::ptr;#[repr(packed)]struct Packed {    f1: u8,    f2: u16,}let packed = Packed { f1: 1, f2: 2 };// `&packed.f2` будет создана ссылка на невыровненную память, таким образом это неопределённое поведение!let raw_f2 = ptr::addr_of!(packed.f2);assert_eq!(unsafe { raw_f2.read_unaligned() }, 2);

Следующие методы были стабилизированы:



Другие изменения


Синтаксис, пакетный менеджер Cargo и анализатор Clippy также претерпели некоторые изменения.


Участники 1.51.0


Множество людей собрались вместе, чтобы создать Rust 1.51.0. Мы не смогли бы сделать это без всех вас. Спасибо!


От переводчиков


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


Данную статью совместными усилиями перевели andreevlex, TelegaOvoshey, blandger, nlinker и funkill.

Подробнее..

Какая идеальная цель развития у языков программирования?

07.04.2021 16:22:59 | Автор: admin


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

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

Об этой проблеме меня заставила задуматься первоапрельская статья Доказательное программирование

Понятно, что дата публикации статьи говорит сама за себя. Тем не менее, новые стандарты С++, постоянно выходящие спецификации Java или новый синтаксис у PHP 8, невольно заставляют задуматься, а в нужную ли сторону идет развитие языков программирования? Ведь большинство нововведений добавляют сложность в основной рабочий инструмент и решая одни проблемы, неявно добавляя множество других.

А что должно быть в конце прогресса у развития такой дисциплины как программирование? Или хотя бы у одного конкретного языка? Ради достижения какой конечной идеальной цели разрабатываются новые стандарты языков программирования?

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

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

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

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

Это делается, в том числе и для того, чтобы сложность одной выполняемой операции не была запредельной для исполнителя. Но как это можно сделать при разработке программного обеспечения?

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

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

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

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

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

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

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

Может быть именно поэтому, невозможно найти серебряную пулю, которая бы повышала производительность труда одного программиста? Ведь внимание и возможности человека не безграничны. И любые новшества и улучшения рабочего инструмента программиста вынужденно толкают всю IT индустрию на экстенсивный путь развития.

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

Перевод Rust теперь и на платформе Android

10.04.2021 14:17:23 | Автор: admin

Корректность кода на платформе Android является наиважнейшим аспектом в контексте безопасности, стабильности и качества каждого релиза Android. По-прежнему сложнее всего вытравливаются ошибки, связанные с безопасностью памяти и попадающиеся в коде на С и C++. Google вкладывает огромные усилия и ресурсы в обнаружение, устранение багов такого рода, а также в уменьшение вреда от них, старается, чтобы багов в релизы Android проникало как можно меньше. Тем не менее, несмотря на все эти меры, ошибки, связанные с безопасностью памяти, остаются основным источником проблем со стабильностью. На их долю неизменно приходится ~70% наиболее серьезных уязвимостей Android.

Наряду с текущимиипланируемымимероприятиями по улучшению выявления багов, связанных с памятью, Google также наращивает усилия по их предотвращению. Языки, обеспечивающие безопасность памяти наиболее эффективные и выгодные средства для решения этой задачи. Теперь в рамках проекта Android Open Source Project (AOSP) наряду с языками Java и Kotlin, отличающимися безопасностью памяти, поддерживается и язык Rust, предназначенный для разработки операционной системы как таковой.

Системное программирование

Управляемые языки, в частности, Java и Kotlin, лучше всего подходят для разработки приложений под Android. Эти языки проектировались в расчете на удобство использования, портируемость и безопасность. Среда исполнения Android (ART)управляет памятью так, как указал разработчик. В операционной системе Android широко используется Java, что фактически защищает большие участки платформы Android от багов, связанных с памятью. К сожалению, на низких уровнях ОС Android Java и Kotlin бессильны.

На низких уровнях ОС нужны языки для системного программирования, такие как C, C++ и Rust. При проектировании этих языков приоритет отдавался контролируемости и предсказуемости. Они обеспечивают доступ к низкоуровневым ресурсам системы и железу. Они обходятся минимальными ресурсами, прогнозировать их производительность также сравнительно просто.

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

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

Пределы работы в песочнице

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

В Android это означает, что, если код написан на C/C++ и разбирает потенциально небезопасный ввод, то он должен содержаться в жестко ограниченной песочнице без привилегий. Тогда как следование правилу двух хорошо помогает снижать тяжесть и повышать доступность уязвимостей, связанных с безопасностью, оно сопряжено с некоторыми ограничениями. Работа в песочнице дорогое удовольствие; для ее обеспечения требуются новые процессы, которыесопряжены с дополнительными накладными расходами и провоцируют задержки, связанные с межпроцессной коммуникацией и дополнительным расходом памяти. Работа в песочнице не позволяет полностью исключить уязвимости в коде, а эффективность такой работы снижается при высокой плотности багов, позволяющей злоумышленникам сцеплять вместе множество уязвимостей сразу.

Язык, обеспечивающий безопасность памяти, такой, как Rust, помогает преодолеть эти ограничения двумя способами:

  1. Снижает плотность багов в нашем коде, тем самым повышая эффективность применяемой песочницы.

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

Что же насчет всего имеющегося C++?

Разумеется, если мы введем новый язык программирования, это никак не поможет нам с исправлением уже имеющихся багов в имеющемся коде на C/C++.

Вышеприведенный анализ возраста багов, связанных с безопасностью памяти (отсчитывается с момента их появления) позволяет судить, почему команда Android делает акцент на новых разработках, а не на переписывании зрелого кода на C/C++. Большинство багов возникает в новом или недавно измененном коде, причем, возраст около 50% багов составляет менее года.

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

Ограничения находимости багов

Выявление багов при помощи надежного тестирования,очистки ифаззингакритически важно для повышения качества и корректности любых программ, в том числе, написанных на Rust. Ключевое ограничение, с которым в данном случае сталкиваются наиболее эффективные приемы проверки безопасности памяти в том, что состояние ошибки должно быть спровоцировано в инструментированном коде, и только так его удастся заметить. Даже в коде, отлично покрытом тестами и фаззингом, множество багов проскальзывает в базу именно потому, что их не удалось спровоцировать.

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

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

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

Предотвращение прежде всего

В Rust модернизируется ряд языковых аспектов, что позволяет улучшить корректность кода:

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

  • Конкурентность данных предотвращает гонку данных. Учитывая, насколько легко при этом становится писать эффективный потокобезопасный код, Rust обрел слоганБезбоязненная Конкурентность.

  • Более выразительная система типов помогает предотвратить логические ошибки при программировании (напр., обертки нового типа, варианты перечислений с содержимым).

  • Ссылки и переменные по умолчанию являются неизменяемыми что помогает разработчику следовать безопасному принципу наименьших привилегий. Программист помечает ссылку или переменную как изменяемые, только если в самом деле намерен сделать их таковыми. Притом, что в C++ есть const, эта возможность обычно используется нечасто и несогласованно. Напротив, компилятор Rust помогает избегать случайных аннотаций об изменяемости, так как выдает предупреждения об изменяемых значениях, которые никогда не меняются.

  • Улучшенная обработка ошибок в стандартных библиотеках потенциально провальные вызовы обертываются в Result, и поэтому компилятор требует от пользователя проверять возможность провала даже для функций, не возвращающих необходимого значения. Это позволяет защититься от таких багов как уязвимостьRage Against the Cage, возникающая из-за необработанной ошибки. Обеспечивая легкое просачивание ошибок при помощи оператора ? и оптимизируя Result с расчетом на низкие издержки, Rust стимулирует пользователей писать все потенциально провальные функции в одном и том же стиле, благодаря чему все они получают ту же защиту.

  • Инициализация требует, чтобы все переменные инициализировались перед началом использования. Исторически сложилось, что неинициализированные уязвимости памяти были в Android причиной 3-5% уязвимостей, связанных с безопасностью. В Android 11, чтобы сгладить эту проблему, стала применятьсяавтоматическая инициализация памяти на C/C++. Однако, инициализация в ноль не всегда безопасна, особенно для таких штук, как возвращаемые значения, и в этой области может стать новым источником неправильной обработки ошибок. Rust требует, чтобы любая переменная перед использованием инициализировалась в полноценный член своего типа. Тем самым избегается проблема непреднамеренной инициализации небезопасного значения. Подобно Clang в C/C++, компилятор Rust знает о требовании инициализации и позволяет избежать потенциальных проблем производительности, связанных с двойной инициализацией.

  • Более безопасная обработка целых чисел Очистка во избежание включена в отладочных сборках Rust по умолчанию, что стимулирует программистов указывать wrapping_add, если действительно предполагается допустить переполнение при расчетах, или saturating_add если не предполагается. Очистка при переполнении в дальнейшем должна быть включена для всех сборок Android. Кроме того, при всех преобразованиях целочисленных типов применяются явные приведения: разработчик не может сделать случайного приведения в процессе вызова функции при присваивании значения переменной или при попытке выполнять арифметические операции с другими типами.

Что дальше

Введение нового языка на платформу Android серьезное предприятие. Существуют такие связки инструментов и зависимости, которые необходимо поддерживать, тестовая инфраструктура и оснащение, которые потребуется обновить. Также придется дополнительно обучить разработчиков. Это проект не на один год. Следите за обновлениями в блоге Google.

Подробнее..

Передача и вызов лямбд на сервере и отказаться от dockerdeploy

26.04.2021 18:04:48 | Автор: admin

При разработке клиент-серверного приложения, у меня всегда появляется вопрос, а как я его буду разворачивать на сервере, упаковать его в jar/war/docker после написания кода, а потом еще надо передать на сервер, и еще много сделать телодвижений чтоб просто засунуть кусок кода на сервере.

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

Но у меня появилась мысль как сделать этот процесс проще, и у меня что-то получилось.

1 | var query = TcpQuery2 |    .create(IEnv.class).host("myserver.com").port(9988)3 |      .build();4 |5 |  query.apply( 6 |      env -> env.processes().stream().filter(7 |          p -> p.getName().contains("java")8 |      )9 |      .collect(Collectors.toList())10|  ).forEach(System.out::println);

Вот код на java,

  • Строки с 1 по 5 включительно и 10 - работают на клиенте

  • А строки с 6 по 9 (тело лямбды env->...) работают на сервере

И это не псевдокод, это реальный рабочий код на Java (11).

Действительно код строк с 6 по 9 (само тело лямбды) передаются на сервер и исполняются на сервере, при том сервер о этом куске кода ничего не знает, он принимает этот код исполняет у себя, и отдает обратно клиенту - а я дальше продолжаю писать код без всякого deploy, не выходя из IDE (Idea/Eclipse/etc...).

Статья будет о том, что такое Serializable Lambda в Java, и как передавать байт-код таких лямбд на сервер, без перезапуска сервера, т.е. как можно в теории отказаться от привычных схем deploy приложений и писать более удобный api (это моя фантазия).


Допустим у нас есть такой интерфейс IEnv:

public interface IEnv {List<OsProc> processes();}

Который возвращает список процессов

public class OsProc implements Serializable {public Optional<Integer> getPpid(){ return ... }public int getPid(){ return ... }public void setPid(int pid){ ... }public String getName(){ return ... }public Optional<String> getCmdline(){ return ... }}

И вот таких простым способом я передаю лямбду на сервер

var query = TcpQuery   .create(IEnv.class).host("myserver.com").port(9988) .build();  query.apply( env -> env.processes().stream().filter(     p -> p.getName().contains("java") ) .collect(Collectors.toList()) ).forEach(System.out::println);

Введение - как это работает ?

При разработке клиентского приложения на Java

  1. Мы создаем набор исходных файлов, допустим Client.java

  2. Компилятор генерирует байт-код - файл Client.class

  3. При вызове query.apply() - мы передаем ссылку на лямбду env -> env.proc...toList())

  4. Реализация query.apply():

    1. Принимает ссылку на лямбду

    2. Для лямбды обнаруживает имя класса (например Client) и метода (например lambda1) реализующего лямбду

    3. Отыскивает среди ресурсов программы соответствующий байт-код класса (Client.class) и его метода

    4. Загружает байт-код реализации лямбды и отправляет его на сервер

      1. Сервер принимает байт-код лямбды

      2. Генерирует в памяти класс в который вставляет принятый байт-код

      3. Загружает этот класс в память и через рефлексию получает доступ к лямбде

      4. Возвращает идентификатор этого метода обратно

    5. Принимает идентификатор метода и делает вызов его на сервере

      1. Сервер выполняет ранее скомпилированный класс/метод/лямбду

      2. Возвращает результат выполнения

    6. Принимает результат серверного вызова и возвращает его как результат локального вызова

  5. Возврат результата вызова query.apply()

Далее в статье будет рассмотрено я на примере очень простой библиотеки покажу, как запустить свои наработки в режиме клиент-сервера, и писать клиентов без выхода из IDE и перезапуска серверов.

Как воспользоваться?

Что бы с этим по играть, воспользуйтесь моим pet project-ом:

v 1.0 https://github.com/gochaorg/trambda/releases/tag/1.0

maven

<dependency>  <groupId>xyz.cofe</groupId>  <artifactId>trambda</artifactId>  <version>1.0</version>  <type>pom</type></dependency><dependency>  <groupId>xyz.cofe</groupId>  <artifactId>trambda-core</artifactId>  <version>1.0</version></dependency><dependency>  <groupId>xyz.cofe</groupId>  <artifactId>trambda-tcp</artifactId>  <version>1.0</version></dependency>

Чтобы воспользоваться, у вас должна быть какая нибудь библиотека к которой вы хотите обращаться по сети (TCP)

В моем примере это будет очень простая библиотека, в ней всего три файла:

  • OsProc.java - описывает процесс ОС (описание было выше)

  • IEnv.java - Интерфейс получения списка процессов для ОС (описание было выше)

  • LinuxEnv.java - Получение списка процессов для ОС Linux - реализация IEnv

Библиотека из примера работает так:

package xyz.cofe.trambda.demo.api;import org.junit.jupiter.api.Test;public class LinuxEnvTest {   @Test   public void test(){       var env = new LinuxEnv();       env.processes().stream()           .filter(p->p.getName().equalsIgnoreCase("java"))           .forEach(System.out::println);   }}

LinuxEnv - это простой класс, он устроен так:

package xyz.cofe.trambda.demo.api;import java.util.ArrayList;import java.util.List;import xyz.cofe.io.fs.File;public class LinuxEnv implements IEnv {   @Override   public List<OsProc> processes(){       ArrayList<OsProc> procs = new ArrayList<>();       File procDir = new File("/proc");       procDir.dirList().stream()           .filter( d -> d.getName().matches("\\d+") && d.isDir() )           .map(OsProc::linuxProc)           .forEach(procs::add);       return procs;   }}

Код его тривиален, он сканирует каталог /proc и находит описание процесса ОС, (для Linux все процессы сервера отображаются в виде файлов/подкаталогов /proc)

Скомпилируем библиотеку (или возьмем свою)

Клонируем репозиторий (git commit 67ec260)

> git clone https://github.com/gochaorg/trambda.gitКлонирование в trambda...remote: Enumerating objects: 978, done.remote: Counting objects: 100% (978/978), done.remote: Compressing objects: 100% (464/464), done.remote: Total 978 (delta 308), reused 862 (delta 195), pack-reused 0Получение объектов: 100% (978/978), 715.70 KiB | 559.00 KiB/s, готово.Определение изменений: 100% (308/308), готово.

Собираем библиотеку для демонстрации

user@user-Modern-14-A10RB:22:10:35:~/Загрузки/sample-tr:> cd trambda/trambda-demo/tr-demo-api/user@user-Modern-14-A10RB:22:10:49:~/Загрузки/sample-tr/trambda/trambda-demo/tr-demo-api:> mvn clean package install...[INFO] ------------------------------------------------------------------------[INFO] BUILD SUCCESS[INFO] ------------------------------------------------------------------------[INFO] Total time:  13.933 s[INFO] Finished at: 2021-04-18T22:11:11+05:00[INFO] ------------------------------------------------------------------------

В каталоге target должен оказаться файл jar с нашей библиотекой

user@user-Modern-14-A10RB:22:13:13:~/Загрузки/sample-tr/trambda/trambda-demo/tr-demo-api:> ll target/итого 48drwxrwxr-x 10 user user 4096 апр 18 22:11 ./drwxrwxr-x  4 user user 4096 апр 18 22:11 ../drwxrwxr-x  3 user user 4096 апр 18 22:11 classes/drwxrwxr-x  3 user user 4096 апр 18 22:11 generated-sources/drwxrwxr-x  3 user user 4096 апр 18 22:11 generated-test-sources/drwxrwxr-x  2 user user 4096 апр 18 22:11 maven-archiver/drwxrwxr-x  3 user user 4096 апр 18 22:11 maven-status/drwxrwxr-x  4 user user 4096 апр 18 22:11 site/drwxrwxr-x  2 user user 4096 апр 18 22:11 surefire-reports/drwxrwxr-x  3 user user 4096 апр 18 22:11 test-classes/-rw-rw-r--  1 user user 6337 апр 18 22:11 tr-demo-api-1.0-SNAPSHOT.jar

Запуск сервера

Теперь когда библиотека собрана копируем сервер с github и распаковываем архив

user@user-Modern-14-A10RB:22:37:25:~/Загрузки/sample-tr:> wget https://github.com/gochaorg/trambda/releases/download/1.0/trambda-tcp-serv-cli.zip--2021-04-18 22:37:31--  https://github.com/gochaorg/trambda/releases/download/1.0/trambda-tcp-serv-cli.zipРаспознаётся github.com (github.com)... 140.82.121.4Подключение к github.com (github.com)|140.82.121.4|:443... соединение установлено.HTTP-запрос отправлен. Ожидание ответа... 302 FoundАдрес: https://github-releases.githubusercontent.com/350075998/47380d00-9b40-11eb-90a4-4e353f42e67c?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20210418%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20210418T173731Z&X-Amz-Expires=300&X-Amz-Signature=97ade1f58bfbe1eaa320805179987e8c4df730b9f5eddf24c05662fb676caafe&X-Amz-SignedHeaders=host&actor_id=0&key_id=0&repo_id=350075998&response-content-disposition=attachment%3B%20filename%3Dtrambda-tcp-serv-cli.zip&response-content-type=application%2Foctet-stream [переход]--2021-04-18 22:37:31--  https://github-releases.githubusercontent.com/350075998/47380d00-9b40-11eb-90a4-4e353f42e67c?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20210418%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20210418T173731Z&X-Amz-Expires=300&X-Amz-Signature=97ade1f58bfbe1eaa320805179987e8c4df730b9f5eddf24c05662fb676caafe&X-Amz-SignedHeaders=host&actor_id=0&key_id=0&repo_id=350075998&response-content-disposition=attachment%3B%20filename%3Dtrambda-tcp-serv-cli.zip&response-content-type=application%2Foctet-streamРаспознаётся github-releases.githubusercontent.com (github-releases.githubusercontent.com)... 185.199.111.154, 185.199.108.154, 185.199.109.154, ...Подключение к github-releases.githubusercontent.com (github-releases.githubusercontent.com)|185.199.111.154|:443... соединение установлено.HTTP-запрос отправлен. Ожидание ответа... 200 OKДлина: 12107487 (12M) [application/octet-stream]Сохранение в: trambda-tcp-serv-cli.ziptrambda-tcp-serv-cli.zip                                    100%[========================================================================================================================================>]  11,55M  3,75MB/s    за 3,1s    2021-04-18 22:37:35 (3,75 MB/s) - trambda-tcp-serv-cli.zip сохранён [12107487/12107487]user@user-Modern-14-A10RB:22:37:35:~/Загрузки/sample-tr:> llитого 11836drwxrwxr-x  3 user user     4096 апр 18 22:37 ./drwxr-xr-x 11 user user     4096 апр 18 22:00 ../drwxrwxr-x 10 user user     4096 апр 18 22:10 trambda/-rw-rw-r--  1 user user 12107487 апр 12 03:36 trambda-tcp-serv-cli.zipuser@user-Modern-14-A10RB:22:37:42:~/Загрузки/sample-tr:> unzip trambda-tcp-serv-cli.zip Archive:  trambda-tcp-serv-cli.zip   creating: trambda-tcp-serv-cli/   creating: trambda-tcp-serv-cli/jars/  inflating: trambda-tcp-serv-cli/jars/asm-9.1.jar    inflating: trambda-tcp-serv-cli/jars/jline-2.14.6.jar    inflating: trambda-tcp-serv-cli/jars/iofun-1.0.jar    inflating: trambda-tcp-serv-cli/jars/groovy-swing-3.0.7.jar    inflating: trambda-tcp-serv-cli/jars/groovy-console-3.0.7.jar    inflating: trambda-tcp-serv-cli/jars/groovy-xml-3.0.7.jar    inflating: trambda-tcp-serv-cli/jars/trambda-tcp-serv-cli-1.0.jar    inflating: trambda-tcp-serv-cli/jars/ecolls-1.10.jar    inflating: trambda-tcp-serv-cli/jars/trambda-core-1.0.jar    inflating: trambda-tcp-serv-cli/jars/slf4j-api-1.7.25.jar    inflating: trambda-tcp-serv-cli/jars/asm-tree-9.1.jar    inflating: trambda-tcp-serv-cli/jars/asm-util-9.1.jar    inflating: trambda-tcp-serv-cli/jars/fs-1.2.jar    inflating: trambda-tcp-serv-cli/jars/logback-classic-1.2.3.jar    inflating: trambda-tcp-serv-cli/jars/trambda-tcp-1.0.jar    inflating: trambda-tcp-serv-cli/jars/groovy-groovysh-3.0.7.jar    inflating: trambda-tcp-serv-cli/jars/groovy-templates-3.0.7.jar    inflating: trambda-tcp-serv-cli/jars/asm-analysis-9.1.jar    inflating: trambda-tcp-serv-cli/jars/text-1.0.jar    inflating: trambda-tcp-serv-cli/jars/logback-core-1.2.3.jar    inflating: trambda-tcp-serv-cli/jars/groovy-3.0.7.jar    inflating: trambda-tcp-serv-cli/jars/cbuffer-1.3.jar     creating: trambda-tcp-serv-cli/bin/  inflating: trambda-tcp-serv-cli/bin/trambda-tcp-serv.bat    inflating: trambda-tcp-serv-cli/bin/trambda-tcp-serv  user@user-Modern-14-A10RB:22:37:50:~/Загрузки/sample-tr:> rm trambda-tcp-serv-cli.zip 

После копируем нашу библиотеку в каталог trambda-tcp-serv-cli/jars

ser@user-Modern-14-A10RB:22:40:47:~/Загрузки/sample-tr:> cp -v trambda/trambda-demo/tr-demo-api/target/tr-demo-api-1.0-SNAPSHOT.jar trambda-tcp-serv-cli/jars/'trambda/trambda-demo/tr-demo-api/target/tr-demo-api-1.0-SNAPSHOT.jar' -> 'trambda-tcp-serv-cli/jars/tr-demo-api-1.0-SNAPSHOT.jar'

Нам понадобиться подготовить скрипт запуска, скрипт на языке groovy (не беспокойтесь, это не обязательно именно так запускать, все можно сделать на голой Java)

Возьмем скрипт из примера:

> cat trambda/trambda-tcp-serv-cli/src/test/samples/sample-1.groov
// Сервис xyz.cofe.trambda.demo.api.LinuxEnv // будет запущен на порту 9988, и будет доступен с любого IPapp.service( "0.0.0.0:9988", new xyz.cofe.trambda.demo.api.LinuxEnv() ) {    daemon false    // Указываем настройки безопасности    security {        // Какие API/Методы будут доступны извне        allow {            // method("System") {            //     methodOwner ==~ /java.lang.System/ && methodName in ['gc']            // }            // field( "System.out" ) {            //     fieldOwner ==~ /java.lang.System/ && fieldName in ['out','in','err'] && readAccess            // }            invoke( 'Java compiler' ){                methodOwner ==~ /java\.lang\.invoke\.(LambdaMetafactory|StringConcatFactory)/            }            invoke( 'Java collections' ){                methodOwner ==~ /java\.util\.(stream\.(Stream|Collectors)|(List))/            }            invoke( 'Java lang' ){                methodOwner ==~ /java\.lang\.String/            }            invoke( 'Api '){                methodOwner ==~ /xyz\.cofe\.trambda\.demo\.api\.(IEnv|OsProc)/            }        }        // Для всех остальных случаев - запрещаем вызов        deny {            any("ban all")        }    }}

Запускаем сервер

user@user-Modern-14-A10RB:22:56:08:~/Загрузки/sample-tr:> bash ./trambda-tcp-serv-cli/bin/trambda-tcp-serv -s trambda/trambda-tcp-serv-cli/src/test/samples/sample-1.groovy# [main] INFO  x.c.t.tcp.serv.cli.TcpServerCLI - starting xyz.cofe.trambda.tcp.serv.cli.TcpServerCLI # [main] INFO  x.c.t.tcp.serv.cli.TcpServerCLI - executeScript( "trambda/trambda-tcp-serv-cli/src/test/samples/sample-1.groovy", UTF-8 ) # [main] INFO  x.c.t.tcp.serv.cli.TcpServerCLI - registry class xyz.cofe.trambda.demo.api.LinuxEnv on 0.0.0.0:9988 # [main] INFO  x.c.t.tcp.serv.cli.TcpServerCLI - starting service xyz.cofe.trambda.demo.api.LinuxEnv@55e7a35c on /0.0.0.0:9988 # [main] DEBUG x.c.t.tcp.serv.cli.TcpServerCLI - create server socket # [main] DEBUG x.c.t.tcp.serv.cli.TcpServerCLI - bind server socket /0.0.0.0:9988 # [main] DEBUG x.c.t.tcp.serv.cli.TcpServerCLI - server started 

Все, сервер запущен, теперь можем им пользоваться

Запуск клиента

Клиента можно взять из примера (ClientTest.java)

package xyz.cofe.trambda.demo.client;import java.util.stream.Collectors;import org.junit.jupiter.api.Test;import xyz.cofe.trambda.demo.api.IEnv;import xyz.cofe.trambda.tcp.TcpQuery;public class ClientTest {   @Test   public void test01(){       var query = TcpQuery           .create(IEnv.class).host("localhost").port(9988)           .build();       query.apply(           env -> env.processes().stream().filter(p ->               p.getName().contains("java"))           .collect(Collectors.toList())       ).forEach(System.out::println);   }}

Запустим и посмотрим, что он выведет, часть логов я опущу - т.к. для пояснения работы они не важны

Что происходит на сервере ?

часть логов я опущу - т.к. для пояснения работы они не важны

Настройка безопасности сервера

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

// Создание TCP сокетаServerSocket ssocket = new ServerSocket(port);// Настраиваем сокетssocket.setSoTimeout(1000*5);// Создаем серверserver = new TcpServer<IEnv>(    // Передаем сокет    ssocket,        // Передаем функцию получения сервиса для новой сессии    s -> new LinuxEnv(),        // Настраиваем безопасность    SecurityFilters.create(s -> {                // Разрешаем вызовы строго - определенных методов         s.allow( a -> {                        // Публикуемый API нашего сервиса             a.invoke("demo api", c->                c.getOwner().matches(                    "xyz\\.cofe\\.trambda\\.tcp\\.demo\\.([\\w\\d]+)"));                        // Работа с коллекциями            a.invoke("java collections api", c->c.getOwner().matches(                "java\\.util\\.(List)|java\\.util\\.stream\\.([\\w\\d]+)"));                        // Работа с Java строками            a.invoke("java lang api", c->                c.getOwner().matches("java\\.lang\\.(String)"));                        // Методы которые использует компилятор Java            a.invoke("java compiler", c->                c.getOwner().matches(                    "java\\.lang\\.invoke\\.(LambdaMetafactory|StringConcatFactory)"));        });                // Все остальное запрещаем        s.deny().any("by default");    }));// Указываем что Thread сервера будет запущен как фоновыйserver.setDaemon(true);// Запускаем серверserver.start();// Создание TCP сокетаServerSocket ssocket = new ServerSocket(port);// Настраиваем сокетssocket.setSoTimeout(1000*5);

Код приведенный выше вроде кажется понятным, но наверно следует дать несколько пояснений

  • Правила проверки безопасности указываются в виде списка фильтров

  • Список фильтров обрабатывается последовательно и соответственно первые правила имеют более высокий приоритет, нежели последние

  • В списке могут быть как разрешающие, так и запрещающие правила

  • Java компилятор генерирует

    • вызовы LambdaMetafactory когда вы используете лямбды в своем коде

    • вызовы StringConcatFactory когда вы применяете оператор + к строкам

  • Вы также можете контролировать обращения к полям класса на чтения/запись - см. SecurityFilters.java / PredicateBuilder#field

  • Для доступа к стандартным функция, там даже как Object.toString() то, же надо будет задать правила доступа

Более подробную информацию о работе можно найти на странице проекта, например на github или git pages

Чем это может быть полезно?

Резонный вопрос, чем может быть полезно и чем оно отличаеться от уже существующих, например RMI, gRPC ? Давайте рассмотрим отличия

Протоколов передачи данных и вызова процедур много, будут рассмотрены некоторые из известных автору. Эти протоколы можно разделить на несколько категорий/свойств:

  • Передача данных

    • Строго ограниченные форматы данных / типы данных

    • Гибкие форматы данных / комбинированные/структурированные типы данных

  • Передача программного кода

    • Ограничения на алгоритмы

      • Простые выражения

      • Циклы/ветвления

      • Процедуры/функции/объекты

      • Ограничения на интерпретацию алгоритмов

      • Слабая типизация

      • Строгая типизация

    • Безопасность

      • авторизация и т.д.

      • время исполнения

      • и т.д.

  • Поддержка существующий решений

    • Потребность в перезапуске серверов, что бы опробовать новые решения

    • Возможность на ходу опробовать новые решения

    • Профилирование выполнения

Фича

Java-RMI

SOAP

REST-JSON

SQL

GraphQL

Hadoop

Передача данных

+

+

+

+

+

+

Строго ограниченные форматы данных

+/-

+/-

-

-

+

-

Гибкие форматы данных

+/-

+/-

+

+

-

+

Передача программного кода

-

-

-

+

-

+

Простые выражения

-

-

-

+

-

+

Циклы/ветвления

-

-

-

+

-

+

Процедуры/функции

-

-

-

+

-

+

программные объекты

-

-

-

?

-

?

Слабая типизация

-

-

+

-

-

+

Строгая типизация

+

+

-

+

+

-

авторизация

+

+

+

+

+

+

время исполнения

?

?

?

+/-

?

?

Потребность в перезапуске серверов

+

+

?

-

+

-

Возможность находу опробывать новые решения

-

-

?

+

-

+

Профилирование выполнения

+

+

?

+

+

?

  • Большинство протоколов ориентированы только на передачу данных (Java-RMI, SOAP, REST-JSON, GraphQL)

    • Часть из них работают со строго типизированными данными (Java-RMI, SOAP, GraphQL)

    • Другие (REST-JSON, Hadoop) со слабо типизированными

  • Небольшое кол-во протоколов поддерживают еще передачу программного кода (SQL, Hadoop)

Наличие строгой типизации и передачу программного кода из рассмотренных есть только в SQL

В предлагаемом проекте есть следующие возможности, с оговорками

  • Передача данных (*)

    • Гибкие форматы данных / комбинированные / структурированные типы данных

  • Передача программного кода

    • Простые выражения

    • Циклы/ветвления

    • Процедуры/функции/

    • программные объекты (**)

  • Ограничения на интерпретацию алгоритмов

    • Строгая типизация

    • Безопасность (***)

  • Поддержка существующий решений

    • Возможность на ходу опробовать новые решения

    • Профилирование выполнения (****)

Оговорки

  • (*)

    • передаваемые типы должны быть Serializable

    • требуется апробация Proxy для интерфейсов

  • (**)

    • требуется апробация Proxy для объектов - очень неоднозначный вопрос

  • (***)

    • реализована проверка байт-кода, без учета текущего пользователя

    • не реализован механизм аутентификации, см план реализации

  • (****)

Область применения

Поскольку проект только начат, говорить о реальном применении рано, можно говорить о потенциальном применении

Возможны следующий области применения

  • фильтрация данных в программах написанных на Java по аналогии SQL WHERE (уже есть)

  • выполнение серверных процедур по аналогии RPC/RMI/SOAP/ (уже есть)

  • подписка клиента на события сервера

  • масштабирование нагрузки (как частный случай реализации сетевого протокола)

При дальнейшем развитии возможно автоматическая прозрачная трансляция JAVA/Kotlin/Scala кода в целевые системы (SQL, MongoDB, REST, )

Данное возможно при условии развития функции декомпиляции байт-кода в код AST/Java/, по факту такая функция реализована в декомпиляторе JAD

Что собственно ведет к уменьшению издержек при разработке ПО.

Как же оно внутри работает ?

Идея была проста, весь код Java который компилируется, обычно сохраняется в виде байт-кода который является файлом с расширением .class, а для любого объекта java можно узнать класс, обычно это выглядит так: obj.getClass()

Через объект Class можно узнать его название, и соответственно через Class.getResource(имя класса) можно получить URL ссылку на данный файл.

Гладко было на бумаге, но забыли про овраги

Такова была идея, но есть нюансы

Процесс разработки, как найти байт код?

Допустим у нас есть такой код

package xyz.cofe.trambda.l1;import java.util.function.Function;import org.junit.jupiter.api.Test;public class SimpleLambdaTest {   @Test   public void javaLambda01(){       Function<Function<String,String>,String> test = (f) -> {           System.out.println("f="+f.getClass());           return null;       };       test.apply( x -> x.repeat(4) );   }}

При выполнении теста будет вот это:

f=class xyz.cofe.trambda.l1.SimpleLambdaTest$$Lambda$235/0x0000000800142040

По идее у нас в каталоге test-classes/ должен быть файл SimpleLambdaTest$$Lambda$235, но такого файла не видно

user@user-Modern-14-A10RB:00:41:32:~/code/trambda/trambda-core/target/test-classes/xyz/cofe/trambda/l1:> llитого 12drwxrwxr-x 2 user user 4096 апр 25 00:40 ./drwxrwxr-x 5 user user 4096 апр 25 00:40 ../-rw-rw-r-- 1 user user 2162 апр 25 00:40 SimpleLambdaTest.class

Тогда посмотрим байт код

> javap -p SimpleLambdaTest.class Compiled from "SimpleLambdaTest.java"public class xyz.cofe.trambda.l1.SimpleLambdaTest {  public xyz.cofe.trambda.l1.SimpleLambdaTest();  public void javaLambda01();  private static java.lang.String lambda$javaLambda01$1(java.lang.String);  private static java.lang.String lambda$javaLambda01$0(java.util.function.Function);}

По факту где-то lambda$javaLambda01$1 или lambda$javaLambda01$0 находиться код нашей лямбды, но можно долго гадать, но это не наш подход

По факту сопоставить class и лямбду в Java с разбегу не удастся, а гадать это сразу путь на костыли.

В Java есть интересный интерфейс Serializable, и этим интерфейсом можно пометить лямбду

Например так

package xyz.cofe.trambda.l2;import java.io.Serializable;import java.util.function.Function;public interface Fn<A,Z> extends Function<A,Z> , Serializable {}

или вот так

Runnable r = (Runnable & Serializable)() -> System.out.println("Serializable!");

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

package xyz.cofe.trambda.l2;import java.lang.invoke.SerializedLambda;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;import java.util.function.Function;import org.junit.jupiter.api.Test;public class SerialLambdaTest {   @Test   public void serLambda01(){       Fn<Fn<String,String>,String> test = (lambda) -> {           System.out.println("lambda="+lambda.getClass());           Method writeReplace = null;           try{               writeReplace = lambda.getClass().getDeclaredMethod("writeReplace");               writeReplace.setAccessible(true);               SerializedLambda sl = (SerializedLambda) writeReplace.invoke(lambda);               System.out.println(sl);           } catch( NoSuchMethodException | InvocationTargetException | IllegalAccessException e ) {               e.printStackTrace();           }           return null;       };       test.apply( x -> x.repeat(4) );   }}

Теперь результат будет таким, его разбор будет ниже.

lambda=class xyz.cofe.trambda.l2.SerialLambdaTest$$Lambda$235/0x0000000800142040SerializedLambda[capturingClass=class xyz.cofe.trambda.l2.SerialLambdaTest, functionalInterfaceMethod=xyz/cofe/trambda/l2/Fn.apply:(Ljava/lang/Object;)Ljava/lang/Object;, implementation=invokeStatic xyz/cofe/trambda/l2/SerialLambdaTest.lambda$serLambda01$3fed5817$1:(Ljava/lang/String;)Ljava/lang/String;, instantiatedMethodType=(Ljava/lang/String;)Ljava/lang/String;, numCaptured=0]

А при просмотре байт кода мы увидим

user@user-Modern-14-A10RB:00:51:48:~/code/trambda/trambda-core/target/test-classes/xyz/cofe/trambda/l2:> javap -p SerialLambdaTest.class Compiled from "SerialLambdaTest.java"public class xyz.cofe.trambda.l2.SerialLambdaTest {  public xyz.cofe.trambda.l2.SerialLambdaTest();  public void serLambda01();  private static java.lang.Object $deserializeLambda$(java.lang.invoke.SerializedLambda);  private static java.lang.String lambda$serLambda01$3fed5817$1(java.lang.String);  private static java.lang.String lambda$serLambda01$47b6c34$1(xyz.cofe.trambda.l2.Fn);}

Первое что мы можем заметить - у нас появился метод $deserializeLambda$ пока не важно что делает этот метод, есть факт что добавление интерфейса Serializable меняет поведение компилятора.

java.lang.invoke.SerializedLambda - это final класс, который содержит ряд интересных свойств, а именно

  • String getImplClass() - Имя класса, содержащего метод реализации.

  • String getImplMethodName() - название метода реализации.

В stdout можно заметить фрагмент

implementation=invokeStatic xyz/cofe/trambda/l2/SerialLambdaTest.lambda$serLambda01$3fed5817$1:(Ljava/lang/String;)Ljava/lang/String;

И в байт коде:

private static java.lang.String lambda$serLambda01$3fed5817$1(java.lang.String);

Т.е. можно однозначно установить байт-код для лямбды

Сериализация байт кода

Теперь, на данном этапе прочесть байт код лямбды - дело техники, а именно так:

SerializedLambda sl = (SerializedLambda)writeReplace.invoke(lambda);var implClassName = sl.getImplClass()var implClassUrl =labmda.getClass().getResource("/"+implClassName.replace(".","/")+".class");

implClassUrl - указывает на файл класса содержащего байт код

получаем байт код из URL как массив байтов

byte[] classByteCode = null;try{   classByteCode = IOFun.readBytes(implClassUrl);} catch( IOException e ) {   throw new IOError(e);}

После этого передаем этот набор байтов в библиотеку ASM для прочтения байт кода

var classReader = new ClassReader(classByteCode);

и читаем байт код нужного метода

ClassVisitor cv = new ClassVisitor(Opcodes.ASM9) {   @Override   public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {       if( methName.equals(name) && descriptor!=null && descriptor.equals(methSign) ){           mdef0.set(new MethodDef(access,name,descriptor,signature,exceptions));           return dump(byteCodes::add);       }       return null;   }};cr.accept(cv, 0);

MethodVisior - это класс который получает байт код конкретного метода. Методы этого класса можно переопределить, рассмотрим часть реализации MethodDump extends MethodVisitor:

package xyz.cofe.trambda;public class MethodDump extends MethodVisitor implements Opcodes {...@Overridepublic void visitParameter(String name, int access){   emit(new MParameter(name,access));}@Overridepublic void visitInsn(int opcode){   emit(new MInsn(opcode));}...}

Данный класс принимает байт код через вызовы методов visitXXXX(...) - их много методов (по этому показа только часть)

При каждом вызове метода генерируется объект который описывает вызов, например для visitInsn( op ) генерируется new MInsn(op), а потом этот объект передается выше emit(..) где уже этот объект передается в сеть

Восстановление классов из байт кода

После этого как код передан по сети в виде набора объектов, эти объекты обратно собирается в байт код (с дополнительными проверками безопасности)

Для этого в моей библиотеке есть класс xyz.cofe.trambda.MethodRestore его работа заключается примерно в следующем

public synchronized byte[] generate(){  // генерируем имя целевого класса  binClassName = className.replace('.', '/');// Создаем ClassWriter (часть ASM) // в котором будем вызывать методы visitXXXX( op )var cw = new ClassWriter(ClassWriter.COMPUTE_MAXS|ClassWriter.COMPUTE_FRAMES);cw.visit(Opcodes.V11,   Opcodes.ACC_PUBLIC|Opcodes.ACC_SUPER,   binClassName,null,   "java/lang/Object", null);// генерация методаvar mv = cw.visitMethod(  acc, // флаги как static public  name, // имя метода  desc, // параметры метода  sign, // сигнатура если есть Generic параметры  excepts); // исключения которые может генерировать метод// Потом в цикле для каждого переданного объекта // генерируем соответствующий вызов visitXXXX()// генерируем код в той же последовательности, что и был прочитанfor( var bc : byteCodes ){   if( bc instanceof MCode )build((MCode) bc);   else if( bc instanceof MEnd )build((MEnd) bc);   else if( bc instanceof MLabel )build((MLabel) bc);   else if( bc instanceof MLineNumber )build((MLineNumber) bc);   else if( bc instanceof MVarInsn )build((MVarInsn) bc);  ...}// Получаем байт кодreturn cw.toByteArray();}// Вызов visitCode() - начало методаprotected void build(MCode code){ mv.visitCode(); }// Вызов visitEnd() - конец методаprotected void build(MEnd end){ mv.visitEnd(); }protected void build(MTypeInsn tinst){   mv.visitTypeInsn(tinst.getOpcode(), tinst.getOperand());}

Теперь когда у нас есть на сервере байт код, остается его загрузить и вызвать соответствующий метод

Для этого создаем свой ClassLoader

var byteCode = new MethodRestore()   .className(clName)   .methodName("lambda1")   .methodDef(mdef)   .generate();ClassLoader cl = new ClassLoader(ClassLoader.getSystemClassLoader()) {   @Override   protected Class<?> findClass(String name) throws ClassNotFoundException{       if( name!=null && name.equals(clName) ){           return defineClass(name,byteCode,0,byteCode.length);       }       return super.findClass(name);   }};

Загружаем класс

System.out.println("try read class "+clName);Class c = null;try{   c = Class.forName(clName,true,cl);   System.out.println("class found "+c);} catch( ClassNotFoundException e ) {   e.printStackTrace();   return;}

Ищем нужный нам метод класса

Method m = null;System.out.println("methods");for( var delMeth : c.getDeclaredMethods() ){  System.out.println(""+delMeth);  if( delMeth.getName().equals(methName) ){    m = delMeth;  }}

и вызываем его с параметрами

try{   Object arg0 = "abc";   System.out.println("call with "+arg0);   Object res = m.invoke(null, arg0);   System.out.println("result "+res);} catch( IllegalAccessException | InvocationTargetException e ) {   e.printStackTrace();}

Ограничения

Конечно у данного решения есть ряд ограничений

  1. Не для любого языка подойдет

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

    2. для динамических языков (Groovy, JavaScript) вообще наверно не подойдет - но там наверно можно и по другому передавать лямбды, например передавать AST дерево лямбд - все зависит от их представления промежуточного кода, чем бы это не являлось.

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

  3. Потенциально на клиенте может Java более новая, с другим байт-кодом нежели на сервере, сервер может не знать о каких либо конструкциях новой Java

  4. Библиотека не весь байт код передает, это связано с тем, что не весь байт код имеет объектное представление - наверно это решаемый вопрос, вопрос доработки.

  5. Отдельно - это вопрос передачи по сети

    1. Вопрос безопасности - технически это решаемо, но требует допиливание напильником

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

Подробнее..

Перевод Rust 1.52.0 улучшения Clippy и стабилизация API

07.05.2021 14:19:42 | Автор: admin

Команда Rust рада сообщить о выпуске новой версии 1.52.0. Rust это язык программирования, позволяющий каждому создавать надёжное и эффективное программное обеспечение.


Если вы установили предыдущую версию Rust средствами rustup, то для обновления до версии 1.52.0 вам достаточно выполнить следующую команду:


rustup update stable

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


Что было стабилизировано в 1.52.0


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


Ранее запуск cargo clippy после cargo check не запускал Clippy: кэширование в Cargo не видело разницы между ними. В версии 1.52 это поведение было исправлено, а значит, теперь пользователи будут получать то поведение, которое ожидают, независимо от порядка запуска этих команд.


Стабилизированные API


Следующие методы были стабилизированы:



Следующие ранее стабилизированные API стали const:



Другие изменения


Синтаксис, пакетный менеджер Cargo и анализатор Clippy также претерпели некоторые изменения.


Участники 1.52.0


Множество людей собрались вместе, чтобы создать Rust 1.52.0. Мы не смогли бы сделать это без всех вас. Спасибо!


От переводчиков


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


Данную статью совместными усилиями перевели Belanchuk, TelegaOvoshey, blandger, nlinker и funkill.

Подробнее..

Почему за интерпретируемыми языками будущее

31.05.2021 00:23:47 | Автор: admin

Зададимся вопросом, что такое прогресс? Википедия говорит нам следующее:

Прогресс, проградация(лат.pro движение вперёд, успех;grad шагать, ступать;gradatio постепенное повышение) направление развития от низшего к высшему, положительная динамика, поступательное движение вперед, повышение уровня организации, усложнение способа организации, характеризуется увеличением внутренних связей. Противоположность регресс,деградация

Люди, общество, социум испокон веков стремилось к познанию, открытию, изучению. Вы когда- нибудь задавались вопросом, почему? Почему мы прогрессируем? Что это, код, заложенный в нас природой на генном уровне или чей-то промысел? От каменного оружия до нанотехнологий, от лечения травами, до пересадки сердца, постепенно, со временем, но мы улучшаем и облегчаем нашу жизнь! Вот, два ключевых слова, улучшаем и облегчаем, именно эти два слова, являются нашим двигателем, двигателем развития и прогресса. О чем вы подумаете, если я вам скажу, что война, это тоже необратимая часть прогресса? История, является отражением нашего с вами развития, наших поступков и достижений, от простейшего организма, до организма со сложнейшей структурой. Мы делаем выводы, мы учимся, словно нейросеть. Допускали ли вы, что человек это и есть искусственный интеллект. Забавно, но человек, подобно Богу, стремится создать существо, по образу и подобию своему.

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

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

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

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

"Всё гениальное просто, и всё простое гениально. Маленькому человечку нравится скрывать свою ничтожность за сложными вещами." Йозеф Геббельс

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

Да, интерпретируемые языки, такие как Python, Ruby, являются "медленными", в сравнении с такими компилируемыми языками, как Java, С++. Скорость работы программ, написанных на них, позволяет нам управлять ракетами, и мы безусловно, должны чтить и уважать их, ибо благодаря им мы имеем, что имеем. Но дело не только в скорости работы программ, но и в простоте написания и скорости разработки. Instagram, Hulu, Twich, Github, Airbnb, Shopify, Reddit, Spotify, все это написано на интерпретируемом языке и я сомневаюсь, что кто-то станет писать Instagram на C++/C, только потому что он быстрее Python, также, верно и обратное утверждение, на Python ни кто не будет писать операционную систему или драйвера. Однако, человеческая природа хочет облегчать и совершенствовать различного рода процессы. Это заложено в нас, идти в перед, развиваться и я не на миг не сомневаюсь, что лет через 100, языки программирования будут подобно человеческому, скорость которых будет сравнима скорости машинному языку. И все это, благодаря человечеству, который подобно организму, развивается, обучается и улучшается.

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

"Я хочу, чтобы компьютер был моим слугой, а не господином, поэтому я должен уметь быстро и эффективно объяснить ему, что делать. " Мацумото Юкихиро

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

"Скорость работы больше не является главной проблемой. Как быстро вы зайдёте на рынок, да." Дин Хуэй

Подробнее..

Перевод Rust 1.53.0 IntoIterator для массивов, quotquot в шаблонах, Unicode-идентификаторы, поддержка имени HEAD-ветки в Cargo

18.06.2021 18:20:53 | Автор: admin

Команда Rust рада сообщить о выпуске новой версии 1.53.0. Rust это язык программирования, позволяющий каждому создавать надёжное и эффективное программное обеспечение.


Если вы установили предыдущую версию Rust средствами rustup, то для обновления до версии 1.53.0 вам достаточно выполнить следующую команду:


rustup update stable

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


Что было стабилизировано в 1.53.0


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


IntoIterator для массивов


Это первый выпуск Rust, в котором массивы реализуют типаж IntoIterator. Теперь вы можете итерироваться в массиве по значению:


for i in [1, 2, 3] {    ..}

Раньше это было возможно только по ссылке, с помощью &[1, 2, 3] или [1, 2, 3].iter().


Аналогично вы теперь можете передать массив в методы, ожидающие T: IntoIterator:


let set = BTreeSet::from_iter([1, 2, 3]);

for (a, b) in some_iterator.chain([1]).zip([1, 2, 3]) {    ..}

Это не было реализовано ранее из-за проблем с совместимостью. IntoIterator всегда реализуется для ссылок на массивы и в предыдущих выпусках array.into_iter() компилировался, преобразовываясь в (&array).into_iter().


Начиная с этого выпуска, массивы реализуют IntoIterator с небольшими оговорками для устранения несовместимости кода. Компилятор, как и прежде, преобразовывает array.into_iter() в (&array).into_iter(), как если бы реализации типажа ещё не было. Это касается только синтаксиса вызова метода .into_iter() и не затрагивает, например, for e in [1, 2, 3], iter.zip([1, 2, 3]) или IntoIterator::into_iter([1, 2, 3]), которые прекрасно компилируются.


Так как особый случай .into_iter() необходим только для предотвращения поломки существующего кода, он будет удалён в новой редакции Rust 2021, которая выйдет позже в этом году. Если хотите узнать больше следите за анонсами редакции.


"Или" в шаблонах


Синтаксис шаблонов был расширен поддержкой |, вложенного в шаблон где угодно. Это позволяет писать Some(1 | 2) вместо Some(1) | Some(2).


match result {     Ok(Some(1 | 2)) => { .. }     Err(MyError { kind: FileNotFound | PermissionDenied, .. }) => { .. }     _ => { .. }}

Unicode-идентификаторы


Теперь идентификаторы могут содержать не-ASCII символы. Можно использовать все действительные идентификаторы символов Unicode, определённые в UAX #31. Туда включены символы из многих разных языков и письменностей но не эмодзи.


Например:


const BLHAJ: &str = "";struct  {    : String,}let  = 1;

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


warning: identifier pair considered confusable between `` and `s`

Поддержка имени HEAD-ветки в Cargo


Cargo больше не предполагает, что HEAD-ветка в git-репозитории называется master. А следовательно, вам не надо указывать branch = "main" для зависимостей из git-репозиториев, в которых ветка по умолчанию main.


Инкрементальная компиляция до сих пор отключена по умолчанию


Как ранее говорилось в анонсе 1.52.1, инкрементальная компиляция была отключена для стабильных выпусков Rust. Функциональность остаётся доступной в каналах beta и nightly. Метод включения инкрементальной компиляции в 1.53.0 не изменился с 1.52.1.


Стабилизированные API


Следующие методы и реализации типажей были стабилизированы:



Другие изменения


Синтаксис, пакетный менеджер Cargo и анализатор Clippy также претерпели некоторые изменения.


Участники 1.53.0


Множество людей собрались вместе, чтобы создать Rust 1.53.0. Мы не смогли бы сделать это без всех вас. Спасибо!




От переводчиков


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


Данную статью совместными усилиями перевели TelegaOvoshey, blandger, Belanchuk и funkill.

Подробнее..

MoVfuscator2, безумный компилятор

21.05.2021 10:23:30 | Автор: admin


Однажды один умный чувак (Кристофер Домас) читал статью другого умного чувака (Стивена Долана) про удивительную особенность архитектуры x86. Стивен ругал её за избыточность и утверждал, что набор инструкций можно сократить до одной лишь mov, потому что она Тюринг-полная. Если бы Стивен не был таким умным, в его словах можно было бы усомниться, но у Кристофера загорелись глаза: проработав двадцать лет с x86, он не слышал ни о чём подобном, и ему страшно захотелось написать компилятор, который бы переводил весь код в наборы одних лишь mov-инструкций. Так родились M/o/Vfuscator и M/o/Vfuscator2, наглядно иллюстрирующие ненормальное программирование.

Идея


Небольшое отступление про инструкцию mov: это самая простая инструкция в ассемблере, перемещающая значение из памяти в регистры или из регистров в память. Как может штука, перекладывающая байты из одного места в другое, оказаться Тьюринг-полной? Ну, если вам правда интересно, почитайте оригинальную статью Стивена с доказательством. Если не очень, перейдём сразу к выводу:

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


Собственно, на последних словах Кристофер и загорается этой идеей и попутно соображает, что через подобную компиляцию можно нехило так обфусцировать код сам чёрт ногу сломит в этих бесконечных mov'ax! Для сравнения, обычный, читаемый ассемблер:



и та же самая программа на мувах:



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

mov может сравнивать значения
Допустим, вы хотите сравнить x и y, для этого вам понадобится следующий код:

  mov [x], 0  mov [y], 1  mov R, [x]


Если x == y, то в третьей строчке, где считывается значение по адресу x, окажется не ноль, а перезаписавшая его единица.
Если x != y, то считается ноль, так как единица лежит по другому адресу.

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



Ограничения
Для выполнения требуется одна инструкция jmp start (в конце списка mov'ов) для перевода программы в начало; для остановки нужен заведомо нерабочий адрес памяти.

Дальше Крис добавляет свои требования:

  • Использовать примитивные операции машины Тьюринга как основу для высокоуровневой логики
  • Работать надо с реальными данными, не с абстрактными символами (эксперимент Стивена всё-таки академичен, далёк от реального мира)
  • Должны быть реализованы основные операции: условные ветвления, арифметика, логика, циклы и так далее


Подробно о реализации некоторых вещей можно послушать в его докладе по ссылке (таймкод 9:06), а мы сразу перепрыгнем к состоянию оно реализовано и работает, чтобы не пересказывать оригинал.

Реализация


Первая версия компилятора была написана для брейнфака, для ощущения максимального абсурда и тщетности жизни реверсера, но, конечно, она осталась ужасно далека от реальных примеров и задач. Поэтому Крис спустя пару лет ВНЕЗАПНО выпустил M/o/Vfuscator2, рабочий mov-компилятор для С. Впечатляющий апгрейд, не правда ли?





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

  • Для дробных чисел используется самописный эмулятор плавающей точки, из-за размера поставляется в трёх версиях: softfloat32.o для float, softfloat64.o для float и double, и softfloatfull для полной поддержки стандарта IEEE
  • Так как арифметика строится на таблицах поиска, таблицы символов могут занимать огромное количество места, и их, возможно, придётся обрезать флагом -s
  • Компилятор работает строго на C89 из-за использования LCC в качестве фронтенда. Нельзя использовать bool, for (int ...), и другие фишки C99
  • Код с нестрогой типизацией или небезопасными конвертациями, скорее всего, не скомпилируется тоже из-за LCC
  • Функция, использующая внешние библиотеки, без прототипа лишает компилятор информации о необходимости и моменте подключения этих библиотек, что почти гарантированно повесит приложение
  • Вызовы внешних функций (printf и т.д.) через указатели функций еще не реализованы
  • Для подключения библиотек, скомпилированных не на mov, могут потребоваться другие инструкции. Полностью избавиться от них можно, перекомпилировав в mov все ресурсы


Заключение


Несмотря на всю крутизну проекта и Кристофера, нужно понимать, что такая обфускация скорее игрушка, чем реальный рабочий инструмент. И всё же, учитывая возможность прикрутить другие фронтенды и архитектуры открывает для M/o/Vfuscator больше возможностей, чем мог бы получить другой безумный ассемблерный проект.

Информацию по установке и использованию можно найти на гитхабе.



На правах рекламы


Эпично! Недорогие серверы на базе новейших процессоров AMD EPYC для размещения проектов любой сложности, от корпоративных сетей и игровых проектов до лендингов и VPN.

Присоединяйтесь к нашему чату в Telegram.

Подробнее..

C20 удивить линкер четыремя строчками кода

09.06.2021 16:12:52 | Автор: admin

Представьте себе, что вы студент, изучающий современные фичи C++. И вам дали задачу по теме concepts/constraints. У преподавателя, конечно, есть референсное решение "как правильно", но для вас оно неочевидно, и вы навертели гору довольно запутанного кода, который всё равно не работает. (И вы дописываете и дописываете всё новые перегрузки и специализации шаблонов, покрывая всё новые и новые претензии компилятора).

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

Сперва преподаватель (то есть, я) минимизировал код вот до такого: https://gcc.godbolt.org/z/TaMTWqc1T

// пусть у нас есть концепты указателя и вектораtemplate<class T> concept Ptr = requires(T t) { *t; };template<class T> concept Vec = requires(T t) { t.begin(); t[0]; };// и три перегрузки функций, рекурсивно определённые друг через другаtemplate<class T> void f(T t) {  // (1)  std::cout << "general case " << __PRETTY_FUNCTION__ << std::endl;}template<Ptr T> void f(T t) {  // (2)  std::cout << "pointer to ";  f(*t);  // допустим, указатель не нулевой}template<Vec T> void f(T t) {  // (3)  std::cout << "vector of ";  f(t[0]);  // допустим, вектор не пустой}// и набор тестов (в разных файлах)int main() {  std::vector<int> v = {1};    // тест А  f(v);  // или тест Б  f(&v);  // или тест В  f(&v);  f(v);  // или тест Г  f(v);  f(&v);}

Мы ожидаем, что

  • f(v) выведет "vector of general case void f(T) [T=int]"

  • f(&v) выведет "pointer to vector of general case void f(T) [T=int]"

А вместо это получаем

  • А: "vector of general case void f(T) [T=int]"

  • Б: "pointer of general case void f(T) [T=std::vector<int>]" ?

  • В: clang выводит Б и А ?!, gcc ошибку линкера

  • Г: clang и gcc выводят ошибку линкера

Что здесь не так?!

А не так здесь две вещи. Первая это то, что из функции (2) видны объявления только (1) и (2), поэтому результат разыменования указателя вызывается как (1).

Без концептов и шаблонов это тоже прекрасно воспроизводится: https://gcc.godbolt.org/z/47qhYv6q4

void f(int x)    { std::cout << "int" << std::endl; }void g(char* p)  { std::cout << "char* -> "; f(*p); }  // f(int)void f(char x)   { std::cout << "char" << std::endl; }void g(char** p) { std::cout << "char** -> "; f(**p); }  // f(char)int main() {  char x;  char* p = &x;  f(x);  // char  g(p);  // char* -> int  g(&p); // char** -> char}

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

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

Ладно, с этим разобрались. Вернёмся к шаблонам. Почему в тестах В и Г мы получили нечто, похожее на нарушение ODR?

Если мы перепишем код вот так:

template<class T> void f(T t) {.....}template<class T> void f(T t) requires Ptr<T> {.....}template<class T> void f(T t) requires Vec<T> {.....}

то ничего не изменится. Это просто другая форма записи. Требование соответствия концепту можно записать и так, и этак.

Но вот если прибегнем к старому доброму трюку SFINAE, https://gcc.godbolt.org/z/4sar6W6Kq

// добавим второй аргумент char или int - для разрешения неоднозначностиtemplate<class T, class = void> void f(T t, char) {.....}template<class T> auto f(T t, int) -> std::enable_if_t<Ptr<T>, void> {.....}template<class T> auto f(T t, int) -> std::enable_if_t<Vec<T>, void> {.....}..... f(v, 0) .......... f(&v, 0) .....

или ещё более старому доброму сопоставлению типов аргументов, https://gcc.godbolt.org/z/PsdhsG6Wr

template<class T> void f(T t) {.....}template<class T> void f(T* t) {.....}template<class T> void f(std::vector<T> t) {.....}

то всё станет работать. Не так, как нам хотелось бы (рекурсия по-прежнему сломана из-за правил видимости), но ожидаемо (вектор из f(T*) видится как "general case", из main - как "vector").

Что же ещё с концептами/ограничениями?

Коллективный разум, спасибо RSDN, подсказал ещё более минималистичный код!

Всего 4 строки: https://gcc.godbolt.org/z/qM8xYKfqe

template<class T> void f() {}void g() { f<int>(); }template<class T> void f() requires true {}void h() { f<int>(); }

Функция с ограничениями считается более предпочтительной, чем функция без них. Поэтому g() по правилам видимости выбирает из единственного варианта, а h() - из двух выбирает второй.

И вот этот код порождает некорректный объектный файл! В нём две функции с одинаковыми декорированными именами.

Оказывается, современные компиляторы (clang 12.0, gcc 12.0) не умеют учитывать requires в декорировании имён. Как когда-то старый глупый MSVC6 не учитывал параметры шаблона, если те не влияли на тип функции...

И, судя по ответам разработчиков, не только не умеют, но и не хотят. Отмазка: "если в разных точках программы одинаковые обращения к шаблону резолвятся по-разному, такая программа ill-formed, никакой диагностики при этом не нужно" (однако, ill-formed означает "не скомпилируется", а не "скомпилируется как попало"...)

Проблема известна с 2017 года, но прогресса пока нет.

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

Подробнее..

Перевод Как LLVM оптимизирует суммы степеней

08.05.2021 18:17:24 | Автор: admin
LLVM оптимизирует суммы степеней, например:
int sum(int count){  int result = 0;  for (int j = 0; j < count; ++j)    result += j*j;  return result;}

в код, вычисляющий результат без цикла (godbolt):
sum(int):        test    edi, edi        jle     .LBB0_1        lea     eax, [rdi - 1]        lea     ecx, [rdi - 2]        imul    rcx, rax        lea     eax, [rdi - 3]        imul    rax, rcx        shr     rax        imul    eax, eax, 1431655766        add     eax, edi        shr     rcx        lea     ecx, [rcx + 2*rcx]        lea     eax, [rax + rcx]        add     eax, -1        ret.LBB0_1:        xor     eax, eax        ret

Также обрабатываются более сложные случаи (godbolt) то есть оптимизация здесь не просто сравнивает паттерны. В этом посте мы рассмотрим, как выполняется эта оптимизация.

Анализ циклов скалярное развёртывание


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

И GCC, и LLVM делают это сходным способом, в проходах scalar evolution (я предпочел не переводить такие термины во избежание потери смысла прим перев.), в которых каждая переменная на итерации i (мы начинаем отсчитывать итерации с 0) представлена как функция $f_0(i)$, представленная как линейная рекуррентная форма

$f_j(i)=\begin{cases}\phi_j & if & i = 0\\f_j(i-1)\odot_{j+1}f_{j+1}(i-1)& if & x > 0\end{cases} $


где $\odot \in \big\{+, \ast \big\} $
Пример 1
Рассмотрим простейший пример цикла:
void foo(int m, int *p){  for (int j = 0; j < m; j++)    *p++ = j;}

Цикл записывает 0 в *p++ на первой итерации, 1 на второй, и т. д. Итак, мы можем выразить значение, записанное на итерации i как

$f_j(i)=\begin{cases}0 & if & i = 0\\f(j-1)+1& if & x > 0\end{cases} $


Пример 2
Полиномы также могут быть выражены в этой форме.
void foo(int m, int k, int *p){  for (int j = 0; < m; j++)    *p++ = j*j*j - 2*j*j + k*j + 7;}

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

$\begin{align}f_2(i) & = \begin{cases} 2\phantom{f_0(i-1) + f_1(i-1)} & \text{if $i = 0$} \\ f_2(i-1) + 6 & \text{if $i > 0$} \end{cases}\\ f_1(i) & = \begin{cases} k-1 & \text{if $i = 0$} \\ f_1(i-1) + f_2(i-1)\phantom{2} & \text{if $i > 0$} \end{cases}\\ f(i) = f_0(i) & = \begin{cases} 7 & \text{if $i = 0$} \\ f_0(i-1) + f_1(i-1)\phantom{2} & \text{if $i > 0$} \end{cases}\end{align}$


Одну оптимизацию мы можем видеть напрямую из этих функций, она заключается в том, что значение может быть вычислено за три сложения в цикле
void foo(int m, int k, int *p){  int t0 = 7;  int t1 = k-1;  int t2 = 2;  for (int j = 0; j < m; j++) {    *p++ = t0;    t0 = t0 + t1;    t1 = t1 + t2;    t2 = t2 + 6;  }}

, что является полезной оптимизацией для архитектур, в которых умножение является дорогостоящим. Код такого вида, однако, не является общепринятым, и большинство компиляторов не выполняет такую оптимизацию, но они делают её для более простых случаев, таких как
void foo(int m, int k, int *p){  for (int j = 0; < m; j++)    *p++ = k*j + 7;}

так как конструкции вида k*j+7 являются распространёнными в вычислениях адреса.

Рекуррентные цепи


Громоздко каждый раз писать рекурсивные функции, поэтому функции обычно пишутся в форме $\left\{ \phi_j,\odot_{j+1},f_{j+1}\right \}$. Например:

$\begin{align}f_2(i) & = \begin{cases} 2\phantom{f_0(i-1) + f_1(i-1)} & \text{if $i = 0$} \\ f_2(i-1) + 6 & \text{if $i > 0$} \end{cases} \phantom{xx}\text{is written as $\{2,+,6\}$}\\ f_1(i) & = \begin{cases} k-1 & \text{if $i = 0$} \\ f_1(i-1) + f_2(i-1)\phantom{2} & \text{if $i > 0$} \end{cases} \phantom{xx}\text{is written as $\{k-1,+,f_2\}$}\\ f(i) = f_0(i) & = \begin{cases} 7 & \text{if $i = 0$} \\ f_0(i-1) + f_1(i-1)\phantom{2} & \text{if $i > 0$} \end{cases} \phantom{xx}\text{is written as $\{7,+,f_1\}$}\end{align}$


Эти функции можно объединить в цепочку, и $f(i)$ может быть записана как рекуррентная цепь (chain of recurrences, CR) $\{7,+,\{k-1,+,\{2,+,6\}\}\}$. Внутренние фигурные скобки избыточны, и CR обычно записывается как кортеж $\{7,+,k-1,+,2,+,6\}$.

Построение реккурентных цепей


Рекуррентные цепи строятся путём итераций над кодом и вычисления результирующего CR для каждой операции (или маркирования неизвестным результатом, если мы не можем обработать операцию), используя правила упрощения:

$\begin{align}c * \{\phi_0, +, \phi_1\} & \phantom{xx} \Rightarrow \phantom{xx} \{c * \phi_0, +, c * \phi_1\} \\ \{\phi_0, +, \phi_1\} + \{\psi_0, +, \psi_1\} & \phantom{xx} \Rightarrow \phantom{xx} \{\phi_0 + \psi_0, +, \phi_1 + \psi_1\} \\ \{\phi_0, +, \phi_1\}* \{\psi_0, +, \psi_1\} & \phantom{xx} \Rightarrow \phantom{xx} \{\phi_0 * \psi_0, +, \psi_1 * \{\phi_0, +, \phi_1\} + \phi_1 * \{\psi_0, +, \psi_1\} + \phi_1*\psi_1\} \\ \{\phi_0, +, \phi_1,+,0\} & \phantom{xx} \Rightarrow \phantom{xx} \{\phi_0, +, \phi_1\}\end{align} $


Итак, для цикла в функции sum:
for (int j = 0; j < count; ++j)  result += j*j;

мы начинаем с j для которой известна CR $\{0,+,1\}$ из примера 1. Затем она используется как j*j, когда мы вычисляем result, и мы можем вычислить CR для j*j, используя правила упрощения:

$\begin{align}j*j& = \{0,+,1\} * \{0,+,1\} \\ & = \{0 * 0, +, 1 * \{0, +,1\} + 1 * \{0, +, 1\} + 1*1\} \\ & = \{0, +, 1,+,2\}\end{align}$


Сходные вычисления для result даёт нам CR $\{0,+,0,+,1,+,2\}$ после добавления j*j.

Выполняем оптимизации


Оптимизация выполняется как упрощение по индукции (induction variable simplification), и LLVM преобразует функцию в форму, удобную для анализа и оптимизации
int sum(int count){  int result = 0;  if (count > 0) {    int j = 0;    do {      result = result + j*j;      ++j;    } while (j < count);  }  return result;}

или, как это выглядит в LLVM IR:
define i32 @sum(i32) {%2 = icmp sgt i32 %0, 0br i1 %2, label %3, label %6; <label>:3:br label %8; <label>:4:%5 = phi i32 [ %12, %8 ] br label %6; <label>:6:%7 = phi i32 [ 0, %1 ], [ %5, %4 ] ret i32 %7; <label>:8:%9 = phi i32 [ %13, %8 ], [ 0, %3 ]     ; {0,+,1}%10 = phi i32 [ %12, %8 ], [ 0, %3 ]    ; {0,+,0,+,1,+,2}%11 = mul nsw i32 %9, %9                ; {0,+,1,+,2}%12 = add nuw nsw i32 %11, %10          ; {0,+,1,+,3,+,2}%13 = add nuw nsw i32 %9, 1             ; {1,+,1}%14 = icmp slt i32 %13, %0br i1 %14, label %8, label %4}

Компилятор может видеть, что функция возвращает 0, если count <= 0, иначе возвращает результат цикла loop iteration count-1.
Приятное свойство рекуррентной цепи состоит в том, что легко вычислить значение определённой итерации: если мы знаем CR:$\{\phi_0,+,\phi_1,+,\ldots,+,\phi_n\}$, тогда значение итерации $i$ может быть вычислено как:
\begin{align}f(i) & = \sum_{j=0}^{n}\phi_j{i \choose j} \\ & = \phi_0 + \phi_1i + \phi_2{i(i-1)\over 2!} + \ldots + \phi_n{i(i-1)\cdots(i-n+1)\over n!}\end{align}
Подставляя значения для CR $\{0,+,1,+,3,+,2\}$, описывающие result, получаем

$f(i) = i + {3i(i-1)\over 2} + {i(i-1)(i-2) \over 3}$


Компилятору сейчас нужно подставить код, который вычисляет значение для $i =$ count-1, после цикла
result = count-1 + 3*(count-1)*(count-2)/2 + (count-1)*(count-2)(count-3)/3;

но нужна некоторая осторожность, при вычислениях может потеряться точность (временные значения могут не помещаться в 32-битные целые). Деление целых медленная операция, и мы делаем некоторый трюк с заменой деления на умножение и сдвиги. Результат в LLVM IR
%4 = add i32 %0, -1  %5 = zext i32 %4 to i33  %6 = add i32 %0, -2  %7 = zext i32 %6 to i33  %8 = mul i33 %5, %7  %9 = add i32 %0, -3  %10 = zext i32 %9 to i33  %11 = mul i33 %8, %10  %12 = lshr i33 %11, 1  %13 = trunc i33 %12 to i32  %14 = mul i32 %13, 1431655766  %15 = add i32 %14, %0  %16 = lshr i33 %8, 1  %17 = trunc i33 %16 to i32  %18 = mul i32 %17, 3  %19 = add i32 %15, %18  %20 = add i32 %19, -1

Вставка этого кода делает цикл мёртвым, и позже он удаляется проходом удаления мёртвого кода (dead code elimination), и мы, наконец, получаем код
sum(int):        test    edi, edi        jle     .LBB0_1        lea     eax, [rdi - 1]        lea     ecx, [rdi - 2]        imul    rcx, rax        lea     eax, [rdi - 3]        imul    rax, rcx        shr     rax        imul    eax, eax, 1431655766        add     eax, edi        shr     rcx        lea     ecx, [rcx + 2*rcx]        lea     eax, [rax + rcx]        add     eax, -1        ret.LBB0_1:        xor     eax, eax        ret

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


Эта оптимизация не всегда выгодна. Например,
int sum(int count){  int result = 0;  for (int j = 0; j < count; ++j)    result += j*j*j*j*j*j;  return result;}

вычисляет три 32-битных умножения и одно сложение за цикл, а оптимизированная версия требует шесть 64-битных умножений, пять 32-битных умножений, и другие инструкции (godbolt), и оптимизированная версия выполняется медленнее для малых значений цикла. На маленьких CPU с, например, более дорогостоящим 64-битным умножением, значение числа циклов, при которых оптимизация будет полезна, будет больше, чем на обычных CPU. Для CPU, которые не имеют инструкций для 64-битного умножения, это значение будет ещё больше (godbolt).
Одна проблема с такой оптимизацией заключается в том, что для разработчика сложно заставить компилятор генерировать цикл, если он знает, что большинство значений, используемых в реальности, достаточно малы, чтобы генерация цикла была лучшим выбором. GCC, например, не заменяет финальное значение, если выражение дорогостоящее для вычисления.
/* Do not emit expensive expressions.  The rationale is that   when someone writes a code like   while (n > 45) n -= 45;   he probably knows that n is not large, and does not want it   to be turned into n %= 45.  */|| expression_expensive_p (def))

Если GCC не выполнил оптимизацию, это не баг, это фича.

Литература:


Рекуррентные цепи:
1. Olaf Bachmann, Paul S. Wang, Eugene V. Zima. Chains of recurrences a method to expedite the evaluation of closed-form functions
2. Eugene V. Zima. On computational properties of chains of recurrences
Цикловые оптимизации, использующие рекуррентные цепи:
3. Robert A. van Engelen. Symbolic Evaluation of Chains of Recurrences for Loop Optimization
4. Robert A. van Engelen. Efficient Symbolic Analysis for Optimizing Compilers
Оптимизация деления с использованием инструкций умножения и сдвига:
5. Torbjrn Granlund, Peter L. Montgomery. Division by Invariant Integers using Multiplication
Подробнее..

Разработка стековой виртуальной машины и компилятора под неё (часть II)

04.06.2021 20:14:20 | Автор: admin

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

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

Итак, первый шаг - определиться с типами токенов (интуитивно будем ориентироваться на синтаксис C), символами которые будут отделять токены друг от друга, а также определить структуру для хранения токена (тип, текст токена, длина, строка и столбец в исходном коде).

constexpr char* BLANKS = "\x20\n\t";constexpr char* DELIMETERS = ",;{}[]()=><+-*/&|~^!.";enum class TokenType {NONE = 0, UNKNOWN, IDENTIFIER,CONST_CHAR, CONST_INTEGER, CONST_REAL, CONST_STRING,COMMA, MEMBER_ACCESS, EOS, OP_BRACES, CL_BRACES, OP_BRACKETS, CL_BRACKETS, OP_PARENTHESES, CL_PARENTHESES,BYTE, SHORT, INT, LONG, CHAR, FLOAT, DOUBLE, STRING, IF, ELSE, WHILE, RETURN,ASSIGN, EQUAL, NOT_EQUAL, GREATER, GR_EQUAL, LESS, LS_EQUAL,PLUS, MINUS, MULTIPLY, DIVIDE, AND, OR, XOR, NOT, SHL, SHR,LOGIC_AND, LOGIC_OR, LOGIC_NOT};typedef struct {TokenType type;          char* text;              WORD length;             WORD row;                WORD col;                } Token;

Далее напишем класс для разбора, ключевым методом которого станет parseToTokens(char*). Тут алгоритм простой: идем до разделителя (BLANKS и DELIMETERS), определяем начало и конец токена, классифицируем его и добавляем в вектор (список) токенов. Особые случаи разбора - это отличать целые числа от вещественных, вещественные числа (например, "315.0") отличать от применения оператора доступа к членам структуры/объекта ("obj10.field1"), а также отличать ключевые слова от других идентификаторов.

void VMParser::parseToTokens(const char* sourceCode) {TokenType isNumber = TokenType::UNKNOWN;bool insideString = false;                                         // inside string flagbool isReal = false;                                               // is real number flagsize_t length;                                                     // token length variablechar nextChar;                                                     // next char variablebool blank, delimeter;                                             // blank & delimeter char flagstokens->clear();                                                   // clear tokens vectorrowCounter = 1;                                                    // reset current row counterrowPointer = (char*)sourceCode;                                    // set current row pointer to beginningchar* cursor = (char*)sourceCode;                                  // set cursor to source beginning char* start = cursor;                                              // start new token from cursorchar value = *cursor;                                              // read first char from cursorwhile (value != NULL) {                                            // while not end of stringblank = isBlank(value);                                          // is blank char found?delimeter = isDelimeter(value);                                  // is delimeter found?length = cursor - start;                                         // measure token length    // Diffirentiate real numbers from member access operator '.'isNumber = identifyNumber(start, length - 1);                    // Try to get integer part of real numberisReal = (value=='.' && isNumber==TokenType::CONST_INTEGER);     // Is current token is real numberif ((blank || delimeter) && !insideString && !isReal) {          // if there is token separator                   if (length > 0) pushToken(start, length);                      // if length > 0 push token to vectorif (value == '\n') {                                           // if '\n' found rowCounter++;                                                // increment row counterrowPointer = cursor + 1;                                     // set row beginning pointer}nextChar = *(cursor + 1);                                      // get next char after cursorif (!blank && isDelimeter(nextChar)) {                         // if next char is also delimeterif (pushToken(cursor, 2) == TokenType::UNKNOWN)              // try to push double char delimeter tokenpushToken(cursor, 1);                                      // if not pushed - its single char delimeterelse cursor++;                                               // if double delimeter, increment cursor} else pushToken(cursor, 1);                                   // else push single char delimeterstart = cursor + 1;                                            // calculate next token start pointer}else if (value == '"') insideString = !insideString;             // if '"' char - flip insideString flag else if (insideString && value == '\n') {                        // if '\n' found inside string// TODO warn about parsing error}cursor++;                                                        // increment cursor pointervalue = *cursor;                                                 // read next char}length = cursor - start;                                           // if there is a last tokenif (length > 0) pushToken(start, length);                          // push last token to vector}

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

class VMParser {public:VMParser();~VMParser();void parseToTokens(const char* sourceCode);Token getToken(size_t index);size_t getTokenCount();  private:vector<Token>* tokens;WORD rowCounter;char* rowPointer;  bool isBlank(char value);bool isDelimeter(char value);TokenType pushToken(char* text, size_t length);TokenType getTokenType(char* text, size_t length);TokenType identifyNumber(char* text, size_t length);TokenType identifyKeyword(char* text, size_t length);};

Попробуем используя класс VMParser разобрать строку исходного кода C подобного языка (исходный код бессмысленный, просто набор случайных токенов для проверки разбора):

int main(){    printf ("Wow!");    float a = 365.0 * 10 - 10.0 / 2 + 3;while (1 != 2) {    abc.v1 = 'x';}if (a >= b) return a && b; else a || b; };

В итоге получаем в консоли следующий перечень классифицированных токенов:

Результат разбора исходного кода C подобного языкаРезультат разбора исходного кода C подобного языка

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

Для простоты сначала реализуем компилятор арифметических выражений с целыми числами (приоритет "*" и "/" над "+" и "-" учитывается), без скобок, унарных операций и других важных вещей, в том числе проверки синтаксических ошибок. Разбор выражений напишем вот так:

void VMCompiler::parseExpression(size_t startIndex, VMImage* destImage) {Token tkn;currentToken = startIndex;parseTerm(destImage);tkn = parser->getToken(currentToken);while (tkn.type==TokenType::PLUS || tkn.type==TokenType::MINUS) {  currentToken++;  parseTerm(destImage);  if (tkn.type==TokenType::PLUS) destImage->emit(OP_ADD); else destImage->emit(OP_SUB);  tkn = parser->getToken(currentToken);}}void VMCompiler::parseTerm(VMImage* destImage) {Token tkn;parseFactor(destImage);currentToken++;tkn = parser->getToken(currentToken);while (tkn.type == TokenType::MULTIPLY || tkn.type == TokenType::DIVIDE) {  currentToken++;  parseFactor(destImage);  if (tkn.type == TokenType::MULTIPLY) destImage->emit(OP_MUL); else destImage->emit(OP_DIV);  currentToken++;  tkn = parser->getToken(currentToken);}}void VMCompiler::parseFactor(VMImage* destImage) {Token tkn = parser->getToken(currentToken);char buffer[32];strncpy(buffer, tkn.text, tkn.length);buffer[tkn.length] = 0;destImage->emit(OP_CONST, atoi(buffer));}

Попробуем скормить этому компилятору выражение "3+5*6+2*3+15/5", запускаем компилятор с выводом скомпилированных команд и сразу запускаем виртуальную машину. Ожидаем, что результат вычисления должен остаться на вершине стека - 42.

Ура! Получилось! Первые шаги в сторону компилятора сделаны.

Подробнее..

История портирования Reindexerа как покорить Эльбрус за 11 дней

15.06.2021 18:13:51 | Автор: admin

Всем привет! На связи Антон Баширов, разработчик из ИТ-кластера Ростелекома. Импортозамещение набирает обороты, а российский софт всё глубже проникает в нашу повседневную ИТ-шную сущность бытия. Процессоры Эльбрус и Байкал становятся более востребованными, комьюнити расширяется, но мысли о необходимости портировать весь наш любимый технологический стек на неизведанную архитектуру E2K звучат страшнее рассказов про горящий в пламени production-кластер.

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

Итак, гость в студии база данных Reindexer, разработка нашего ИТ-кластера.

Стоит сказать, почему выбор пал именно на Reindexer, а не другую БД. Во-первых, всеми любимый и известный Postgres уже есть в составе пакетов ОС Эльбрус. Переносить его нет необходимости. Во-вторых, наш гость уже прошел испытания на ARM, следовательно, пришло время ему покорить Эльбрус.

Стоит упомянуть, что Reindexer появился на свет как продуктвыбора между Elastic и Tarantool. Это определенно хочется попробовать завести на отечественном процессоре.

День 0. Знакомство с гостем

Несколько слов про нашего гостя:

Имя:NoSQL in-memory database Reindexer с функционалом полнотекстового поиска
Возраст:на момент испытаний версия БД была 3.1.2
Происхождение:Россия
Место рождения:пытливый ум разработчика Олега Герасимова@olegator99
Место жительства:https://github.com/Restream/reindexer
Место работы:Ростелеком
Строение:основная составляющая C++, имеются примеси из языка Ассемблера и немного CMake
Особенности:- Ассемблерные вставки;
- Много С++11 и C++14;
- Используются корутины.

Деятельность:- Хранение данных в памяти и на диске;
- Выполнение SQL запросов со скоростью 500K queries/sec для PK запросов;
- Выполнение запросов типа full-text search (почти как Elastic, только не тормозит);
- Работа в режиме server и embedded;
- Общение с крутыми парнями: Go, Java, Rust, .NET, Python, C/C++ и (да простит меня Хабр) PHP.

Заскучали? На этом нудная часть закончилась. В эфире рубрика Ээээксперименты!

День 1. А ты думал, в сказку попал?

Для начала попробуем нашего друга собрать из того, что есть.Идем на тестовый сервер Эльбрус 8C с ОС Эльбрус 6.0.1, клонируем туда репозиторий и запускаем CMake.

Хорошие новости, мы нашли компилятор! Новость действительно хорошая, ведь у Эльбруса свой компилятор LCC.

К счастью, создатели Эльбрус сделали LCC полностью совместимым с GCC и наши любимые нативные программки и сборщики смогут чувствовать себя хорошо без особых манипуляций. LCC уже сделал за вас нужные линки:gcc -> /opt/mcst/bin/lcc*.

Зачем Эльбрусу свой компилятор?

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

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

Ключевая идея архитектуры давайте попробуем выполнить несколько операций за один присест. Эльбрусы позволяют реализовать параллельные выполнение кода без применения многопоточности. Достигается это путем одновременного выполнения разных инструкций внутри одной широкой команды на разных устройствах процессора. Но для того, чтобы процессор понимал, что от него хотят и мог правильно распределить инструкции, ему нужно дать на вход оптимизированный поток больших команд. Именно в формировании этого потока и заключается назначение LCC компилировать код так, чтобы широкие команды содержали параллельно выполняемые инструкции с целью задействовать все устройства CPU. Подробнеетут.

Но вернемся к нашему гостю и тому, что не понравилось CMake.

В скриптах сборки, в CMakeLists.txt выполняется определение операционной системы и архитектуры процессора, на которой собирается Reindexer. Нужно это для того, чтобы подключать правильные исходники ассемблера, ведь как говорится Write once, run anywhere.

Разумеется, на момент написания скриптов никто не знал про процессоры Эльбрус, поэтому наш скрипт и упал. Исправляем.

Программисты люди ленивые, поэтому:

Попытка 2:

А что так можно было? На самом деле да для того, чтобы завелся CMake, этого было достаточно. Теперь ударим в бубен и запустимmake -j8:

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

Поэтому, в некоторые места кода Reindexer понадобилось добавить парочку новых условий для__E2K__и__LCC__:

После колдовства с макросами нас ждал монстр пострашнее:

Вот что бывает, когда игнорируешь messages у CMake.

Из-за того, что CMake не нашел подходящих исходников, компилятор не смог найти реализации двух функций:jump_fcontextиmake_fcontext. Про назначение этих функций я расскажу чуть позже, а пока что давайте успокоим нашего гостя и подсунем ему пару пустышек:

Этих операций оказалось достаточно, чтобы Reindexer собрался.

Поздравляю, у вас двойня!

Вот они, наши два долгожданных файла:

# file reindexer_serverreindexer_server: ELF 64-bit LSB executable, MCST Elbrus, version 1 (GNU/Linux
# file reindexer_toolreindexer_tool: ELF 64-bit LSB executable, MCST Elbrus, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux.so.2, for GNU/Linux 2.6.33, with debug_info, not stripped

Это можно считать успехом. Мы смогли собрать наш первый Эльбрус-бинарник. Даже два бинарника: reindexer_server и reindexer_tool.

Подведем итоги. Чтобы собрать Reindexer, мы:

  • Поправили CMake-скрипты;

  • Добавили несколько условий в макросы препроцессора;

  • Заглушили ASM функции.

День 3. Наш Гость, попав на Эльбрус, начал подавать признаки жизни

Первый запуск прошел успешно - сервер поднялся, и даже открылся web-ui.

Я привык не останавливаться на достигнутом и решил немного поработать над Reindexer.

Результат меня порадовал - Гость устал и прилег отдохнуть:

Такое поведение наблюдалось только на Эльбрус. На i5 все работало штатно.

Давайте разбираться. Вооружившись отладчиком (gdb кстати под E2K работает прекрасно) и CLion, мне удалось докопаться до истины.

Пару запросов к Reindexer смогли воспроизвести ошибку:

Падает в деструкторе. Интересный момент - упало на free (в данном случае free реализуется через jemalloc). Видимо, здесь идет высвобождение памяти по некорректному указателю. Ошибку эту я исправлял в два этапа:

  1. work around - дело в том, что QueryEntry лежит в объекте ExpressionTree, в самом классе примечательного я не нашел, поэтому копнул в сторону родителя. Оказалось, что до вызова деструктора был вызван вот такой копирующий конструктор, в котором есть интересный MakeDeepCopy(), реализованный с помощью библиотечкиmpark-variant.

    Подробнее про expression tree рассказываюттут.

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

    Итог - оно заработало.

  2. TODO: Исправление тоже есть, но рассказ про него уже выходит за рамки данной статьи. Небольшой спойлер (код из mpark-variant с патчем под e2k):

inline constexpr DECLTYPE_AUTO visit(Visitor &&visitor, Vs &&... vs)#ifdef E2K //Fix for Elbrus    -> decltype(detail::visitation::variant::visit_value(lib::forward<Visitor>(visitor),                                                 lib::forward<Vs>(vs)...))    {     return detail::visitation::variant::visit_value(lib::forward<Visitor>(visitor),                                                 lib::forward<Vs>(vs)...);    }#else    DECLTYPE_AUTO_RETURN(        (detail::all(             lib::array<bool, sizeof...(Vs)>{{!vs.valueless_by_exception()...}})             ? (void)0             : throw_bad_variant_access()),        detail::visitation::variant::visit_value(lib::forward<Visitor>(visitor),                                                 lib::forward<Vs>(vs)...))#endif

День 5. Он ожил! Ну почти

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

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

Помните, как мы убрали завязку на ASM и функцииmake_fcontext и jump_fcontext?

Так вот, ASM исходники в Reindexer нужны для реализации C++ корутин, а эти функции - ключевые для корутин из библиотеки boost/context.

Кто такая эта ваша Корутина?

Корутина это функция, которая может быть приостановлена и возобновлена в любой момент. Для реализации достаточно трех основных компонентов:
- функция
- механизм для ее паузы (suspend)
- механизм для возобновления ее с той точки, в которой была сделана пауза (resume).

Реализацию корутин можно увидеть в таких языках и библиотеках, как:

  • Libcoro(корутины на C/С++)

  • Koishi(тоже корутины, тоже на С/С++)

  • Boost(и это тоже корутины, тоже на С/С++, корутин много не бывает!)

  • Node-fibers(корутины для NodeJS на базе libcoro)

  • Tarantool(fibers на базе libcoro)

  • Kotlin(свои корутины, не на C++)

  • C++20

  • Goroutine

Для наглядности вот скрин теста корутин из Koishi:

Последовательность следующая:

  1. Создали корутину, выделили стек, задали функцию;

  2. Возобновили корутину - в этот момент вызвалась наша функция;

  3. Приостановили корутину с помощьюkoishi_yield здесь мы вернули управление в test1 сразу после строчкиkoishi_resume;

  4. Если мы еще раз сделаемkoishi_resumeна нашей корутине мы вернемся на строчку функции cofunc1 сразу после первого вызоваkoishi_yield.

Без корутин не будет работать reindexer_tool, но не можем же мы оставить Reindexer без такой важной конечности. Будем исправлять.

Начнем с анализа ситуации. Ожидаемо, что ASM Эльбруса бустовские корутины не поддерживают (пока что), а значит нам нужно реализовать их самим. Подробности и реализацию бустовских корутин можете посмотретьздесь, а сейчас наступило время скрещивать ужа с ежом.

Есть несколько вариантов реализовать корутины:

  • Вариант 1:написать ASM реализацию. Самый очевидный, но при этом самый сложный вариант. У нас есть в планах попробовать именно этот вариант, так как ASM корутины - самые быстрые.

  • Вариант 2: забить. Нет, это не наш путь.

  • Вариант 3: использовать библиотеку.

По счастливой случайности Koishi оказалось той самой библиотекой, поддерживающей реализацию корутин с помощью встроенных E2K функций:makecontext_e2k()иfreecontext_e2k().

Koishi, Koishi, что это вообще такое?

Если коротко это библиотека, предоставляющая удобный API по использованию корутин на C/C++ с несколькими вариантами реализации:

  • ucontext

  • ucontext_e2k (наша прелесть)

  • fcontext

  • win32fiber

  • ucontext_sjlj

  • emscripten

Там, где стандартная медицина бессильна, в дело вступает генная инженерия.

Для начала внедрим Koishi в организм Reindexer:

Заменим больной орган на здоровый:

Стараемся не повредить то, что уже работает:

Одним из backend-ов Koishi для корутин выступает fcontext (те же самые boost исходники, что в Reindexer). Последуем древней мудрости работает не трогай! и оставим как есть в случае, если у нас не E2K архитектура. Для Эльбруса мы будем использоватьucontext_e2k.c

И вот он, наш мутант с корутинами полностью здоров и функционален (и на amd64, и на E2K):

День 11. Проводим финальные испытания

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

Всего в Reindexer около 300 функциональных тестов, и мне не терпится запустить их все.

Тесты бежали, бежали и не добежали. Один из тестов вызвал Segmentation Fault.

Всему виной оказались вот эти строчки:

struct ConnectOpts {  /* тут тоже код, но он не такой интересный */  uint16_t options = 0;  int expectedClusterID = -1;};

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

Попробуем воспроизвести на изолированном более простом примере.

ASM, настало твое время

Внимание на скриншот:

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

Баг заключается в том, что скомпилированное создание структуры выглядит как странныйaddd. Именно эта строчка и вызывает segfault. Кстати, если вместоbool anyField = falseнаписать простоbool anyField, то код совершенно другой и ошибки нет.

Семь бед, один ответ обновитесь!

Разгадка тайны оказалась проста. На момент работы с Reindexer последней версией компилятора LCC была v.1.25.16, однако в состав пакетов ОС Эльбрус по умолчанию входит 1.25.14, на котором я и запускал тесты. Добрые люди из сообщества подсказали нам, что уже в 15 версии данный баг был исправлен. Я решил обновиться на 16-ую и посмотреть, что будет.

Вот что нам принес новый компилятор LCC v.1.25.16:

C++ код такой же, однако ASM совсем другой, и что самое главное он работает! Последовательно (заранее прошу прощения за мой ломаный asm-русский переводчик):

  1. gestp - выделяем стек и кладем его в %dr3

  2. setwd - задаем текущее окно в регистровом файле

  3. setbn - задаем подвижную базу регистров в текущем окне регистрового файла

  4. addd - кладем "дно" стека (стек размером 0x20) в %dr2

  5. addd - выделяем на стеке нашу структуру

  6. ldb - достаем из констант наш false и кладем его в %r5

  7. stb - кладем наш false из регистра %r5 в наше поле anyField1

  8. addd - подготовили локальный указатель на структуру для вызова метода

  9. addd - передаем указатель в базовый регистр для передачи при вызове anyMethod (в anyMethod мы достаем указатель из регистра %dr0)

  10. disp - подготавливаем регистр перехода на anyMethod

  11. call - вызываем anyMethod

Дальше все просто пересобрать, перезапустить, обрадоваться.

Тесты прошли успешно, и теперь у нас есть полностью рабочий и протестированный Reindexer на Эльбрусе.

На этом все ?

На самом деле нет. Сейчас мы смогли:

  • собрать Reindexer Server

  • запустить Reindexer Server

  • собрать Reindexer Tool

  • запустить Reindexer Tool

  • переписать кусочек Reindexer Tool

  • уронить Reindexer и поднять его

  • добиться 100% проходимости Reindexer тестов

Мы научились:

  • собирать C++ софт на Эльбрус

  • переписывать C++ софт под особенности и отличия Эльбрусов

  • разбираться в ASM E2K

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

  • разбираться в C++ корутинах даже на Эльбрусе

Что в планах:

  • корутины на ASM E2K (может быть даже сравнить fcontext ASM на i5 и ucontext/ASM на E2K)

  • оптимизировать Reindexer под архитектуру Эльбрус

  • перенести еще несколько интересных приложений на Эльбрус

  • дополнить базу библиотек и пакетов Эльбрус

  • организовать E2K инфраструктуру и песочницу

Что же можно сказать про Эльбрус со стороны простого разработчика?

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

На сегодня это всё, что хотелось бы вам рассказать, но я еще вернусь с новыми интересными подробностями про портирование и оптимизацию, и да прибудет с вами сила Эльбруса!

Подробнее..

Разработка стековой виртуальной машины и компилятора под неё (часть III)

20.06.2021 10:05:00 | Автор: admin

По ходу разработки генератора кода для виртуальной машины понял, что виртуальная машина не готова к полноценным вызовам функций, с передачей аргументов и хранением локальных переменных функций. Поэтому её необходимо доработать. А именно, нужно определиться с Соглашением о вызовах (calling convention). Есть много разных вариантов, но конечный выбор за разработчиком. Главное - это обеспечить целостность стека, после вызова.

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

На сегодняшний день, наиболее знакомые мне Соглашения о вызове (calling convention), регулирующее правила передачи аргументов функции, очистки стека после вызова, а также логика хранения локальных переменных - это C declaration (cdecl, x86/64) и pascal. Попробую применить эти знания с небольшими модификациями, а именно без прямого доступа программы к регистрам виртуальной машины (она же всё таки стековая, а не регистровая). Итак, логика будет следующая:

Поясню что происходит. Функция main() вызывает функцию sum() и передаёт ей два аргумента - значение переменной i и константное число 10. Осуществляется передача аргументов путём добавления значений аргументов в стек слева направо (как в pascal). После чего осуществляется вызов функции инструкцией виртуальной машины call, которой указывается адрес вызова и количество передаваемых через стек аргументов - 2.

Дальше команда call должна сделать следующие вещи:
1) сохранить в стек адрес возврата после вызова IP (Instruction Pointer + 1)
2) сохранить в стек значение Frame Pointer (регистр виртуальной машины, которым мы показываем до куда очищать стек после вызова).
3) сохраняем в стек значение Locals Pointer (регистр указывающий на место в стеке где начинаются локальные переменные вызывающей функции).
4) выставить значение Frame Pointer на первый аргумент в стеке, чтобы мы знали докуда очищать стек после завершения выполнения функции.
5) выставить значение Locals Pointer на адрес в стеке сразу после сохранных значение IP, FP, LP.

В свою очередь команда ret должна выполнить действия в обратном порядке:
1) Восстановить из стека предыдущие значения IP, FP, LP.
2) Взять результат выполнения функции с вершины стека.
3) Выставить SP = FP (очистить стек в состояние до вызова).
4) Положить на вершину стека результат выполнения функции.

Так как делаем стековую, а не регистровую виртуальную машину, не хочу давать прямой доступ к регистрам, поэтому доработаем инструкцию CALL / RET, а также добавим четыре дополнительные инструкции LOAD (положить в стек значение локальной переменной с указанным индексом), STORE (взять верхнее значение в стеке и сохранить в локальной переменной с указанным индексом), ARG (добавить в стек значение аргумента функции с указанным индексом), а также DROP - инструкция выбросить из стека верхнее значение. Последняя инструкция DROP нужна для функций значение которых нам не нужно, так как мы не даём прямой доступ к регистрам.

case OP_CALL:a = memory[ip++];      // get call address and increment addressb = memory[ip++];      // get arguments count (argc)b = sp + b;            // calculate new frame pointermemory[--sp] = ip;     // push return address to the stackmemory[--sp] = fp;     // push old Frame pointer to stackmemory[--sp] = lp;     // push old Local variables pointer to stackfp = b;                // set Frame pointer to arguments pointerlp = sp - 1;           // set Local variables pointer after top of a stackip = a;                // jump to call addressbreak;case OP_RET:a = memory[sp++];      // read function return value on top of a stackb = lp;                // save Local variables pointersp = fp;               // set stack pointer to Frame pointer (drop locals)lp = memory[b + 1];    // restore old Local variables pointerfp = memory[b + 2];    // restore old Frame pointerip = memory[b + 3];    // set IP to return addressmemory[--sp] = a;      // save return value on top of a stackbreak;case OP_LOAD:a = memory[ip++];         // read local variable indexb = lp - a;               // calculate local variable addressmemory[--sp] = memory[b]; // push local variable to stackbreak;case OP_STORE:a = memory[ip++];         // read local variable indexb = lp - a;               // calculate local variable addressmemory[b] = memory[sp++]; // pop top of stack to local variablebreak;case OP_ARG:a = memory[ip++];         // read parameter indexb = fp - a - 1;           // calculate parameter addressmemory[--sp] = memory[b]; // push parameter to stackbreak;case OP_DROP:                   // pop and drop value from stacksp++;break;

Скомпилируем код представленный на иллюстрации выше чтобы протестировать как работают новые инструкции CALL, RET, LOAD, STORE, ARG (примечание: syscall 0x21 - это распечатка числа с вершины стека в консоль):

Запустим исполнение этого кода с распечаткой состояния виртуальной машины после выполнения каждой инструкции:

[   0]    iconst  5     IP=2 FP=65535 LP=65534 SP=65534 STACK=[5] -> TOP[   2]    iload   #0    IP=4 FP=65535 LP=65534 SP=65533 STACK=[5,5] -> TOP[   4]    idec          IP=5 FP=65535 LP=65534 SP=65533 STACK=[5,4] -> TOP[   5]    idup          IP=6 FP=65535 LP=65534 SP=65532 STACK=[5,4,4] -> TOP[   6]    istore  #0    IP=8 FP=65535 LP=65534 SP=65533 STACK=[4,4] -> TOP[   8]    idup          IP=9 FP=65535 LP=65534 SP=65532 STACK=[4,4,4] -> TOP[   9]    iconst  10    IP=11 FP=65535 LP=65534 SP=65531 STACK=[4,4,4,10] -> TOP[  11]    call [32], 2  IP=32 FP=65533 LP=65527 SP=65528 STACK=[4,4,4,10,14,65535,65534] -> TOP[  32]    iconst  10    IP=34 FP=65533 LP=65527 SP=65527 STACK=[4,4,4,10,14,65535,65534,10] -> TOP[  34]    iarg    #0    IP=36 FP=65533 LP=65527 SP=65526 STACK=[4,4,4,10,14,65535,65534,10,4] -> TOP[  36]    iarg    #1    IP=38 FP=65533 LP=65527 SP=65525 STACK=[4,4,4,10,14,65535,65534,10,4,10] -> TOP[  38]    iadd          IP=39 FP=65533 LP=65527 SP=65526 STACK=[4,4,4,10,14,65535,65534,10,14] -> TOP[  39]    iload   #0    IP=41 FP=65533 LP=65527 SP=65525 STACK=[4,4,4,10,14,65535,65534,10,14,10] -> TOP[  41]    isub          IP=42 FP=65533 LP=65527 SP=65526 STACK=[4,4,4,10,14,65535,65534,10,4] -> TOP[  42]    ret           IP=14 FP=65535 LP=65534 SP=65532 STACK=[4,4,4] -> TOP[  14]    syscall 0x21  IP=16 FP=65535 LP=65534 SP=65533 STACK=[4,4] -> TOP[  16]    iconst  0     IP=18 FP=65535 LP=65534 SP=65532 STACK=[4,4,0] -> TOP[  18]    icmpjg  [2]   IP=2 FP=65535 LP=65534 SP=65534 STACK=[4] -> TOP[   2]    iload   #0    IP=4 FP=65535 LP=65534 SP=65533 STACK=[4,4] -> TOP[   4]    idec          IP=5 FP=65535 LP=65534 SP=65533 STACK=[4,3] -> TOP[   5]    idup          IP=6 FP=65535 LP=65534 SP=65532 STACK=[4,3,3] -> TOP[   6]    istore  #0    IP=8 FP=65535 LP=65534 SP=65533 STACK=[3,3] -> TOP[   8]    idup          IP=9 FP=65535 LP=65534 SP=65532 STACK=[3,3,3] -> TOP[   9]    iconst  10    IP=11 FP=65535 LP=65534 SP=65531 STACK=[3,3,3,10] -> TOP[  11]    call [32], 2  IP=32 FP=65533 LP=65527 SP=65528 STACK=[3,3,3,10,14,65535,65534] -> TOP[  32]    iconst  10    IP=34 FP=65533 LP=65527 SP=65527 STACK=[3,3,3,10,14,65535,65534,10] -> TOP[  34]    iarg    #0    IP=36 FP=65533 LP=65527 SP=65526 STACK=[3,3,3,10,14,65535,65534,10,3] -> TOP[  36]    iarg    #1    IP=38 FP=65533 LP=65527 SP=65525 STACK=[3,3,3,10,14,65535,65534,10,3,10] -> TOP[  38]    iadd          IP=39 FP=65533 LP=65527 SP=65526 STACK=[3,3,3,10,14,65535,65534,10,13] -> TOP[  39]    iload   #0    IP=41 FP=65533 LP=65527 SP=65525 STACK=[3,3,3,10,14,65535,65534,10,13,10] -> TOP[  41]    isub          IP=42 FP=65533 LP=65527 SP=65526 STACK=[3,3,3,10,14,65535,65534,10,3] -> TOP[  42]    ret           IP=14 FP=65535 LP=65534 SP=65532 STACK=[3,3,3] -> TOP[  14]    syscall 0x21  IP=16 FP=65535 LP=65534 SP=65533 STACK=[3,3] -> TOP[  16]    iconst  0     IP=18 FP=65535 LP=65534 SP=65532 STACK=[3,3,0] -> TOP[  18]    icmpjg  [2]   IP=2 FP=65535 LP=65534 SP=65534 STACK=[3] -> TOP[   2]    iload   #0    IP=4 FP=65535 LP=65534 SP=65533 STACK=[3,3] -> TOP[   4]    idec          IP=5 FP=65535 LP=65534 SP=65533 STACK=[3,2] -> TOP[   5]    idup          IP=6 FP=65535 LP=65534 SP=65532 STACK=[3,2,2] -> TOP[   6]    istore  #0    IP=8 FP=65535 LP=65534 SP=65533 STACK=[2,2] -> TOP[   8]    idup          IP=9 FP=65535 LP=65534 SP=65532 STACK=[2,2,2] -> TOP[   9]    iconst  10    IP=11 FP=65535 LP=65534 SP=65531 STACK=[2,2,2,10] -> TOP[  11]    call [32], 2  IP=32 FP=65533 LP=65527 SP=65528 STACK=[2,2,2,10,14,65535,65534] -> TOP[  32]    iconst  10    IP=34 FP=65533 LP=65527 SP=65527 STACK=[2,2,2,10,14,65535,65534,10] -> TOP[  34]    iarg    #0    IP=36 FP=65533 LP=65527 SP=65526 STACK=[2,2,2,10,14,65535,65534,10,2] -> TOP[  36]    iarg    #1    IP=38 FP=65533 LP=65527 SP=65525 STACK=[2,2,2,10,14,65535,65534,10,2,10] -> TOP[  38]    iadd          IP=39 FP=65533 LP=65527 SP=65526 STACK=[2,2,2,10,14,65535,65534,10,12] -> TOP[  39]    iload   #0    IP=41 FP=65533 LP=65527 SP=65525 STACK=[2,2,2,10,14,65535,65534,10,12,10] -> TOP[  41]    isub          IP=42 FP=65533 LP=65527 SP=65526 STACK=[2,2,2,10,14,65535,65534,10,2] -> TOP[  42]    ret           IP=14 FP=65535 LP=65534 SP=65532 STACK=[2,2,2] -> TOP[  14]    syscall 0x21  IP=16 FP=65535 LP=65534 SP=65533 STACK=[2,2] -> TOP[  16]    iconst  0     IP=18 FP=65535 LP=65534 SP=65532 STACK=[2,2,0] -> TOP[  18]    icmpjg  [2]   IP=2 FP=65535 LP=65534 SP=65534 STACK=[2] -> TOP[   2]    iload   #0    IP=4 FP=65535 LP=65534 SP=65533 STACK=[2,2] -> TOP[   4]    idec          IP=5 FP=65535 LP=65534 SP=65533 STACK=[2,1] -> TOP[   5]    idup          IP=6 FP=65535 LP=65534 SP=65532 STACK=[2,1,1] -> TOP[   6]    istore  #0    IP=8 FP=65535 LP=65534 SP=65533 STACK=[1,1] -> TOP[   8]    idup          IP=9 FP=65535 LP=65534 SP=65532 STACK=[1,1,1] -> TOP[   9]    iconst  10    IP=11 FP=65535 LP=65534 SP=65531 STACK=[1,1,1,10] -> TOP[  11]    call [32], 2  IP=32 FP=65533 LP=65527 SP=65528 STACK=[1,1,1,10,14,65535,65534] -> TOP[  32]    iconst  10    IP=34 FP=65533 LP=65527 SP=65527 STACK=[1,1,1,10,14,65535,65534,10] -> TOP[  34]    iarg    #0    IP=36 FP=65533 LP=65527 SP=65526 STACK=[1,1,1,10,14,65535,65534,10,1] -> TOP[  36]    iarg    #1    IP=38 FP=65533 LP=65527 SP=65525 STACK=[1,1,1,10,14,65535,65534,10,1,10] -> TOP[  38]    iadd          IP=39 FP=65533 LP=65527 SP=65526 STACK=[1,1,1,10,14,65535,65534,10,11] -> TOP[  39]    iload   #0    IP=41 FP=65533 LP=65527 SP=65525 STACK=[1,1,1,10,14,65535,65534,10,11,10] -> TOP[  41]    isub          IP=42 FP=65533 LP=65527 SP=65526 STACK=[1,1,1,10,14,65535,65534,10,1] -> TOP[  42]    ret           IP=14 FP=65535 LP=65534 SP=65532 STACK=[1,1,1] -> TOP[  14]    syscall 0x21  IP=16 FP=65535 LP=65534 SP=65533 STACK=[1,1] -> TOP[  16]    iconst  0     IP=18 FP=65535 LP=65534 SP=65532 STACK=[1,1,0] -> TOP[  18]    icmpjg  [2]   IP=2 FP=65535 LP=65534 SP=65534 STACK=[1] -> TOP[   2]    iload   #0    IP=4 FP=65535 LP=65534 SP=65533 STACK=[1,1] -> TOP[   4]    idec          IP=5 FP=65535 LP=65534 SP=65533 STACK=[1,0] -> TOP[   5]    idup          IP=6 FP=65535 LP=65534 SP=65532 STACK=[1,0,0] -> TOP[   6]    istore  #0    IP=8 FP=65535 LP=65534 SP=65533 STACK=[0,0] -> TOP[   8]    idup          IP=9 FP=65535 LP=65534 SP=65532 STACK=[0,0,0] -> TOP[   9]    iconst  10    IP=11 FP=65535 LP=65534 SP=65531 STACK=[0,0,0,10] -> TOP[  11]    call [32], 2  IP=32 FP=65533 LP=65527 SP=65528 STACK=[0,0,0,10,14,65535,65534] -> TOP[  32]    iconst  10    IP=34 FP=65533 LP=65527 SP=65527 STACK=[0,0,0,10,14,65535,65534,10] -> TOP[  34]    iarg    #0    IP=36 FP=65533 LP=65527 SP=65526 STACK=[0,0,0,10,14,65535,65534,10,0] -> TOP[  36]    iarg    #1    IP=38 FP=65533 LP=65527 SP=65525 STACK=[0,0,0,10,14,65535,65534,10,0,10] -> TOP[  38]    iadd          IP=39 FP=65533 LP=65527 SP=65526 STACK=[0,0,0,10,14,65535,65534,10,10] -> TOP[  39]    iload   #0    IP=41 FP=65533 LP=65527 SP=65525 STACK=[0,0,0,10,14,65535,65534,10,10,10] -> TOP[  41]    isub          IP=42 FP=65533 LP=65527 SP=65526 STACK=[0,0,0,10,14,65535,65534,10,0] -> TOP[  42]    ret           IP=14 FP=65535 LP=65534 SP=65532 STACK=[0,0,0] -> TOP[  14]    syscall 0x21  IP=16 FP=65535 LP=65534 SP=65533 STACK=[0,0] -> TOP[  16]    iconst  0     IP=18 FP=65535 LP=65534 SP=65532 STACK=[0,0,0] -> TOP[  18]    icmpjg  [2]   IP=20 FP=65535 LP=65534 SP=65534 STACK=[0] -> TOP[  20]    ---- halt ----IP=21 FP=65535 LP=65534 SP=65534 STACK=[0] -> TOPEXECUTION TIME: 0.620997s

В консоли данная программа выдает следующее

Эти числа в консоли говорят, что вызовы функций, передача аргументов, аллокация локальных переменных, возврат значения и восстановление стека после вызова работают нормально. Значит можно полностью переходить и фокусироваться на разработке компилятора для этой виртуальной машины (AST и генерацию кода). Теперь в виртуальной машине есть всё необходимое.

Ура! Это вдохновляет!

Подробнее..

On commutativity of addition

27.04.2021 22:20:31 | Автор: admin
Does an assembly change, if we write (b + a) instead (a + b)?
Let's check out.

Let's write:
__int128 add1(__int128 a, __int128 b) {    return b + a;}

and compile it with risc-v gcc 8.2.0:

add1(__int128, __int128):
.LFB0:
.cfi_startproc
add a0,a2,a0
sltu a2,a0,a2
add a1,a3,a1
add a1,a2,a1
ret


Now write the following:

__int128 add1(__int128 a, __int128 b) {    return a + b;}

And get:

add1(__int128, __int128):
.LFB0:
.cfi_startproc
mv a5,a0
add a0,a0,a2
sltu a5,a0,a5
add a1,a1,a3
add a1,a5,a1
ret

The difference is obvious.

Now do the same using clang (rv64gc trunk). In both cases we get the same result:
add1(__int128, __int128): # @add1(__int128, __int128)
add a1, a1, a3
add a0, a0, a2
sltu a2, a0, a2
add a1, a1, a2
ret

The result is the same we got from gcc in the first case. Compilers are smart now, but not so smart yet.

Let's try to find out, what happened here and why. Arguments of a function __int128 add1(__int128 a, __int128 b) are passed through registers a0-a3 in the following order: a0 is a low word of a operand, a1 is a high word of a, a2 is a low word of b and a1 is the high word of b. The result is returned in the same order, with a low word in a0 and a high word in a1.

Then high words of two arguments are added and the result is located in a1, and for low words, the result is located in a0. Then the result is compared against a2, i.e. the low word of b operand. It is necessary to find out if an overflow has happened at an adding operation. If an overflow has happened, the result is less than any of the operands. Because the operand in a0 does not exist now, the a2 register is used for comparison. If a0 < a2, the overflow has happened, and a2 is set to 1, and to 0 otherwise. Then this bit is added to the hight word of the result. Now the result is located in (a1, a0).

Completely similar text is generated by Clang (rv32gc trunk) for the 32-bit core, if the function has 64-bit arguments and the result:

long long add1(long long a, long long b) {    return a + b;}

The assembler:
add1(long long, long long): # @add1(long long, long long)
add a1, a1, a3
add a0, a0, a2
sltu a2, a0, a2
add a1, a1, a2
ret

There is absolutely the same code. Unfortunately, a type __int128 is not supported by compilers for 32-bit architecture.

Here there is a slight possibility for the core microarchitecture optimization. Considering the RISC-V architecture standard, a microarchitecture can (but not has to) detect instruction pairs (MULH[[S]U] rdh, rs1, rs2; MUL rdl, rs1, rs2) and (DIV[U] rdq, rs1, rs2; REM[U] rdr, rs1, rs2) to process them as one instruction. Similarly, it is possible to detect the pair (add rdl, rs1, rs2; sltu rdh, rdl, rs1/rs2) and immediately set the overflow bit in the rdh register.
Подробнее..
Категории: C , Assembler , Компиляторы , Llvm , Risc-v , Optomization

Мультиклеточная архитектура тесты и развитие

02.04.2021 18:19:41 | Автор: admin

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

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

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

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

Процессоры могут отличаться количеством ядер, системой команд, разрядностью данных, методами доступа к памяти. Конфигурация процессора может быть гомогенной, если процессор состоит из однотипных ядер. Гетерогенной, если в составе процессора присутствуют ядра различных типов. Частным случаем гомогенной конфигурации является матричная организация, когда 1, 4 или 16 ядер объединены блоком матричных операций.

Ядро в мультиклеточном процессоре это четыре клетки объединенные в единый неделимый блок - мультиклетку. Ядра отличаются разрядностью данных. Младшее ядро использует малоразрядные данные 8i, 16if, 32if, где цифры указывают на разрядность используемых данных, а индексы i, f на их тип (i целочисленные, f с плавающей точкой). Старшее данные 16if, 32if, 64if. Каждый тип ядра имеет два варианта реализации внутренней архитектуры:

  1. Первый предназначен для создания процессоров с низким энергопотреблением и имеет внутренние конвейеры с минимальным количеством ступеней.

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

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

Блоки матричной арифметики также отличаются разрядностью, а именно блок, использующий 16-ти битовые данные с плавающей точкой и блок с данными 32f/64f. Каждый из блоков обеспечивает выполнение операции умножения матрицы. Допустимы три варианта размерности матрицы. При использовании одного ядра: 2х2 (64f); 2х4 (32f); 4х4 (16f). При использовании 4-х ядер: 4х4 (64f); 4х8 (32f); 8х8 (16f). Для 16-ти ядер размерности составят: 8х8 (64f); 8х16 (32f); 16х16 (16f).

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

Максимальную эффективность метод DMC демонстрирует при решениии расчетных задач, которые отличаются сранительно небольшими объемами программ, но при этом выпоняют большой объем вычислений.

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

При создании MultiClet B была проведена множественная оптимизации ядра, что позволило увеличить тактовую частоту ядра до 2 ГГц на топонорме 28 нм и одновременно на 15-20% ускорить исполнения кода на этой частоте. Ускорение было достигнуто за счет сокращения времени прохождения команд по тракту исполнения. В частности было уменьшено время обработки команд в буфере АЛУ и обеспечена передача адреса нового параграфа клеткам по отдельному тракту. Компилятор был обновлен до LLVM 11.0.0, что также дало ускорение скомпилированного кода.

Бенчмарки

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

Данные в таблице, приведенной ниже, будут использоваться для расчетов удельной производительности на Ватт и на мм2 для всех бенчмарков. Характеристики ядра MultiClet B получены по результатам логического синтеза и последующего моделирования.

Процессор

MultiClet B

1 ядро Intel Kaby Lake

Частота

2000 МГц

4500 МГц

Энергопотребление

0.14 W

7 W

Площадь (14 нм)

0.23 мм2

9.5 мм2

CoreMark

CoreMark набор тестов для комплексной оценки производительности микроконтроллеров и центральных процессоров, а также их C-компиляторов. Более подробное его описание и причину использования можно посмотреть в одной из предыдущих статей.

Процессор

MultiClet B, LLVM 11.0.0

1 ядро Intel Kaby Lake

CoreMark, тактов

1 069 416

125 740

CoreMark / с

1870

35788

CoreMark / c / Ватт

13358

5112

CoreMark / c / мм2

8131

3767

DhryStone

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

Процессор

MultiClet B, LLVM 11.0.0

1 ядро Intel Kaby Lake

Dhrystone, тактов

730

87

Dhrystone / с

2 739 726

51 724 137

Dhrystone / c / Ватт

19 569 471

7 389 162

Dhrystone / c / мм2

11 911 852

5 444 646

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

Задача о 8 ферзях

Задача о восьми ферзях широко известная комбинаторная задача по расстановке фигур на шахматной доске. Исходная формулировка: Расставить на стандартной 64-клеточной шахматной доске 8 ферзей так, чтобы ни один из них не находился под боем другого.

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

Процессор

MultiClet B, LLVM 11.0.0

1 ядро Intel Kaby Lake

8 ферзей, тактов

3 510 425

700 389

8 ферзей / с

570

6425

8 ферзей / c / Ватт

4069

917

8 ферзей / c / мм2

2477

676

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

Бинарные деревья, глубина 6

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

Процессор

MultiClet B, LLVM 11.0.0

1 ядро Intel Kaby Lake

Бинарные деревья, тактов

1 166 775

247 511

Бинарные деревья / с

1714

18181

Бинарные деревья / c / Ватт

12243

2597

Бинарные деревья / c / мм2

7452

1913

Множество Мандельброта, размерность 32х32

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

Процессор

MultiClet B, LLVM 11.0.0

1 ядро Intel Kaby Lake

Множество Мандельброта 32х32, тактов

905 430

301 888

Множество Мандельброта / с

2208

14906

Множество Мандельброта / c / Ватт

15777

2129

Множество Мандельброта / c / мм2

9603

1569

Все показанные бенчмарки были скомпилированы компилятором LLVM из исходного кода на С. Компилятор для мультиклеточной архитектуры пока генерирует не самый оптимальный код, что видно по количеству тактов, затрачиваемых на выполнение бенчмарков. Не вдаваясь в разницу между мультиклеточной системой команд и x86, количество инструкций, которые исполняет процессор от Интела для выполнения любого из этих бенчмарков в среднем в 2 и более раза ниже, чем количество инструкций, исполняемых мультиклеточным процессором. Это значит, что у компилятора есть перспектива развития, а также что производительность процессора на данных бенчмарках можно значительно ускорить только за счет оптимизации программного обеспечения.

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

  • Удалены лишние пробросы значений инструкцией load, оставшиеся после phi узлов

  • Вместо использования памяти, передача данных между параграфами сделана через коммутатор, где это возможно

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

  • Объединены некоторые параграфы и/или перенесены некоторые инструкции из следующего параграфа в предыдущий для заполнения простоев, возникающих при переходе с одного параграфа на другой

Эти оптимизации, которые можно внедрить в компилятор и которые требуют разработки, были вручную вписаны в ассемблерный код, что позволило понять какие оптимизации компилятора будут самые эффективные, какие нужно делать в первую очередь, и какую можно ожидать производительность. В итоге, количество тактов уменьшилось с 905,430 до 412,117:

Процессор

MultiClet B, LLVM 11.0.0

MultiClet B, прогноз с оптимизациями

1 ядро Intel Kaby Lake

Множество Мандельброта 32х32, тактов

905 430

412 117

301 888

Множество Мандельброта / с

2208

4852

14906

Множество Мандельброта / c / Ватт

15777

34664

2129

Множество Мандельброта / c / мм2

9603

21099

1569

Полученные данные показывают, что при реализации сугубо последовательных алгоритмов, мультиклеточное ядро по производительности почти эквивалентно одному ядру архитектуры Intel Kaby Lake, но при этом энергопотребление и затраты кремниевых ресурсов получаются в десятки раз меньше.

Ethereum

Ethereum мы тоже используем как бенчмарк, и он очень важен по двум причинам:

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

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

Плата-ускоритель с 16 MultiClet S1

Плата-ускоритель с 16 MultiClet S1

NVIDIA 90HX

NVIDIA RTX 2080 Ti

AMD Radeon RX 5700 XT

AMD Radeon RX Vega 64

Хэшрейт, MHash/s

62

52

86

52.5

51.5

46

TDP

50 W

84 W

320 W

180 W

150 W

200 W

Хэшрейт / TDP

1.24

0.62

0.27

0.29

0.34

0.23

Техпроцесс

7 нм

28 нм

7 нм

12 нм

7 нм

14 нм

Как можно заметить, при сопоставимом хэшрейте энергопотребление в 2-3 раза ниже даже на техпроцессе 28 нм. При переходе на техпроцесс 7 нм энергопотребление сокращается еще сильнее.

Оценка использования мультитклеточной платформы для майнинга проводилась на примере получения криптовалюты Ethereum. Моделирование проводилось для процессора MultiClet S1, состоящего из 16 мультиклеток. Данные по производительности платы получены расчетным путем. Для сравнения была выбрана старшая модель платы компании NVIDIA, которая спроектирована специально для майнинга и выпуск которой намечен на 2021год. Данные по ее планируемой производительности взяты с сайта компании. Результаты показывают, что при близкой производительности, плата использующая процессоры MultiClet S1, по удельным показателям, эффективна более чем в 2 раза по сравнению с NVIDIA 90HX.

Заключение

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

  • Во первых, на аппаратном уровне это оптимизация доступа к памяти, аналогичная той, которая реализована для команд, проходящих через буфер АЛУ.

  • Во вторых, это разработка и реализация алгоритмов оптимизации бэкенда для компилятора LLVM. При выполнении всех схем оптимизации (показанные на примере компиляции генератора множества Мандельброта) производительность мультиклеточного ядра на скомпилированном коде будет практически эквивалентно современному ядру Intel, но при этом удельные характеристики по энергопотреблению и кремниевым ресурсом в будут превосходить его в десятки раз.

Если у вас есть желание попробовать скомпилировать и запустить программы на функциональной модели, SDK есть на сайте Мультиклета.

Благодарим за внимание!

Подробнее..

Перевод Компилятор всё оптимизирует? Ну уж нет

17.06.2021 12:20:28 | Автор: admin
Многие программисты считают, что компиляторы это волшебные чёрные ящики, на вход в которые можно подать хаотичный код, а на выходе получить красивый оптимизированный двоичный файл. Доморощенные философы часто начинают рассуждать о том, какие фишки языка или флаги компилятора следует использовать, чтобы раскрыть всю мощь магии компилятора. Если вы когда-нибудь видели кодовую базу GCC, то и в самом деле могли поверить, что он выполняет какие-то волшебные оптимизации, пришедшие к нам из иных миров.

Тем не менее, если вы проанализируете результаты работы компиляторов, то узнаете, что они не очень-то хорошо справляются с оптимизацией вашего кода. Не потому, что пишущие их люди не знают, как генерировать эффективные команды, а просто потому, что компиляторы способны принимать решения только в очень малой части пространства задач. [В своём докладе Data Oriented Design (2014 год) Майк Эктон сообщил, что в проанализированном фрагменте кода компилятор теоретически может оптимизировать лишь 10% задачи, а 90% он оптимизировать не имеет никакой возможности. Если бы вам интересно было узнать больше о памяти, то стоит прочитать статью What every programmer should know about memory. Если вам любопытно, какое количество тактов тратят конкретные команды процессора, то изучите таблицы команд процессоров]

Чтобы понять, почему волшебные оптимизации компилятора не ускорят ваше ПО, нужно вернуться назад во времени, к той эпохе, когда по Земле ещё бродили динозавры, а процессоры были чрезвычайно медленными. На графике ниже показаны относительные производительности процессоров и памяти в разные годы (1980-2010 гг.). [Информация взята из статьи Pitfalls of object oriented programming Тони Альбрехта (2009 год), слайд 17. Также можно посмотреть его видео
(2017 год) на ту же тему.]


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

  • В 1980 году задержка ОЗУ составляла примерно 1 такт процессора
  • В 2010 году задержка ОЗУ составляла примерно 400 тактов процессора

Ну и что же? Мы тратим чуть больше тактов на загрузку из памяти, но компьютеры всё равно намного быстрее, чем раньше. Какая разница, сколько тактов мы тратим?

Ну, как ни грустно осознавать, но даже если у нас будут более качественные компиляторы и мощное железо, скорость ПО значительно не повысится, потому что ПО такое медленное не из-за них. Главная современная проблема использование процессоров не во всю их мощь.

В таблице ниже указаны параметры задержки самых распространённых операций. [Таблица взята из книги Systems Performance: Enterprise and the cloud (2nd Edition 2020).] В столбце Задержка в масштабе указана задержка в значениях, которые проще понимать людям.

Событие Задержка Задержка в масштабе
1 такт ЦП 0,3 нс 1 с
Доступ к кэшу L1 0,9 нс 3 с
Доступ к кэшу L2 3 нс 10 с
Доступ к кэшу L3 10 нс 33 с
Доступ к основной памяти 100 нс 6 мин
Ввод-вывод SSD 10-100 мкс 9-90 ч
Ввод-вывод жёсткого диска 1-10 мс 1-12 месяцев

Посмотрев на столбец задержек в масштабе, мы быстро поймём, что доступ к памяти затратен, и что в случае подавляющего большинства приложений процессор просто бездельничает, ожидая ответа от памяти. [Узким местом не всегда является память. Если вы записываете или считываете много данных, то узким местом, скорее всего, будет жёсткий диск. Если вы рендерите большой объём данных на экране, то узким местом может стать GPU.]

На то есть две причины:

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

Языки программирования


Язык Время создания
C 1975 год
C++ 1985 год
Python 1989 год
Java 1995 год
Javascript 1995 год
Ruby 1995 год
C# 2002 год

Перечисленные выше языки программирования придуманы более 20 лет назад, и принятые их разработчиками проектные решения, например, глобальная блокировка интерпретатора Python или философия Java всё это объекты, в современном мире неразумны. [Все мы знаем, какой бардак представляет собой C++. И да, успокойтесь, я знаю, что в списке нет вашего любимого нишевого языка, а C# всего 19 лет.] Оборудование подверглось огромным изменениям, у процессоров появились кэши и многоядерность, однако языки программирования по-прежнему основаны на идеях, которые уже не истинны.

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

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

Да, компьютеры чрезвычайно быстры, но только если вы пишете ПО таким образом, что оно хорошо взаимодействует с железом. На одном и том же оборудовании вы может работать и очень плавная 3D-игра и заметно лагающий MS Word. Очевидно, что проблема здесь не в оборудовании и что мы можем выжать из него гораздо больше, чем среднестатистическое приложение.

Совершенствовалось оборудование, но не языки


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

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

Объяснение будет долгим, но давайте начнём с примера:

Представьте, что мы симулируем колонию муравьёв. Административный отдел колонии был уничтожен атакой муравьеда, поэтому он не знает, сколько муравьёв-воинов осталось живо в колонии.

Поможем нашему муравью-администратору посчитать муравьёв-воинов!

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

class Ant {    public String name = "unknownAnt";    public String color = "red";    public boolean isWarrior = false;    public int age = 0;}// shh, it's a tiny ant colonyList<Ant> antColony = new ArrayList<>(100);// fill the colony with ants// count the warrior antslong numOfWarriors = 0;for (Ant ant : antColony) {    if (ant.isWarrior) {         numOfWarriors++;    }}

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

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

  1. Уменьшив объём данных, которые нужно получать для нашей задачи.
  2. Храня необходимые данные в соседних блоках, чтобы полностью использовать строки кэша.

В приведённом выше примере мы будем считать, что из памяти запрашиваются следующие данные (я предполагаю, что используются compressed oops; поправьте меня, если это не так):

+ 4 байта на ссылку имени
+ 4 байта на ссылку цвета
+ 1 байт на флаг воина
+ 3 байта заполнителя
+ 4 байта на integer возраста
+ 8 байт на заголовки класса
---------------------------------
24 байта на каждый экземпляр муравья


Сколько раз нам нужно обратиться к основной памяти, чтобы подсчитать всех муравьёв-воинов (предполагая, что данные колонии муравьёв ещё не загружены в кэш)?

Если учесть, что в современных процессорах строка кэша имеет размер 64 байта, то мы можем получать не больше 2,6 экземпляра муравьёв на строку кэша. Так как этот пример написан на языке Java, в котором всё это объекты, находящиеся где-то в куче, то мы знаем, что экземпляры муравьёв могут находиться в разных строках кэша. [Если распределить все экземпляры одновременно, один за другим, то есть вероятность, что они будут расположены один за другим и в куче, что ускорит итерации. В общем случае лучше всего заранее распределить все данные при запуске, чтобы экземпляры не разбросало по всей куче, однако если вы работаете с managed-языком, то сложно будет понять, что сделают сборщики мусора в фоновом режиме. Например, JVM-разработчики утверждают, что распределение мелких объектов и отмена распределения сразу после их использования обеспечивает бОльшую производительность, чем хранение пула заранее распределённых объектов. Причина этого в принципах работы сборщиков мусора, учитывающих поколения объектов.]

В наихудшем случае экземпляры муравьёв не распределяются один за другим и мы можем получать только по одному экземпляру на каждую строку кэша. Это значит, что для обработки всей колонии муравьёв нужно обратиться к основной памяти 100 раз, и что из каждой полученной строки кэша (64 байта) мы используем только 1 байт. Другими словами, мы отбрасываем 98% полученных данных. Это довольно неэффективный способ пересчёта муравьёв.

Ускоряем работу


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

Мы используем максимально наивный Data Oriented Design. Вместо моделирования муравьёв по отдельности мы смоделируем целую колонию за раз:

class AntColony {    public int size = 0;    public String[] names = new String[100];    public String[] colors = new String[100];    public int[] ages = new int[100];    public boolean[] warriors = new boolean[100];    // I am aware of the fact that this array could be removed    // by splitting the colony in two (warriors, non warriors),    // but that is not the point of this story.    //     // Yes, you can also sort it and enjoy in an additional     // speedup due to branch predictions.}AntColony antColony_do = new AntColony();// fill the colony with ants and update size counter// count the warrior antslong numOfWarriors = 0;for (int i = 0; i < antColony_do.size; i++) {    boolean isWarrior = antColony_do.warriors[i];    if (isWarrior) {        numOfWarriors++;    }}

Эти два примера алгоритмически эквивалентны (O(n)), но ориентированное на данные решение превосходит по производительности объектно-ориентированное. Почему?

Потому что ориентированный на данные пример получает меньше данных, и эти данные запрашиваются соседними блоками мы получаем 64 флагов воинов за раз и не тратим ресурсы впустую. Так как мы получаем только нужные нам данные, нам достаточно обратиться к памяти всего два раза, а не по разу для каждого муравья (100 раз). Это намного более эффективный способ подсчёта муравьёв-воинов.

Я выполнил бенчмарки производительности при помощи тулкита Java Microbenchmark Harness (JMH), их результаты показаны в таблице ниже (измерения выполнялись на Intel i7-7700HQ с частотой 3,80 ГГц). Чтобы не загромождать таблицу, я не указал доверительные интервалы, но вы можете выполнить собственные бенчмарки, скачав и запустив код бенчмарка.

Задача (размер колонии) ООП DOD Ускорение
countWarriors (100) 10 874 045 операций/с 19 314 177 операций/с 78%
countWarriors (1000) 1 147 493 операций/с 1 842 812 операций/с 61%
countWarriors (10000) 102 630 операций/с 185 486 операций/с 81%

Сменив точку зрения на решение задачи, нам удалось добиться огромного ускорения. Стоит также учесть, что в нашем примере класс муравьёв довольно мал, поэтому данные с большой вероятностью останутся в одном из кэшей процессора и разница между двумя вариантами не так заметна (согласно таблице задержек, доступ к памяти из кэша в 30-100 раз быстрее).

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

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

Где есть один, будет несколько.

Майк Эктон

Но постойте! Почему ООП настолько популярно, если имеет такую низкую производительность?

  1. Нагрузка часто зависит от ввода-вывода (по крайней мере, в бэкенде серверов), который примерно в 1000 раз медленнее доступа к памяти. Если вы записываете много данных на жёсткий диск, то улучшения, внесённые в структуру памяти, могут и почти не повлиять на показатели.
  2. Требования к производительности большинства корпоративного ПО чудовищно низки, и с ними справится любой старый код. Это ещё называют синдромом клиент за это не заплатит.
  3. Идеи в нашей отрасли движутся медленно, и сектанты ПО отказываются меняться. Всего 20 лет назад задержки памяти не были особой проблемой, и best practices пока не догнали изменения в оборудовании.
  4. Большинство языков программирования поддерживает такой стиль программирования, а концепцию объектов легко понять.
  5. Ориентированный на данные способ программирования тоже обладает собственным множеством проблем.

Проблемы: допустим, нам нужно найти, сколько в колонии красных муравьёв возрастом старше 1 года. В объектно-ориентированном подходе эта задача решается довольно просто:

long numOfChosenAnts = 0;for (Ant ant : antColony) {    if (ant.age > 1 && "red".equals(ant.color)) {        numOfChosenAnts++;     }}

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

long numOfChosenAnts = 0;for (int i = 0; i < antColony.size; i++) {    int age = antColony.ages[i];    String color = antColony.colors[i];    if (age > 1 && "red".equals(color)) {        numOfChosenAnts++;    }}

А теперь представьте, что кому-то нужно отсортировать всех муравьёв в колонии на основании их имени, а затем что-то сделать с отсортированными данными (например, посчитать всех красных муравьёв из первых 10% отсортированных данных. У муравьёв могут быть странные правила, не судите их строго). При объектно-ориентированном решении мы можем просто использовать функцию сортировки из стандартной библиотеки. При ориентированном на данные способе придётся сортировать массив имён, но в то же самое время сортировать все остальные массивы на основании того, как перемещаются индексы массива имён (мы предполагаем, что нам важно, какие цвет, возраст и флаг воина связаны с именем муравья). [Также можно скопировать массив имён, отсортировать их и найти соответствующее имя в исходном неотсортированном массиве имён, чтобы получить индекс соответствующего элемента. Получив индекс элемента в массиве, можно делать с ним что угодно, но подобные операции поиска выполнять кропотливо. Кроме того, если массивы большие, то такое решение будет довольно медленным. Понимайте свои данные! Также выше не упомянута проблема вставки или удаления элементов в середине массива. При добавлении или удалении элемента из середины массива обычно требуется копировать весь изменённый массив в новое место в памяти. Копирование данных медленный процесс, и если не быть внимательным при копировании данных, может закончиться память. Если порядок элементов в массивах не важен, можно также заменить удалённый элемент последним элементом массива и уменьшить внутренний счётчик, учитывающий количество активных элементов в группе. При переборе таких элементов в этой ситуации мы, по сути, будем перебирать только активную часть группы. Связанный список не является разумным решением этой задачи, потому что данные не расположены в соседних фрагментах, из-за чего перебор оказывается очень медленным (плохое использование кэша).]

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

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

Best practices


Если вы когда-нибудь работали в энтерпрайзе и засовывали нос в его кодовую базу, то, вероятнее всего, видели огромную кучу классов с множественными полями и интерфейсами. Большинство ПО по-прежнему пишут подобным образом, потому что из-за влияния прошлого в таком стиле программирования достаточно легко разобраться. Кроме того, те, кто работает с большими кодовыми базами естественным образом тяготеют к знакомому стилю, который видят каждый день. [См. также On navigating a large codebase]

Для смены концепций подхода к решению задач требуется больше усилий, чем для подражания: Используй const, и компилятор всё оптимизирует! Никто не знает, что сделает компилятор, и никто не проверяет, что он сделал.

Компилятор это инструмент, а не волшебная палочка!

Майк Эктон

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

Если вы хотите больше узнать об этой теме, то прочитайте книгу Data-Oriented Design и остальные ссылки, которые приведены в статье в квадратных скобках.

[БОНУС] Статья, описывающая проблемы объектно-ориентированного программирования:
Data-Oriented Design (Or Why You Might Be Shooting Yourself in The Foot With OOP).
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru