Шифр “Кузнечик” на ассемблере arm64/AArch64 со 128-битными инструкциями

Добавил arm64 в свою реализацию шифра “Кузнечик” (ГОСТ Р 34.12-2015) на ассемблере для Go. Новые функции – для платформы arm64 со 128-битной арифметикой. Попутно немного изменил состав библиотеки, так что название теперь новое (см. исходники ниже). Раньше ассемблер был только для amd64 с AVX, на остальных платформах – компилировалось с “заглушкой”, в которой операции шифра реализованы просто на конструкциях Go – ассемблер, понятно, быстрее.

ARM – весьма и весьма распространённая архитектура, а использование ассемблера, как обычно, даёт прирост производительности во много раз, если сравнивать с “обычным” Go-шным вариантом (тоже оптимизированным, конечно). Однако я всё проверял на Raspberry Pi4 (RPi4), потому что другого варианта ARM-аппаратуры под рукой сейчас не оказалось (а техники Apple у меня вообще нет). Зато на Linux/arm64 в RPi4 – прирост более чем в двадцать раз. Было: примерно мегабайт в секунду для “обычной” реализации. Стало: около 23 мегабайтов в секунду для ассемблерной. Думаю, что должно работать и на других ARM-платформах, и гораздо быстрее, чем на RPi4, но пока не проверял: если кто-то использует, и обнаружит существенные несовместимости (см. ниже) – пишите, пожалуйста, в комментарии здесь или пишите почтой. Вообще, написанный ARM-код ещё есть куда оптимизировать: например, в arm64 имеется “векторная” инструкция, реализующая XOR сразу для трёх входных регистров, что тут очень полезно, но эта инструкция недоступна в процессоре RPi4 (Cortex-A72/BCM2711).

Новая версия кода “Кузнечика”, так как отличается от предыдущей, называется GOSThp. Исходный код публикую ниже. Возможно, потом оформлю в виде полноценного модуля для Golang и выложу копию на Github, а возможно – не оформлю и не выложу. Но использовать в составе проекта на Go этот пакет нетрудно: достаточно скопировать директорию gosthp (см. ещё детали ниже). Работает со старыми версиями Go (проверено для 1.7).

Файлы:

gosthp.tar.gz – всё вместе в одном архиве.
SHA-256: b3f860f15d43814db2f2becfb4c72272de6d3b49f9b0dec70712438af0a6e085
(Update, 20/01/2024: архив перезагружен – изменено описание платформ в +build, даты в комментариях, и даты создания файлов; код – без изменений.)

Внутри архива:

cmd/
	| gostrun.go		- файл с примерами и тестами.
gosthp/	- это сама библиотека.
	| docipher_amd64.go	- объявления функций для amd64.
	| docipher_amd64.s	- код на ассемблере для amd64.
	| docipher_arm64.go	- объявления функций для arm64.
	| docipher_arm64.s	- код на ассемблере для arm64.
	| docipher.go		- универсальная реализация для прочих платформ (см. +build внутри: могут быть расхождения).
	| gosthp.go		- основной модуль.
go.mod
README

Основное дополнение – docipher_arm64.s, собственно, реализация шифра для архитектуры arm64/AArch64.

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

$ cd cmd
$ go build -o ~/my-builds/gostrun gostrun.go

Go самостоятельно выберет нужные файлы в соответствии с целевой платформой. Внутри исходников – много комментариев (англ.).
Саму библиотеку можно использовать в среде Go в составе других проектов простым копированием gosthp/, разместив её либо в директорию сборки, либо в общую директорию для пакетов.

Ассемблерная реализация для ARM от x86 отличается в некоторых деталях, не алгоритмических: во-первых, очевидно, другие инструкции; во-вторых, используется больше регистров и основные операции XOR собраны в общий блок; в-третьих, изменён способ вычисления смещений для обращения к таблице значений.

Screenshot

Как обычно, основная оптимизация – использование предвычисленной таблицы. На ассемблере написаны только низкоуровневые операции шифра, то есть, преобразование блока с таблицей и раундовыми ключами. Библиотека позволяет создать экземпляр объекта, который реализует интерфейсы для доступа к шифру. Таблица, ускоряющая операции, общая для всех экземпляров, эта таблица создаётся при инициализации модуля, а потом используется процедурами, реализующими шифр, только для чтения. Строго говоря, таблиц там несколько: есть “обратная” таблица для расшифрования и несколько вспомогательных (см. исходный код). Так как значения в таблицах не зависят от конкретного ключа, то их использование не требует повторного вычисления значений. В принципе, можно было бы просто записать эти таблицы в код в виде статического массива, так часто делают, но можно и генерировать программно, что делает код менее громоздким, а на производительность никак не влияет.

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

Что касается совместимости. И amd64, и arm64 – достаточно широкие понятия. В этой реализации шифра “Кузнечик” используются регистры и команды для разрядности 128-бит, которая соответствует разрядности шифра – в этом, собственно, смысл. Уже использование команд “длинной” арифметики из SSE4.1 для amd64 означает, что некоторые старые процессоры, полностью попадающие в amd64, не смогут, тем не менее, исполнять скомпилированный код, поскольку не поддерживают нужных команд. То же самое относится и к arm64 (например, выше я уже упомянул, что не использую “трёхрегистровую” команду XOR по этой же причине). Данная библиотека поддержку команд не проверяет, так что, вообще говоря, это нужно отслеживать отдельно, используя механизмы детектирования свойств аппаратуры, и, соответственно, либо заменять вызываемые функции, либо выводить предупреждение пользователю.

Адрес записки: https://dxdt.ru/2024/01/02/12066/

Похожие записки:



Далее - мнения и дискуссии

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

Комментарии читателей блога: 7

  • 1. 2nd January 2024, 23:30 // Читатель vlad написал:

    Очередные странности в алгоритмах ГОСТ Кузнечик и Стрибог
    https://habr.com/ru/companies/virgilsecurity/articles/439788/

  • 2. 3rd January 2024, 11:54 // Александр Венедюхин:

    Это, всё же, ещё за 2019 год сообщение, про TKlog.

  • 3. 24th January 2024, 12:46 // Читатель ostapru написал:

    Я некоторое время назад переписывал некоторый код для SSE/AVX и для себя выяснил, что сейчас писать не асме, особенно для intel x86/x64 – гиблое дело. Слишком уж сложна оптимизация с учётом внутреннего паралеллизма процессора. Особенно если нужны версии под разные поколения intel/amd.
    В С++ есть встроенные макро/функции ассемблерных команд compiler intrinsics, которые сильно облегчают переписывание старого кода на SSE/AVX без прямого использования ассемблера. В крайнем случае можно использовать мелкие ассемблерные вставки прямо в код.
    Преимущества:
    1. Даёт возможность проявить себя оптимизатору компилятора. Беглыи взглядом на картинку с кодом в статье 19го года сразу увидел пару мест где оптимизатор бы поработал – когда регистр с результатом тут же используется в следующей команде. Но, естественно, это нужно втьюнить.
    2. Не нужно заморачиваться ассемблером для не SSE/AVX кода, что сильно улучшает читабельность и поддерживаемсть этого кода.
    3. Возможность перекомпиляции с оптимизацией под разные поколения процессоров.
    4. Возможность перекомпиляции с небольшими изменениями под разные поколения SSE/AVX.

    Так вот, для golang вроде есть подобные макро: https://dave.cheney.net/2019/08/20/go-compiler-intrinsics

  • 4. 24th January 2024, 13:01 // Читатель ostapru написал:

    Пример где смешивается SSE макро и ассемблерная вставка:
    https://github.com/Bendr0id/cryptonight-multi-miner/blob/9fe7da31c54cb7d54f833e4aadcf9b4da97a8102/cryptonight.c#L377

  • 5. 24th January 2024, 21:03 // Александр Венедюхин:

    Согласен, что в большинстве случаев развитый современный компилятор справляется весьма и весьма неплохо, обычно – лучше, чем человек с ассемблером. Поэтому ассемблер имеет смысл довольно редко, да. Однако компилятор, так сказать, не знает алгоритмов, поэтому остаются случаи, когда “ручной” ассемблер всё же очень и очень помогает. Intrinsic-и, как компромиссный вариант для C, это неплохо, особенно, если учитывать все “платформенные” особенности C/C++. C вообще от ассемблера не так далеко ушёл, как хотелось бы. Для Go – ну, как-то уже не так привлекательно выглядит идея, на мой вкус.

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

  • 6. 24th January 2024, 23:25 // Читатель ostapru написал:

    Может тогда на С++ модуль переписать? ;)

  • 7. 24th January 2024, 23:38 // Александр Венедюхин:

    Не, это вряд ли.

Написать комментарий

Ваш комментарий:

Введите ключевое слово "GS7G9" латиницей СПРАВА НАЛЕВО (<--) без кавычек: (это необходимо для защиты от спама).

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