Содержание

1.5. Intel Intrinsics на Эльбрусе? Чего, мать?

Из того, что я понял, Эльбрус – это чистая числодробилка, которая может производить много операций за раз, но в ряде случаев его производительность не велика. Что это за ряд случаев? Это когда компилятор не справляется с оптимизацией кода. Например, Эльбрус может простаивать из-за большого числа переходов в коде (jmp). К сожалению, компилятор – это не волшебный инструмент, он не поможет вам исправить всю кривизну кода и заставить его летать на любом железе. Компилятор не весь код оптимизирует, а только тот, что может. Компилятор может при необходимости даже автоматически векторизировать код (ускорить работу с матрицами за счёт векторизации), и задействовать аналоги SSE/AVX. Но автоматически компилятор вовсе не каждый код сможет оптимизировать.

И, как вы понимаете, вручную оптимизировать код под Эльбрус также можно и нужно. Если вы думаете, что польза от этого будет минимальной, что ж, вы глубоко заблуждаетесь. Для примера, на GitHub можете найти репозиторий с патчами софта под E2K. В чём суть этих патчей? Чтобы понять это, можете взглянуть на изменения для файла benchmark-1.5.2-e2k.patch.

История изменений файла benchmark-1.5.2-e2k.patch в репозитории e2k-ports.

Скриншот 9. История изменений файла benchmark-1.5.2-e2k.patch в репозитории e2k-ports.

Что же там происходит? Магия, на Эльбрусе задействуется код специально под Intel, который даёт нехилый прирост производительности.

В общем-то, оказалось, что оптимизации под процессоры Intel будут оптимизациями и для процессоров Эльбрус. Зачем использовать медленный не оптимизированный базовый код на C для всех процессоров, если можно задействовать хорошо выглаженный оптимизированный код под Intel?

Стоп, но какого лешего это вообще работает? Разве этот код не уникален для процессоров Intel?

Оказалось, компилятор LCC умеет для низкоуровневого кода под процессоры Intel находить аналогичные команды для E2K и подставлять их при компиляции. Представьте, что вы пишете на Ассемблере для одного процессора, а потом ваш код каким-то боком начинает работать и на другом процессоре, про Ассемблер которого вы даже не слышали. Всё не настолько удивительно, но оно работает. А как? Что ж.

И тут мы начинаем тему Intel Intrinsics.

И, откуда бы вы думали, я возьму инфу? Ну, конечно же, источником послужит статья с блога компании Intel на habr. Я думаю, если ребята из Intel прочтут мою статью, они просто завопят с моей наглости, позволяющей мне использовать их статьи для объяснения того, как отлаженный под Intel код адаптируют МЦСТ под Эльбрус. Ну, ладно, в чём тут суть?

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

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

Скалярные операции, обрабатывающие по паре операндов, заменяются на операции над массивами (векторами), обрабатывающие несколько элементов вектора в каждый момент времени.

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

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

Краткий ликбез: скалярные операции – это операции с простыми числами, будь то целые числа или числа с плавающей запятой (но не массив).

Итак, если для вас это было сложное определение, я постараюсь ещё проще пояснить: вы преобразуете типичные операции, которые можно ускорить, если считать их как операции с массивами, в эти самые массивы. Я ещё в Кремниевых Секретах Apple M1 (обзоре на Macbook Pro) рассказывал вам вкратце о том, зачем нужны AVX-инструкции. Так вот, фишка в том, что можно и без использования отдельных инструкций добиться повышения производительности за счёт выполнения операций не с отдельными числами, а сразу с большими массивами данных / матрицами. Те. на обычном языке C всё это пишете без SSE и AVX. Вы так тоже можете провести векторизацию и за счёт автоматических оптимизаций компилятора ускорить вычисления.

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

А теперь я позаимствую изображение из той статьи с habr.

Схематическое изображение вариантов векторизации кода.

Скриншот 10. Схематическое изображение вариантов векторизации кода. Источник: статья с habr.

Как вы видите, автовекторизация – это самый просто способ векторизации, который во многом полагается на корректность обработки вашего кода компилятором. А самый сложный вариант – написание кода на чистом Ассемблере. Сильно проще будет написание программ на C или C++ с использованием C кода, который вызывает Ассемблерные функции. Т.е. фронтэнд (то, на чём вы пишете) у вас это язык C с некоторыми нюансами, но за этим фронтом скрывается настоящий Ассемблерный бэкенд.

И вот оно настоящее безумие: Intel Intrinsics, за которым должен скрываться Ассемблер под Intel x86, компилятор LCC от МЦСТ умеет «переваривать» в аналогичный код от МЦСТ. И это, мать вашу, шок. Оптимизации под Intel работают на Эльбрусе и обеспечивают значительный прирост производительности! В идеале, конечно, лучше оптимизировать код конкретно под Эльбрус, но на худой конец и Intel Intrinsics ускорят ваш код.

double A[1000], B[1000], C[1000], D[1000], E[1000];
for (int i = 0; i < 1000; i++)
  E[i] = (A[i] < B[i]) ? C[i] : D[i];

Код, который может векторизировать компилятор. Источник: статья из блога Intel на habr.

Выше вы видите нормальный код на C/C++, который может векторизировать компилятор.

double A[1000], B[1000], C[1000], D[1000], E[1000];for (int i = 0; i < 1000; i += 2) {__m128d a = _mm_load_pd(&A[i]);__m128d b = _mm_load_pd(&B[i]);__m128d c = _mm_load_pd(&C[i]);__m128d d = _mm_load_pd(&D[i]);__m128d e;__m128d mask = _mm_cmplt_pd(a, b);e = _mm_or_pd(_mm_and_pd (mask, c),_mm_andnot_pd(mask, d));_mm_store_pd(&E[i], e);}

Код на C/C++ с использованием Intel Intrinsics на 128 бит. Источник: статья из блога Intel на habr.

А вот как выглядит код на C/C++ с использованием Intel Intrinsics функций на 128 бит, которые, в общем-то, делают то же самое.

#include <immintrin.h>double A[100], B[100], C[100];for (int i = 0; i < 100; i += 4) {__m256d a = _mm256_load_pd(&A[i]);__m256d b = _mm256_load_pd(&B[i]);__m256d c = _mm256_add_pd(a, b);_mm256_store_pd(&C[i], c);}

Другой код на C/C++ с использованием AVX Intel Intrinsics. Источник: статья из блога Intel на habr.

Здесь уже показан другой код, тут работа ведётся с другими массивами, но пример примечателен тем, что используются уже AVX-инструкции при помощи Intel Intrinsics. Как видите, тут чётко указывается, для хранения данных, выделяются регистры объёмом 256 бит, что и нужно AVX. На предыдущих скриншотах был лишь SSE, т.к. под него хватит и 128 бит.

Т.е. мы не пишем на самом Ассемблере, но вызываем Ассемблерные x86 функции из C кода при помощи этих самых Intel Intrinsics.

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

И это просто безумие, что это всё чудо работает на Эльбрусе. Да и не просто работает, а даёт огромный прирост по производительности.

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

А теперь лучше сядьте. Если сидите, лягте. На Эльбрус можно собрать из исходного кода игру The Dark Mod с использованием Intel Intrinsics. Игры можно собирать на Эльбрусе не только с SSE интринсиками, но даже с AVX интринсиками! Все инструкции для сборки Open-Source игр есть на сайте Альт Линукс, на странице «How-to compile games on e2k» (да, есть целая страница, посвящённая сборке игр под Эльбрус, и за её наполнение большая благодарность Рамилю и Дмитрию с YouTube-канала Elbrus PC Test).

И тут вы можете задаться вопросом: «стоп. А каким образом Эльбрус может исполнять AVX код. Ладно ещё 8СВ поддерживает SIMD на 128 бит, но как на Эльбрусе может работать аналоги AVX, которому нужны 256 бит регистры? Их же нет ни в одном из поколения Эльбрус. Да ещё и 128 бит интринсики на Эльбрус-8С без 128 бит регистров? Дядь, ты в порядке?».

Сейчас всё поясню. Компилятор LCC от МЦСТ неспроста считается самым сложным компилятором в мире: он может даже имитировать наличие регистров под AVX и SSE, задействуя 2 или 4 маленьких регистра по 64-128 бит вместо одного реального на 128-256 бит. Поскольку на Эльбрус 8С регистры объёмом до 64 бит (128 бит завезли только в 8СВ), компилятор просто задействует 2 таких регистра для имитации одного регистра, используемого при работе с SSE инструкциями (ну, понятно, 128/64 = 2). А для AVX он задействует 4 таких регистра (256/64 = 4).

Работают приложения в таком случае медленно, т.к. мы используем таким образом в 2 или в 4 раза меньше регистров, чем могли бы. Но, мать вашу, я потрясён, что они таким образом вообще работают. Чудеса, не иначе.

И даже на стареньком Эльбрус-8С пашут SSE инструкции, хотя и не должны вовсе с его 64 бит регистрами. У Эльбрус-8СВ всё должно быть с этим сильно лучше, т.к. у него имеются родные регистры на 128 бит.

Мне писали, что и ARM NEON интринсики пашут, но я не проверял.

Я просто сразу уточню: Intel интринсики не создавались для Эльбруса, и то, компилятор у Эльбруса может обрабатывать их, не значит, что они для Эльбруса – ультимативное решение.

В идеале, понятное дело, оптимизируйте код сразу под Эльбрус,

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

Теперь вопрос с языками JavaScript и Python: к сожалению, в случае с ними компилятор уже никак не поможет, т.к. это интерпретируемые языки. Они не задействуют ни SSE, ни, тем более, AVX, и у них под капотом куча условий if и куча циклов, и при таком раскладе ускорить их крайне сложно программно. Для них требуется аппаратный оптимизатор кода, тут то и нужен предсказатель ветвлений, который собираются добавить с выходом процессоров на базе более новой версии микроархитектуры E2Kv7 (первым процессором на базе этой микроархитектуры может стать Эльбрус-32С в 2025 году). С интерпретируемыми языками всё далеко не так гладко обстоит, как с компилируемыми, поэтому остаётся надеяться только на Эльбрус-32С.

И, разумеется, встаёт ещё вопрос: а как же правильно проводить оптимизацию ПО под Эльбрус? Ну, понятно, что Intel интринсики пашут, но родная оптимизация под Эльбрус как производится то? Попробуем разобраться.