Содержание

1.2. Как исполняется код на Эльбрусе.

Главное преимущество оптимизации кода при помощи компилятора LCC от МЦСТ в сравнении с оптимизацией исполняемого кода внутри конвейера процессора – это то, что компилятор может видеть не маленький участок программы, а большой, вплоть до всей программы (опция -fwhole), и решение по оптимизации он принимает на основе анализа кода программы.

Видео 2. HighLoad Channel – Архитектура процессора Эльбрус 2000 / Дмитрий Завалишин (Digital Zone)

Наиболее понятно об этом на конференции HighLoad++ рассказал Дмитрий Завалишин, специалист из Digital Zone. Советую ознакомиться с его докладом. Здесь же я вкратце резюмирую информацию.

На LCC, компиляторе под Эльбрус, стоит вовсе не простая задача по оптимизации кода под эту платформу. На самом процессоре инструментов, позволяющих ускорить выполнение кода, и которыми, собственно, и оперирует компилятор, просто превеликое множество. Для начала рассмотрим регистры предиката. Что это за регистры такие? Это регистры, хранящие булевое значение (т.е. либо 0, либо 1). Зачем они нужны? На них вешается условие IF/ELSE. В зависимости от значения в регистре предиката, процессор решает, какую часть кода в условии ему надо выполнить.

Покажу пример с псевдокодом. Примерно так бы выглядел код на C.

if (likevideo == true) {print(“krasavchik”);} else {printf(“ti smozhesh, druzhok, ya v tebya veryu”);}

В обычной ситуации, когда процессор подходит к выполнению этого кода, он проверяет, выполнено ли условие like video. Если выполнено, значит говорим krasavchik, а если нет – говорим ti smozhesh, druzhok, ya v tebya veryu. С Эльбрусом ситуация обстоит иначе. Эльбрус, как только выполнено условие like video, кладёт в регистр предиката число 1, и быстро считывает это значение при проверке условия. Т.е. в момент, когда надо определиться, какой код надо выполнить процессору, Эльбрус не проверяет условие, а быстро считывает значение регистра предиката. 1 – выполняем кода, а 0 – не выполняем. Обе команды (и для вывода текста krasavchik, и для вывода текста ti smozhesh, druzhok, ya v tebya veryu) Эльбрус уже подгрузил и готов выполнить, и он не тратит времени на раздумья о том, какой код выполнить, т.к. информация для принятия решения уже хранится в регистре предиката. Так Эльбрус сильно экономит на прыжках (jmp) в коде и повторном анализе соблюдения условий для кода.

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

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

Но это далеко не всё. Теперь мы затронем ещё и вопрос безопасности. Как организована работа с данными в Эльбрусе? Если хотите изучить вопрос в деталях, рекомендую вам прочитать статью от разработчиков операционной системы Embox: «Восхождение на Эльбрус — Разведка боем. Техническая Часть 1. Регистры, стеки и другие технические детали».

Я воспользуюсь частью инфы из статьи. Полностью я её не осилил. В Эльбрусе регистровый файл (используемые регистры) делится на 3 стека:

  • Стек процедур (Procedure Stack — PS).
  • Стек связующей информации (Procedure Chain Stack — PCS).
  • Стек пользователя (User Stack — US).
3 стека организации работы регистров в Эльбрусе

Скриншот 3. 3 стека организации работы регистров в Эльбрусе.

Три аппаратных стека по сравнению с одним в Intel.

Два из них защищены от модификации программистом. Один — chain-стек — отвечает за хранение адресов для возвратов из функций, другой — стек регистров — содержит параметры, через которые они передаются. В третьем — пользовательском стеке — хранятся переменные и данные пользователя. В процессорах Intel все хранится в одном стеке, что порождает уязвимости, так как все адреса переходов, параметров находятся в одном незащищенном от модификаций пользователем месте.

Это определение взял из другой статьи на habr «Портирование JS на Эльбрус», т.к. оно мне показалось более простым для понимания.

Сейчас будет ещё одна грубая аналогия, за которую меня эксперты просто загрызут. Представьте, что вы хотите посмотреть фильм в плеере VLC. Вы открываете программу, в ней открываете фильм, и смотрите. Фильм у вас будет в User Stack, т.е. это данные с которыми вы работаете, в PS stack у вас будет адрес главного окошка VLC, куда надо будет вернуться после окончания воспроизведения видео, а в PCS будет храниться дополнительная информация для того самого возврата из окна с видео в главное окно плеера.

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

Логическая схема внутреннего устройства ядра Эльбруса

Скриншот 4. Логическая схема внутреннего устройства ядра Эльбруса. Источник: Alt Linux.

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

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

Что за циклы? Вот пример с псевдокодом:

for (i = 1; i <= 5; i++) {printf(i);}

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

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

Для таких задач у Эльбруса есть отдельный параллельно работающий модуль для подкачки данных, APB (Asynchronous Prefetch Buffer). Тут я просто процитирую часть документа «Руководство по эффективному программированию на платформе Эльбрус» с сайта МЦСТ.

Для увеличения производительности в случае регулярного доступа к элементам массивов в архитектуре Эльбрус реализован механизм асинхронного доступа к элементам массива. Суть его состоит в следующем: доступ к массивам описывается особым образом в виде кода асинхронной программы. Она состоит только из операций fapb. Операции fapb запускаются по циклу, пополняя буферы упреждающих данных для разных массивов. При этом основной поток исполнения забирает данные из этого буфера операциями mova (вместо запуска операций чтения).

Преимущества, предоставляемые механизмом асинхронного доступа к массивам:

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

Говоря более простым языком, если вы корректно пишете программу, и компилируете её с опцией -fprefetch, включающей предпокачку данных в циклах, у вас задача по подгрузке данных перекладывается с арифметико-логических устройств на APB (Asynchronous Prefetch Buffer). Это экономит сотни тактов, в течение которых иной процессор бы просто ожидал данные.

Но важное уточнение: не во всех задачах этот APB будет эффективен, тем более если вы не оптимизировали код под Эльбрус. Недостаточно просто применить опцию -fprefetch к коду. Это не волшебная палочка на все случаи.

Тест dav1d декодера AV1 видео без опции -fprefetch и с ней. Эльбрус 16С (128 ГБ ОЗУ)

Скриншот 5. Тест dav1d декодера AV1 видео без опции -fprefetch и с ней. Эльбрус 16С (128 ГБ ОЗУ).

Я попробовал собрать декодер AV1 видео dav1d (версия 6aaeeea6 с git) от VideoLAN, разработчиков VLC. Я проверял с тремя разными тестовыми видео Chimera от Netflix, и существенной разницы в пользу варианта с -fprefetch не увидел. Короче говоря, не будет преимуществ от использования этой опции без адаптации кода программы должным образом.

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

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

Что ещё интересного может Эльбрус? Он способен начать работать со следующим циклом ещё до того, как закончился предыдущий. У Эльбруса компилятор разбирает, какие данные в каких циклах от каких других данных зависимы. Если данные независимы друг от друга, он может сам определить, что циклы, в которых ведётся обработка этих данных, надо запускать параллельно. И за счёт огромного числа регистров, т.е. за счёт большого числа данных, с которыми единовременно может оперировать процессор, Эльбрус может параллельно выполнять те операции, которые, программист изначально даже не задумывал считать параллельно. SuperScalar (CISC-> RISC) решает эту задачу у Intel и AMD, а на Эльбрусе её решает компилятор.

Вообще, у Эльбруса, начиная с 16С (E2Kv6) должен был появиться предсказатель переходов внутри самого процессора, однако из-за санкций производство 16С (да и вообще Эльбрусов) сейчас не ведётся. Переезд с тайваньских заводов TSMC на китайские заводы SMIC займёт время. Сейчас наиболее актуальным процессором Эльбрус является 8СВ с архитектурой E2Kv5. Что принципиально нового в E2K v5? Самое главное – это SIMD инструкции для задействования регистров ёмкостью 128 бит, и эти самые регистры на 128 бит. Чем больше объём регистров, тем больший объём данных вычислительные блоки процессора могут обработать за 1 раз.

А зачем вообще нужны эти регистры, когда есть оперативная память? Причина проста: скорость считывания данных с оперативной памяти намного ниже, чем скорость считывания данных с регистров внутри самого процессора. Ваша программа при выполнении может затормозить на несколько сотен тактов только из-за ожидания подгрузки дальнейших данных из оперативной памяти (чего уж говорить про постоянную память). Чтобы программа выполнялась как можно быстрее, нужно грамотно задействовать ту память, что имеется внутри самого процессора. И то, что с E2K v5 добавили регистры на 128 бит и расширения набора инструкций, позволяющие задействовать эти регистры, это огромный плюс.

В Эльбрус 8СВ на базе E2K v5 появились инструкции, рассчитанные на задействование FPU и ускорение векторных вычислений (вспоминаем операции из матриц, примером которых будет фото моего кролика из обзора Macbook Pro на M1). Векторизация – это один из методов распараллеливания кода, и мы его рассмотрим в главе 1.5. В 16С (E2Kv6) появится и аппаратное ускорение виртуализации (в том числе в кодах x86-64).

Там много особенностей в E2K v5 и E2K v6, об особенностях 8СВ и 16С вы можете подробнее прочитать на сайте МЦСТ.

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

Слайд из презентации "Операционная система Эльбрус и микропроцессоры серии Эльбрус в бортовыхсистемах реального времени".

Скриншот 6. Слайд из презентации "Операционная система Эльбрус и микропроцессоры серии Эльбрус в бортовыхсистемах реального времени". Евгений Кравцунов, Константин Трушкин. Презентация загружена с myshared.

У Эльбруса есть возможность компилировать приложения для работы в защищённом режиме. В таком режиме программа не может вылезти за предел тех данных, с которыми ей явно позволили работать. Т.е. если вы сказали программе работать с массивом на 256 бит, а ей попытались подсунуть данные на 257 бит, она просто упадёт. Выдаст ошибку и на том всё. Тут уже речь не о регистрах, не о том, как ведётся работа с данными, 23 переданными программе, а о том, какие данные вы подаёте программе в принципе. Подменить данные и заставить Эльбрус выполнять не то, что надо, в таком случае практически нереально. Мы так получаем защищённость уровня Java в Enterprise на C и C++ коде. Но минуса 2. Первый – нельзя в защищённом режиме запустить ядро Linux, да и в целом далеко не все программы в нём заработают из-за ошибок в коде и причин, описанных в ALT wiki. Защищённый режим требователен к корректности написанного кода, абы какой код не подойдёт. Второй– просадка производительности в защищённом режиме. Зависит от задачи: может, её не будет, а может, она составит 20%-80%. Хоть аппаратная реализация защищённого режима и круче программной, но местами лучше уже просто на Java код писать.

Тема сложная, я постарался подать только ту информацию, которую сам понял, и которую более-менее на простых примерах можно пояснить людям. Я опустил много деталей, например, то, что у Эльбруса имеется ещё 256 регистров по 80 бит, что размер самой команды может составлять до 512 бит (минимум – 32). Я ничего не писал про регистровые окна и их смещение при вызове функций. Много подробностей есть, которые я, к сожалению, не осилю описывать, т.к. чтобы это вам пояснить, я должен сам разбираться в теме достаточно глубоко. Если вы хотите детальнее разобраться в том, как на Эльбрусе пашет код, могу посоветовать ознакомиться с этими материалами:

  • Микропроцессоры и вычислительные комплексы семейства «Эльбрус» – Ким А. К., Перекатов В. И., Ермаков С. Г., 2013.
  • Параллелизм вычислительных процессов и развитие архитектуры суперЭВМ. МВК "Эльбрус" – В.С. Бурцев, 1998.
  • Научно-технический отчет по внутреннему ОКР “Защищенный режим”. Внедрение технологии защищенного режима исполнения Эльбрус в задаче разработки и отладки СПО промышленных контроллеров – Мустафин Т. Р., Алехин А. И., Черкашин С. Ю., Зубов И. Н., Лубинец М. И., Кравцунов Е.М, 2017.
  • Руководство по эксплуатации. ТВГИ.431281.028РЭ. Часть 1. Глава 2.1.3.