Как стать автором
Обновить
0
Миландр
Разрабатываем микросхемы, приборы, ПО

По пути к вершине Эльбруса: Кузнечик на DSP

Время на прочтение6 мин
Количество просмотров6K

C интересом прочитал статью о впечатляющих показателях процессора Эльбрус на алгоритме шифрования Кузнечик. В этой же статье приведена ссылка на проект с реализацией указанного алгоритма. Вот мне и захотелось посмотреть, как пойдет реализация этого алгоритма шифрования на сигнальном процессоре К1967ВН44(28) , с которым мне часто приходится работать. 

Шаг за шагом

DSP серии К1967ВНхх имеют собственную среду разработки CM-LYNX , компилятор С и С++ на базе Clang. Этого набора достаточно чтобы попробовать сделать оценку производительности процессора на указанной выше реализации алгоритма . В архиве проекта два файла: в одном 8-битная версия алгоритма, а в другом 128-битная версия, т.е. вариант для процессоров поддерживающих операции со 128-разрядными числами. 

Для полноты эксперимента, начинаю с 8-битной версии. После компиляции и запуска на отладочной плате К1967ВН44, при максимально возможном уровне оптимизации –О2, получаю результат

Self-test OK!

kuz_encrypt_block(): 54.804 kB/s (n=200kB,t=3.649s)

kuz_decrypt_block(): 52.435 kB/s (n=200kB,t=3.814s) 

Программа информирует , что тест самопроверки прошел успешно , а затем производит замер скорости шифрования-дешифрования. По умолчанию, в инструментальном софте для платы К1967ВН44 используется определение частоты процессора 250 МГц. Для этой частоты и произведены вычисления.  

Чтобы понять, что это за уровень скорости 54.804 kB/s, приведу аналогичный показатель последовательной обработки для процессора Эльбрус(8СВ) - 150 мегабайт в секунду на одном ядре. До Эльбруса нужно еще ускориться где-то в 3 000 раз. 

Хотя 8-битовый вариант и не был для меня особо интересен, но я все же попробовал его немного улучшить. Для функции uint8_t kuz_mul_gf256(uint8_t x, uint8_t y) я сделал подстановку, создав заранее вычисленный массив результатов размером 64К байт. С новым вариантом получен результат

kuz_encrypt_block(): 161.728 kB/s (n=400kB,t=2.473s)

kuz_decrypt_block(): 185.177 kB/s (n=400kB,t=2.160s) 

Замена оказала хорошее влияние на производительность. Ускорение более чем в 3 раза, но нужно еще в 1000 раз. 

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

Создаю новый проект и копирую в него 128-разрядную версию алгоритма шифрования. Исходный проект отлаживался на архитектуре х86, поэтому я заменяю тип данных __m128i , просто добавив в проект переопределение 

#define __m128i __builtin_quad

Все замечательно компилируется. Запускаю проект на исполнение, вначале без оптимизации, а затем с оптимизацией О2 , и получаю результат 

kuz_encrypt_block(): 1027.337 kB/s (n=3200kB,t=3.115s)

kuz_decrypt_block(): 999.474 kB/s (n=3200kB,t=3.202s) 

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

Если на 8-битной реализации переход в оптимизации от –О0 к –О2 дал ускорение в 11 раз, то на 128-разрядном варианте была получена всего 30% прибавка. Объяснение данного феномена я нахожу только в том, что работа с 128-разрядными данными в компиляторе СМ-LYNX очень слабо оптимизируется. Получается, что дальнейшее ускорение невозможно без использования ассемблера. 

 Удобство 128-разрядной реализации Кузнечика базового проекта в том, что функции кодирования и декодирования очень просты в Си-реализации. Это позволяет очень быстро сделать их ассемблерные варианты. Добавляю в проект опцию, которая меняет вызов Си - функций на их ассемблерные эквиваленты и получаю новые результаты

kuz_encrypt_block(): 14679.652 kB/s (n=51200kB,t=3.488s)

kuz_decrypt_block(): 13535.172 kB/s (n=51200kB,t=3.783s) 

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

Несколько слов 

об архитектуре процессора и деталях ассемблерного варианта на примере реализации процесса шифрования . 

Процессор ВН44 имеет VLIW архитектуру и может выполнять в одном такте до 4-х инструкций. Обработка 128-разрядных данных в процессоре может выполняться в модулях Х и Y (смотрите рисунок ниже), которые могут работать как независимо (каждый под управлением собственных команд), так и синхронно (под управлением общих команд).

Если, например, у Эльбруса информация находится в одном большом наборе регистров, то у ВН44 каждый модуль обработки имеет индивидуальный набор регистров 32х32 бита. Для данного алгоритма модуль SIMD обработки можно рассматривать как два 64-разрядных модуля (X+Y) которые могут управляться одной общей командой. Именно спарка этих двух модулей и единая команда управления, позволяют рассматривать их как единый 128-разрядный модуль обработки.

Реализацию функции void kuz_encrypt_block(kuz_key_t *key, void *blk) на Си можно посмотреть в выше упомянутом проекте. Это всего лишь цикл, повторяющийся 9 раз, в котором выполняется операция исключающего ИЛИ для 16-ти 128-разрядных значений. Проблема только в том, что каждый новый виток цикла требует вычисления индексов этих 128-разрядных операндов на основании результата предыдущего витка. Возможно, реализация этой функции шифрования на ассемблере будет и сложна для человека не знакомого с К1967ВН44, но я все же приведу её здесь. 

.global __ASM_FUN(kuz_encrypt_block_asm);

// j4 = kuz_key_t *key, j5 = void *blk j6= kuz_pil_enc128 j7= offset

 __ASM_FUN(kuz_encrypt_block_asm):

 V0 = Q[j5+0]; lc0 = 9;; // get block

 V1 = Q[j4+=4];; // get first key

 YV7x6 = Q[j7+0]; k6 = j6;;

 XV7x6 = Q[j7+4];; // Q – 128bit load

// loop is here

next_velp:

 //+1 пузырь

 LV0 = V0 xor V1 ;; // L- 64-bit elements

 j12 = yr0; k12 = xr0;;

 SV3x2 = expand BV0 (IU);; // S – 16-bit elements

 V8 = Q[j4+=4];; // next key

 SV2 = lshift V2 by 2;;

 SV3 = lshift V3 by 2; j12 = lshiftl j12 by 24; k12 = lshiftl k12 by 24;;

 V5x4 = expand SV2 + SV6 (IU); j12 = lshiftr j12 by 22; k12 = lshiftr k12 by 22;;

 V3x2 = expand SV3 + SV7 (IU); j3:0 = YV5x4; k3:0 = XV5x4;;

 j11:8 = YV3x2; k11:8 = XV3x2;;

 V0 = Q[j12+j6]; k12 = k12+0x2000;; // 0x2000 is half=256*4*8

 V1 = Q[k12+k6];;

 V2 = Q[j1+j6]; LV0 = V0 xor V8;;

 V3 = Q[k1+k6];;

 LV0 = V0 xor V2 ; V2 = Q[j2+j6];;

 LV1 = V1 xor V3 ; V3 = Q[k2+k6];;

 LV0 = V0 xor V2 ; V2 = Q[j3+j6];;

 LV1 = V1 xor V3 ; V3 = Q[k3+k6];;

 LV0 = V0 xor V2 ; V2 = Q[j8+j6];;

 LV1 = V1 xor V3 ; V3 = Q[k8+k6];;

 LV0 = V0 xor V2 ; V2 = Q[j9+j6];;

 LV1 = V1 xor V3 ; V3 = Q[k9+k6];;

 LV0 = V0 xor V2 ; V2 = Q[j10+j6];;

 LV1 = V1 xor V3 ; V3 = Q[k10+k6];;

 LV0 = V0 xor V2 ; V2 = Q[j11+j6];;

 LV1 = V1 xor V3 ; V3 = Q[k11+k6];;

 LV0 = V0 xor V2 ;;

 if nlc0e, jump next_velp; LV1 = V1 xor V3;; // repeat 9 

//+1 пузырь

 LV0 = V0 xor V1;;

 cjmp(abs); Q[j5+0] = V0;; // store 128-bit and return

Если у Эльбруса широкое слово ограничивается скобками {}, то у ВН44 это ;; Регистры J и K это регистры модулей адресной арифметики. V3x2 это уже 256-разрядный регистр, как объединение двух 128-разрядных. 

Одна итерация цикла занимает 28 тактов. На обработку 16 байт данных тратится (28*9) + 7 = 259 тактов. Получается, что процессор тратит на шифрование одного байта входных данных в среднем 17 тактов. Из 28 тактов цикла 16 тактов уходит на операцию XOR. Поскольку процессор в одном такте может выполнять только одну такую операцию , то как-то еще ускорить этот тип вычислений просто невозможно. Единственный вариант ускорения это поиск других операций цикла, которые могут быть выполнены одновременно с выполнением операции XOR. Однако доступной операцией в данном случае является только операция загрузки данных. Процессор может выполнять в течение одного такта и две загрузки 128-разрядных данных, но в данном случае это не позволит ускорить алгоритм из-за узкого места в единственном 128-разрядном АЛУ.

11 тактов цикла тратится на подготовку индексов операндов массива данных. В данном случае используется команда SIMD обработки и 16 байт результата сначала преобразуются в 16 значений типа short. Далее эти 16-разрядные значения сдвигаются влево на два разряда для формирования индекса квадрослова. Следующим этапом является операция сложения вектора индексов с заранее подготовленным вектором смещений. Одновременно со сложением происходит расширение индекса до 32-разрядного значения. Далее индексы пересылаются в модули адресной арифметики, где уже используются для адресации данных следующего цикла. 

Что в итоге

Увы, достигнуть вершины Эльбруса пока не получилось. Но мне достигнутый результат понравился. Скорость обработки 17 тактов на байт очень хороший показатель. Алгоритм как раз задействовал лучшие возможности процессора – 128 разрядные шины обмена с памятью и 128-разрядную обработку данных в АЛУ. Также алгоритм показал и слабое место в архитектуре процессора, когда, при наличии двух конвейеров 128-разрядной обработки, определенный тип команд может присутствовать только на одном конвейере.

Подробная информация о DSP процессоре, который я использовал, приведена здесь. С исходными кодами проекта, адаптированными под К1967ВН44, можно ознакомиться здесь

Теги:
Хабы:
Всего голосов 47: ↑46 и ↓1+45
Комментарии6

Публикации

Информация

Сайт
www.milandr.ru
Дата регистрации
Дата основания
Численность
501–1 000 человек
Местоположение
Россия

Истории