Программирование высокопроизводительных приложений на процессоре Cell BE: Часть 4. Программирование SPU с расчетом на производительность

Создавайте оптимальный код для вычислительного элемента SPU процессора Cell Broadband Engine™ (Cell BE), обеспечивая вашим приложениям молниеносную скорость работы. В этой части серии статей Программирование высокопроизводительных приложений на процессоре Cell BE рассматриваются такие вопросы, как векторное SIMD-программирование, устранение ветвлений (branch elimination), раскрутка циклов (loop unrolling), планирование выполнения инструкций (instruction scheduling) и приемы прогнозирования ветвлений (branch hinting). В предыдущих статьях была представлена информация о приставке Sony® PLAYSTATION® 3, архитектуре процессора Cell BE и программировании SPU.

Джонатан Бартлетт, технический директор, New Media Worx

Джонатан Бартлет (Jonathan Bartlett) является автором книги "Программирование с нуля" - введения в программирование на языке ассемблера для Linux. Он является ведущим разработчиком в New Media Worx и занимается Web-приложениями (видео, киосками), а также настольными приложениями для клиентов. Вы можете связаться с ним по адресу johnnyb@eskimo.com.



09.09.2008

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

Начальная программа

В последней статье мы остановились на функции под названием convert_to_upper, которая одновременно оперировала только одним байтом данных для преобразования строки в верхний регистр. Функции в программах этой статьи оперируют одновременно целыми буферами. Процессор SPU разработан для работы с пакетными данными, поэтому переход к модели "содержимое буфера сразу" (buffer-at-a-time) упростит усовершенствование ваших программ. Первая рассмотренная здесь версия программы просто поместит в цикл код из предыдущей статьи. Поскольку эта программа основана на коде и концепциях, описанных в предыдущих статьях, я не буду подробно объяснять каждую ее строку.

Ниже представлена неоптимизированная версия buffer-at-a-time функции для преобразования строки в верхний регистр (введите как convert_buffer.s).

Листинг 1. Начальный пример программы
.text

.global convert_buffer_to_upper
.type convert_buffer_to_upper, @function
convert_buffer_to_upper:
	##ИСПОЛЬЗОВАНИЕ РЕГИСТРОВ:
	#   3) адрес буфера / текущий адрес
	#   4) размер буфера
	#   5) конечный адрес
	#   6) текущее четверное слово
	#   7) текущее четверное слово с байтом в первой позиции
	#   8, 9, и 10) Определение того, находится ли байт в диапазоне
	#   11) управление вставкой байта
	#   12) текущее четверное слово с правильно вставленным байтом
	#   13) true, если нужно ответвление, иначе false
	#   14) фактор преобразования

	#Вычисление конечного адреса
	a $5, $4, $3

loop_start:
	#НЕВЫРОВНЕННАЯ ЗАГРУЗКА
	lqd $6, 0($3)
	rotqby $7, $6, $3
	rotqbyi $7, $7, -3

	#НАХОДИТСЯ ЛИ В ДИАПАЗОНЕ 'a'-'z'?
	cgtbi $8, $7, 'a' - 1
	cgtbi $9, $7, 'z'
	xor $10, $8, $9
	andi $10, $10, 255

	#Выход, если нет
	brz $10, finish_loop

is_lowercase:
	#Выполнение преобразования, если да
	il $14, 'a' - 'A'
	absdb $7, $7, $14

finish_loop:
	#Невыровненное сохранение (регистр $6 уже содержит текущее слово)
	cbd $11, 0($3)
	shufb $12, $7, $6, $11
	stqd $12, 0($3)

	#IИнкремент указателя
	ai $3, $3, 1

	#Если мы не достигли конца, продолжаем цикл
	cgt $13, $3, $5
	brz $13, loop_start

end_function:
	#Возврат
	bi $lr

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

Функция, управляющая преобразованием, теперь немного упростится, поскольку она должна лишь загрузить данные, выполнить преобразование и скопировать данные обратно. Ниже приведен ее код (введите как convert_driver.s).

Листинг 2. Основная функция преобразования строки в верхний регистр
.data

#Это структура, которую мы будем копировать из основного процесса PPE
.align 4
conversion_info:
conversion_length:
	.octa 0
conversion_data:
	.octa 0
.equ CONVERSION_STRUCT_SIZE, 32

.section .bss #Uninitialized Data Section

#Это буфер, в котором мы будем хранить строку
.align 4
.lcomm conversion_buffer, 16384

.text

#Константы MFC
.equ MFC_GET_CMD, 0x40
.equ MFC_PUT_CMD, 0x20

.equ LR_OFFSET, 16

.global main
.type main, @function
.equ MAIN_FRAME_SIZE, 32
main:
	#Пролог
	stqd $lr, LR_OFFSET($sp)
	stqd $sp, -MAIN_FRAME_SIZE($sp)
	ai $sp, $sp, -MAIN_FRAME_SIZE

	##КОПИРОВАНИЕ ИНФОРМАЦИИ ПРЕОБРАЗОВАНИЯ##
	ila $3, conversion_info       #Адрес локальной памяти
	#регистр 4 уже содержит адрес #64-разрядный адрес основной памяти
	il $5, CONVERSION_STRUCT_SIZE #Размер передачи
	il $6, 0                      #Тэг DMA
	il $7, MFC_GET_CMD            #Команда DMA
	brsl $lr, perform_dma

	#Ожидание завершения работы DMA
	il $3, 0
	brsl $lr, wait_for_dma_completion

	##КОПИРОВАНИЕ СТРОКИ В БУФЕР##
	#Загрузка указателя буферных данных
	ila $3, conversion_buffer #Адрес локальной памяти
	lqr $4, conversion_data   #64-разрядный адрес основной памяти
	lqr $5, conversion_length #РАЗМЕР
	il $6, 0                  #Тэг DMA
	il $7, MFC_GET_CMD        #Команда DMA
	brsl $lr, perform_dma

	#Ожидание завершения работы DMA
	il $3, 0
	brsl $lr, wait_for_dma_completion

	##ВЫПОЛНЕНИЕ ПРЕОБРАЗОВАНИЯ##
	ila $3, conversion_buffer
	lqr $4, conversion_length
	brsl $lr, convert_buffer_to_upper

	##КОПИРОВАНИЕ ДАННЫХ ОБРАТНО##
	ila $3, conversion_buffer   #Адрес локальной памяти
	lqr $4, conversion_data     #64-разрядный адрес основной памяти
	lqr $5, conversion_length   #Размер
	il $6, 0                    #Тэг DMA
	il $7, MFC_PUT_CMD          #Команда DMA
	brsl $lr, perform_dma

	#Ожидание завершения работы DMA
	il $3, 0
	brsl $lr, wait_for_dma_completion

	##ВЫХОД ИЗ ПРОГРАММЫ##
	#Возврат значения
	il $3, 0

	#Эпилог
	ai $sp, $sp, MAIN_FRAME_SIZE
	lqd $lr, LR_OFFSET($sp)
	bi $lr

Вам также понадобятся файлы dma_utils.s и ppu_dma_main.c из предыдущей статьи.

Чтобы выполнить сборку и запуск программы, выполните следующие шаги:

spu-gcc convert_buffer.s convert_driver.s dma_utils.s -o spe_convert
embedspu -m64 convert_to_upper_handle spe_convert spe_convert_csf.o
gcc -m64 spe_convert_csf.o ppu_dma_main.c -lspe -o dma_convert
./dma_convert

Эти же действия можно использовать для сборки всех остальных примеров этой статьи.


Векторизация кода

Самым очевидным способом оптимизации векторного процесса является векторизация кода, также известная как SIMD (single instruction, multiple data – один поток команд - много потоков данных), или информационный параллелизм. В процессорах SPU большинство инструкций могут оперировать регистрами так, как если бы они содержали несколько независимых значений (таким образом, одна инструкция действует на несколько элементов данных). Каждый 128-разрядный регистр может рассматриваться как 16 независимых байтов, 8 полуслов, 4 слова, 2 двойных слова, или как один элемент. Хотя набор команд процессора в первую очередь нацелен на разделение регистра на четыре 32-разрядных слова, однако он обеспечивает поддержку для обработки всех вышеперечисленных вариантов.

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

Первое, что вам необходимо сделать – это устранить ветвление так, чтобы независимо от выполнения условия ваш код использовал один и тот же набор инструкций (как я покажу далее, устранение ветвлений также помогает уменьшить простои ветвлений). Итак, как же это делается? Ответ заключается в том, что SPU имеет несколько команд условий, таких как selb, shufb, а также использует побитовые операции, которые позволяют выполняться командам условий без использования ветвлений. Программа заканчивается тем, что вычисляет оба ответа, а затем использует команду условия для выбора заданного ответа.

Ниже приведен код преобразования в том виде, в котором он существует на данный момент.

	#ПОПАДАЕТ В ДИАПАЗОН 'a'-'z'
	cgtbi $8, $7, 'a' - 1
	cgtbi $9, $7, 'z'
	xor $10, $8, $9
	andi $10, $10, 255

	brz $10, finish_loop

is_lowercase:
	#условие для случая нижнего регистра
	il $14, 'a' - 'A'
	absdb $7, $7, $14

finish_loop:
	#условие для случая не нижнего регистра
        #весь код доводится до конца здесь

В этом случае мы вычисляем следующие два ответа:

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

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

	#$7 содержит наше исходное значение
	il $14, 'a' - 'A'
	absdb $15, $7, $14
	#$7 содержит исходное, а $15 – преобразованное значение
	#Выбор между значениями в регистрах $7 и $14 и 
        #помещение выбранного значения в регистр $7

	##...остаток цикла...

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

	cgtbi $8, $7, 'a' - 1
	cgtbi $9, $7, 'z'
	xor $10, $8, $9

Обратите внимание на то, что инструкция andi, использовавшаяся ранее, больше не нужна, поскольку она использовалась для указания ненужных значений для условного ветвления (условные ветвления основаны на значении "правда" или "ложь" preferred-слота слова, и вам важно только значения preferred-слота байта). Поскольку вы не используете условные ветвления, то это вам не важно! Итак, теперь регистр $10 содержит в preferred-слоте все единицы, если он попадает в диапазон, и все нули – если выходит за границы диапазона. Теперь все, что вам нужно, это выбрать регистр $7 или $15 на основании значения в регистре $10. Инструкция selb (select bits – выбрать биты) отлично подходит для этого. Эта инструкция имеет четыре операнда:

  1. регистр назначения
  2. исходное значение 1
  3. исходное значение 2
  4. селектор

Инструкция selb работает, проходя через селектор бит за битом. Для каждой битовой позиции проверяется следующее: если бит равен нулю, то в этой же позиции регистра назначения используется бит из исходного значения 1; если же бит равен единице, то используется бит из исходного значения 2. Если представить каждый регистр, как массив битов, то инструкция selb имеет следующее значение:

//воображаемое представление инструкции selb для тех,
//кому более близок язык C, а не ассемблер
for(i = 0; i < 128; i++) {
	destination[i] = selector[i] == 0 ? source_1[i] : source_2[i]
}

Теперь, я надеюсь, вы видите, почему операторы условия устанавливают все соответствующие биты в регистре назначения равными 1, если условие выполняется – это упрощает использование значения для selb. В этом случае вы можете просто добавить следующую строку кода:

	selb $7, $7, $15, $10

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

Листинг 3. Код для преобразования регистра, не использующий ветвлений
	#Исходное значение находится в $7

	#Выполнение преобразования и сохранение в $15
	il $14, 'a' - 'A'
	absdb $15, $7, $14

	#Нижний ли это диапазон ('a'-'z')?
	cgtbi $8, $7, 'a'-1
	cgtbi $9, $7, 'z'
	xor $10, $8, $9
	#$10 содержит в preferred-слоте все единицы для случая нижнего регистра
	#и все нули – для случая не нижнего регистра
    
	#Выбор соответствующего значения в $7, исходя из условия
	selb $7, $7, $15, $10

	#теперь $7 содержит правильное значение

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

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

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

  • Ветвление приводило к тому, что весь регистр либо преобразовывался, либо не преобразовывался целиком.
  • Регистр, содержащий фактор преобразования, был предназначен для работы с простым байтом, а не с целым регистром (инструкция il загружает указанное значение в каждое слово, но вам необходимо это значение в каждом байте).
  • Инструкции загрузки/сохранения, а также счетчик цикла, были предназначены для одновременной обработки одного байта.

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

.equ CONVERSION_FACTOR, 'a' - 'A'
.align 4
conversion_bytes:
	.fill 16, 1, CONVERSION_FACTOR

В код перед циклом добавьте следующую строку:

	lqr $14, conversion_bytes

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

Листинг 4. Преобразование набора значений
	#$7 начинается с 'Hello There!    '
	#В шестнадцатеричной системе это будет   0x48656c6c6f2054686572652120202020
	#$14  - это фактор преобразования в каждом байте
	# В шестнадцатеричной системе это будет   0x20202020202020202020202020202020

	absdb $15, $7, $14
	#  -> $15 теперь содержит 0x28454c4c4f0034484552450100000000
	cgtbi $8, $7, 'a'-1
	#  -> $8 теперь содержит  0xffffffffff00ffffffffff0000000000
	cgtbi $9, $7, 'z'
	#  -> $9 теперь содержит  0xff0000000000ff000000000000000000
	xor $10, $8, $9
	#  -> $10 теперь содержит 0x00ffffffff0000ffffffff0000000000
	selb $7, $7, $15, $10
	#  -> $7 теперь содержит  0x48454c4c4f2054484552452120202020
	# это является шестнадцатеричным значением для 'HELLO THERE!    '

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

Листинг 5. Структура цикла для векторизованного кода
##Сохранение фактора преобразования##
.data
.equ CONVERSION_FACTOR, 'a' - 'A'
.align 4
conversion_bytes:
	.fill 16, 1, CONVERSION_FACTOR

.text
.global convert_buffer_to_upper
.type convert_buffer_to_upper, @function
convert_buffer_to_upper:
	#Вычисление конечного адреса
	a $5, $4, $3

	#Загрузка факторов преобразования
	lqr $14, conversion_bytes

loop_start:
	#Выровненная загрузка
	lqd $7, 0($3)

	##ПРЕОБРАЗОВАНИЕ##
	absdb $15, $7, $14
	cgtbi $8, $7, 'a'-1
	cgtbi $9, $7, 'z'
	xor $10, $8, $9
	selb $7, $7, $15, $10
	##КОНЕЦ ПРЕОБРАЗОВАНИЯ##

	#Выровненное сохранение
	stqd $7, 0($3)

	#Инкремент указателя
	ai $3, $3, 16

	#Выход при необходимости ($5 содержит конечный адрес)
	cgt $13, $3, $5
	brz $13, loop_start

end_function:
	bi $lr

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


Раскрутка циклов

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

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

Листинг 6. Программа преобразования с использованием именованных регистров
.data
.equ CONVERSION_FACTOR, 'a' - 'A'
.align 4
conversion_bytes:
	.fill 16, 1, CONVERSION_FACTOR

.text
.global convert_buffer_to_upper
.type convert_buffer_to_upper, @function
	##ОПРЕДЕЛЕНИЯ РЕГИСТРОВ##
	#регистры управления циклом/функцией
	.equ BUFFER_REG, 3             #Адрес буфера / текущий адрес
	.equ BUFFER_SZ_REG, 4          #Размер буфера
	.equ BUFFER_END_REG, 5         #Конечный адрес
	.equ CONVERSION_BYTES_REG, 6   #Данные преобразования
	.equ IS_FINISHED_REG, 7        #Преобразование закончено?

	#Регистры, связанные с преобразованием
	.equ CURRENT_VAL_REG, 8  #Текущее четверное слово
	.equ BOOL_TMP1_REG, 9    #используется для вычисления IN_RANGE_REG
	.equ BOOL_TMP2_REG, 10   #используется для вычисления IN_RANGE_REG
	.equ IN_RANGE_REG, 11    #Попадает ли значение в диапазон?
	.equ PROCESSED_VAL_REG, 12   #Байты преобразования, имеющие соответствующую маску

	#Информация о регистрах
	.equ NUMREGS, 5          #Число регистров на итерацию
	.equ REGBYTES, 16        #Число байтов в регистре
convert_buffer_to_upper:
	#Вычисление конечного адреса
	a $BUFFER_END_REG, $BUFFER_SZ_REG, $BUFFER_REG

	lqr $CONVERSION_BYTES_REG, conversion_bytes

loop_start:
	#Выровненная загрузка
	lqd $CURRENT_VAL_REG, 0($BUFFER_REG)

	##ПРЕОБРАЗОВАНИЕ##
	absdb $PROCESSED_VALS_REG, $CURRENT_VAL_REG, $CONVERSION_BYTES_REG
	cgtbi $BOOL_TMP1_REG, $CURRENT_VAL_REG, 'a'-1
	cgtbi $BOOL_TMP2_REG, $CURRENT_VAL_REG, 'z'
	xor $IN_RANGE_REG, $BOOL_TMP1_REG, $BOOL_TMP2_REG
	selb $CURRENT_VAL_REG, $CURRENT_VAL_REG, $PROCESSED_VAL_REG, $IN_RANGE_REG
	##КОНЕЦ ПРЕОБРАЗОВАНИЯ##

	#Выровненное сохранение
	stqd $CURRENT_VAL_REG, 0($BUFFER_REG)

	#IИнкремент указателя
	ai $BUFFER_REG, $BUFFER_REG, REGBYTES

	#Выход при необходимости
	cgt $IS_FINISHED_REG, $BUFFER_REG, $BUFFER_END_REG
	brz $IS_FINISHED_REG, loop_start

end_function:
	bi $lr

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

Листинг 7. Преобразования буфера – раскрученный цикл
loop_start:

	#ИТЕРАЦИЯ 0
	lqd $(CURRENT_VAL_REG+0*NUMREGS), 0*REGBYTES($BUFFER_REG)
	absdb $(PROCESSED_VAL_REG+0*NUMREGS), $(CURRENT_VAL_REG+0*NUMREGS), 
	$CONVERSION_BYTES_REG
	cgtbi $(BOOL_TMP1_REG+0*NUMREGS), $(CURRENT_VAL_REG+0*NUMREGS), 'a'-1
	cgtbi $(BOOL_TMP2_REG+0*NUMREGS), $(CURRENT_VAL_REG+0*NUMREGS), 'z'
	xor $(IN_RANGE_REG+0*NUMREGS), $(BOOL_TMP1_REG+0*NUMREGS), 
	$(BOOL_TMP2_REG+0*NUMREGS)
	selb $(CURRENT_VAL_REG+0*NUMREGS), $(CURRENT_VAL_REG+0*NUMREGS),
	$(PROCESSED_VAL_REG+0*NUMREGS), $(IN_RANGE_REG+0*NUMREGS)
	stqd $(CURRENT_VAL_REG+0*NUMREGS), 0*REGBYTES($BUFFER_REG)

	#ИТЕРАЦИЯ 1
	lqd $(CURRENT_VAL_REG+1*NUMREGS), 1*REGBYTES($BUFFER_REG)
	absdb $(PROCESSED_VAL_REG+1*NUMREGS), $(CURRENT_VAL_REG+1*NUMREGS),
	 $CONVERSION_BYTES_REG
	cgtbi $(BOOL_TMP1_REG+1*NUMREGS), $(CURRENT_VAL_REG+1*NUMREGS), 'a'-1
	cgtbi $(BOOL_TMP2_REG+1*NUMREGS), $(CURRENT_VAL_REG+1*NUMREGS), 'z'
	xor $(IN_RANGE_REG+1*NUMREGS), $(BOOL_TMP1_REG+1*NUMREGS), 
	$(BOOL_TMP2_REG+1*NUMREGS)
	selb $(CURRENT_VAL_REG+1*NUMREGS), $(CURRENT_VAL_REG+1*NUMREGS),
	$(PROCESSED_VAL_REG+1*NUMREGS), $(IN_RANGE_REG+1*NUMREGS)
	stqd $(CURRENT_VAL_REG+1*NUMREGS), 1*REGBYTES($BUFFER_REG)

	#ИТЕРАЦИЯ 2
	lqd $(CURRENT_VAL_REG+2*NUMREGS), 2*REGBYTES($BUFFER_REG)
	absdb $(PROCESSED_VAL_REG+2*NUMREGS), $(CURRENT_VAL_REG+2*NUMREGS), 
	$CONVERSION_BYTES_REG
	cgtbi $(BOOL_TMP1_REG+2*NUMREGS), $(CURRENT_VAL_REG+2*NUMREGS), 'a'-1
	cgtbi $(BOOL_TMP2_REG+2*NUMREGS), $(CURRENT_VAL_REG+2*NUMREGS), 'z'
	xor $(IN_RANGE_REG+2*NUMREGS), $(BOOL_TMP1_REG+2*NUMREGS), 
	$(BOOL_TMP2_REG+2*NUMREGS)
	selb $(CURRENT_VAL_REG+2*NUMREGS), $(CURRENT_VAL_REG+2*NUMREGS),
	$(PROCESSED_VAL_REG+2*NUMREGS), $(IN_RANGE_REG+2*NUMREGS)
	stqd $(CURRENT_VAL_REG+2*NUMREGS), 2*REGBYTES($BUFFER_REG)

	#ИТЕРАЦИЯ 3
	lqd $(CURRENT_VAL_REG+3*NUMREGS), 3*REGBYTES($BUFFER_REG)
	absdb $(PROCESSED_VAL_REG+3*NUMREGS), $(CURRENT_VAL_REG+3*NUMREGS), 
	$CONVERSION_BYTES_REG
	cgtbi $(BOOL_TMP1_REG+3*NUMREGS), $(CURRENT_VAL_REG+3*NUMREGS), 'a'-1
	cgtbi $(BOOL_TMP2_REG+3*NUMREGS), $(CURRENT_VAL_REG+3*NUMREGS), 'z'
	xor $(IN_RANGE_REG+3*NUMREGS), $(BOOL_TMP1_REG+3*NUMREGS),
	 $(BOOL_TMP2_REG+3*NUMREGS)
	selb $(CURRENT_VAL_REG+3*NUMREGS), $(CURRENT_VAL_REG+3*NUMREGS),
	$(PROCESSED_VAL_REG+3*NUMREGS), $(IN_RANGE_REG+3*NUMREGS)
	stqd $(CURRENT_VAL_REG+3*NUMREGS), 3*REGBYTES($BUFFER_REG)

	#Инкремент указателя
	ai $BUFFER_REG, $BUFFER_REG, 4*REGBYTES

	#Выход при необходимости
	cgt $IS_FINISHED_REG, $BUFFER_REG, $BUFFER_END_REG
	brz $IS_FINISHED_REG, loop_start

Эта программа вычисляет используемые регистры. Вы могли бы просто пронумеровать регистры, но после этого написание кода и запоминание того, какой регистр за что отвечает, могло бы стать еще более утомительным, чем прежде. Однако, поскольку каждая итерация использует те же самые номера регистров, вы можете просто вычислить номер регистра во время ассемблирования. Взгляните, например, на регистр $(BOOL_TMP1_REG+2*NUMREGS). Данная конструкция означает, что для итерации 2 это регистр называется BOOL_TMP1_REG. Поскольку регистр BOOL_TMP1_REG имеет номер 9, а значение NUMREGS равно 5, то фактическим номером регистра будет являться 9+2*5, или 19. Таким образом, если позже вам потребуется добавить регистр в ваш код, ассемблер автоматически пересчитает номер нового регистра, и вам не придется вносить изменения в правила нумерации регистров. В этом случае вам понадобится лишь назначить регистру его символическое имя и увеличить значение NUMREGS.

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


Планирование выполнения инструкций

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

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

Большинство работы по настройке производительности SPU связано с минимизацией регистровых остановов. По этой причине вам не помешает взглянуть на конвейерную обработку различных типов инструкций SPU (информация взята из справочника Cell BE Handbook, страница 688):

Задержки для инструкций SPU
Тип инструкцииЗадержкаКонвейерДополнительные примечания
Операции с данными типа double-precision floating-point13ЧетныйПервые шесть тактов фактически работают в режиме останова, и никакие другие инструкции в это время не выполняются. Режим dual-issue (будет обсуждаться позже) также не поддерживается этими инструкциями.
Умножение целых чисел, преобразование floating-point/integer, интерполирование7Четный
Тип данных single-precision floating-point6Четный
Байтовые операции4Четный
Последовательные перестановки и циклические сдвиги на основе элементов4Четный
Загрузки в непосредственном (immediate-mode) режиме2Четный
Простые целочисленные и логические операции (включая selb)2Четный
Операции загрузки и сохранения6НечетныйВ отличие от других архитектур, загрузки и сохранения SPU являются детерминированными, поскольку не используется кэш. Уменьшая количество памяти таким образом, чтобы она могла полностью располагаться на одной микросхеме локальной памяти, можно заставить SPU выполнять загрузку и сохранение данных значительно быстрее и более надежно по сравнению с другими типами процессоров.
Прогнозирование ветвлений6НечетныйСпециальные правила для прогнозирования ветвлений будут обсуждаться в следующем разделе.
Операции с каналами6Нечетный
Манипуляции с регистрами специального назначения6Нечетный
Ветвления4НечетныйПравильно спрогнозированные ветвления (будут обсуждаться в следующем разделе) позволяют следующей инструкции начаться в ближайшем такте.
Перестановка байтов4Нечетный
Последовательные перестановки и циклические сдвиги четверных слов4Нечетный
Оценка4Нечетный
Сбор, маскирование и генерирование элементов управления вводом4Нечетный

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

	a $5, $6, $7   #инструкция 1
	a $8, $5, $9   #инструкция 2
	a $10, $8, $7  #инструкция 3
	a $11, $8, $7  #инструкция 4

В этой программе для завершения выполнения инструкции 1 требуется четыре такта. Инструкция 2 требует результат выполнения инструкции 1 ($5), поэтому ей приходится ждать все четыре такта. Инструкция 3 требует результат выполнения инструкции 2 ($8), поэтому ей приходится ждать четыре такта. Инструкция 4 может быть выполнена сразу же за инструкцией 3, поскольку ей не требуется результат выполнения инструкции 3. Можно представить это следующим образом:

	a $5, $6, $7     # такт 1
	#Останов для $5  # такт 2
	#Останов для $5  # такт 3
	#Останов для $5  # такт 4
	a $8, $5, $9     # такт 5
	#Останов для $8  # такт 6
	#Останов для $8  # такт 7
	#Останов для $8  # такт 8
	a $10, $8, $7    # такт 9
	a $11, $8, $7    # такт 10

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

Процессор SPU не только может обрабатывать через свой конвейер несколько значений одновременно, но также поддерживает инструкции dual-issue режима, которые обрабатываются через различные конвейеры. SPU имеет два конвейера – четный (иногда его называют конвейер 0 или конвейер выполнения), и нечетный (конвейер 1 или конвейер загрузки). В вышеприведенной таблице были перечислены различные типы инструкций и конвейеры, на которых они выполняются. Фактически SPU загружает одновременно две инструкции из границ, выровненных до двойного слова. Это называется групповой операцией. Если первая инструкция групповой операции выполняется на четном конвейере, а вторая инструкция – на нечетном, то обе этих инструкции могут быть выполнены одновременно. Если эти условия не выполняются, или если вторая инструкция должна ждать завершения предыдущей, то в этом случае они выполняются в разных тактах. Чтобы помочь правильно выстроить инструкции и задействовать режим dual-issue, существует две команды холостого хода, которые можно использовать для соответствующего расположения инструкций – nop (no-operation on the even pipeline – холостой ход на четном конвейере) и lnop (no-operation on the odd pipeline– холостой ход на нечетном конвейере). Кроме того, вы можете использовать инструкцию .align 3, чтобы заставить заданную инструкцию запуститься в новой групповой операции (для этого она будет дополнена соответствующими командами холостого хода).

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

Листинг 8. Итерация цикла с информацией об остановах
.align 4 #начинаем новую групповую операцию
	#ИТЕРАЦИЯ  0
	nop
	lqd $(CURRENT_VAL_REG+0*NUMREGS), 0*REGBYTES($BUFFER_REG)
	#останов (обрабатываем CURRENT_VAL_REG)
	#останов
	#останов
	#останов
	#останов
	absdb $(PROCESSED_VAL_REG+0*NUMREGS), $(CURRENT_VAL_REG+0*NUMREGS), 
	$CONVERSION_BYTES_REG
	lnop
	cgtbi $(BOOL_TMP1_REG+0*NUMREGS), $(CURRENT_VAL_REG+0*NUMREGS), 'a'-1
	lnop
	cgtbi $(BOOL_TMP2_REG+0*NUMREGS), $(CURRENT_VAL_REG+0*NUMREGS), 'z'
	lnop
	#останов (обрабатываем BOOL_TMP2_REG)
	xor $(IN_RANGE_REG+0*NUMREGS), $(BOOL_TMP1_REG+0*NUMREGS),
	 $(BOOL_TMP2_REG+0*NUMREGS)
	lnop
	#останов (обрабатываем IN_RANGE_REG)
	selb $(CURRENT_VAL_REG+0*NUMREGS), $(CURRENT_VAL_REG+0*NUMREGS),
	$(PROCESSED_VAL_REG+0*NUMREGS), $(IN_RANGE_REG+0*NUMREGS)
	lnop
	#останов (обрабатываем CURRENT_VAL_REG)
	nop
	stqd $(CURRENT_VAL_REG+0*NUMREGS), 0*REGBYTES($BUFFER_REG)

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

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

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

Листинг 9. Чередующееся тело цикла минимизирует простои по зависимости
	lqd $(CURRENT_VAL_REG+0*NUMREGS), 0*REGBYTES($BUFFER_REG)
	lqd $(CURRENT_VAL_REG+1*NUMREGS), 1*REGBYTES($BUFFER_REG)
	lqd $(CURRENT_VAL_REG+2*NUMREGS), 2*REGBYTES($BUFFER_REG)
	lqd $(CURRENT_VAL_REG+3*NUMREGS), 3*REGBYTES($BUFFER_REG)
	absdb $(PROCESSED_VAL_REG+0*NUMREGS), $(CURRENT_VAL_REG+0*NUMREGS),
	 $CONVERSION_BYTES_REG
	absdb $(PROCESSED_VAL_REG+1*NUMREGS), $(CURRENT_VAL_REG+1*NUMREGS), 
	$CONVERSION_BYTES_REG
	absdb $(PROCESSED_VAL_REG+2*NUMREGS), $(CURRENT_VAL_REG+2*NUMREGS), 
	$CONVERSION_BYTES_REG
	absdb $(PROCESSED_VAL_REG+3*NUMREGS), $(CURRENT_VAL_REG+3*NUMREGS),
	 $CONVERSION_BYTES_REG
	cgtbi $(BOOL_TMP1_REG+0*NUMREGS), $(CURRENT_VAL_REG+0*NUMREGS), 'a'-1
	cgtbi $(BOOL_TMP1_REG+1*NUMREGS), $(CURRENT_VAL_REG+1*NUMREGS), 'a'-1
	cgtbi $(BOOL_TMP1_REG+2*NUMREGS), $(CURRENT_VAL_REG+2*NUMREGS), 'a'-1
	cgtbi $(BOOL_TMP1_REG+3*NUMREGS), $(CURRENT_VAL_REG+3*NUMREGS), 'a'-1
	cgtbi $(BOOL_TMP2_REG+0*NUMREGS), $(CURRENT_VAL_REG+0*NUMREGS), 'z'
	cgtbi $(BOOL_TMP2_REG+1*NUMREGS), $(CURRENT_VAL_REG+1*NUMREGS), 'z'
	cgtbi $(BOOL_TMP2_REG+2*NUMREGS), $(CURRENT_VAL_REG+2*NUMREGS), 'z'
	cgtbi $(BOOL_TMP2_REG+3*NUMREGS), $(CURRENT_VAL_REG+3*NUMREGS), 'z'
	xor $(IN_RANGE_REG+0*NUMREGS), $(BOOL_TMP1_REG+0*NUMREGS), 
	$(BOOL_TMP2_REG+0*NUMREGS)
	xor $(IN_RANGE_REG+1*NUMREGS), $(BOOL_TMP1_REG+1*NUMREGS),
	 $(BOOL_TMP2_REG+1*NUMREGS)
	xor $(IN_RANGE_REG+2*NUMREGS), $(BOOL_TMP1_REG+2*NUMREGS), 
	$(BOOL_TMP2_REG+2*NUMREGS)
	xor $(IN_RANGE_REG+3*NUMREGS), $(BOOL_TMP1_REG+3*NUMREGS), 
	$(BOOL_TMP2_REG+3*NUMREGS)
	selb $(CURRENT_VAL_REG+0*NUMREGS), $(CURRENT_VAL_REG+0*NUMREGS),
	$(PROCESSED_VAL_REG+0*NUMREGS), $(IN_RANGE_REG+0*NUMREGS)
	selb $(CURRENT_VAL_REG+1*NUMREGS), $(CURRENT_VAL_REG+1*NUMREGS),
	$(PROCESSED_VAL_REG+1*NUMREGS), $(IN_RANGE_REG+1*NUMREGS)
	selb $(CURRENT_VAL_REG+2*NUMREGS), $(CURRENT_VAL_REG+2*NUMREGS),
	$(PROCESSED_VAL_REG+2*NUMREGS), $(IN_RANGE_REG+2*NUMREGS)
	selb $(CURRENT_VAL_REG+3*NUMREGS), $(CURRENT_VAL_REG+3*NUMREGS),
	$(PROCESSED_VAL_REG+3*NUMREGS), $(IN_RANGE_REG+3*NUMREGS)
	stqd $(CURRENT_VAL_REG+0*NUMREGS), 0*REGBYTES($BUFFER_REG)
	stqd $(CURRENT_VAL_REG+1*NUMREGS), 1*REGBYTES($BUFFER_REG)
	stqd $(CURRENT_VAL_REG+2*NUMREGS), 2*REGBYTES($BUFFER_REG)
	stqd $(CURRENT_VAL_REG+3*NUMREGS), 3*REGBYTES($BUFFER_REG)

	#Инкремент указателя
	ai $BUFFER_REG, $BUFFER_REG, 4*REGBYTES

	#Выход при необходимости
	cgt $IS_FINISHED_REG, $BUFFER_REG, $BUFFER_END_REG
	brz $IS_FINISHED_REG, loop_start

Данный метод называется программной конвейеризацией, и этот код теряет на простои всего лишь 2 такта. Тем не менее, здесь все еще не вполне используется режим dual-issue. На самом деле, существует не много возможностей реализации этого режима в данном коде.

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

Листинг 10. Измененный порядок выполнения инструкций
	selb $(CURRENT_VAL_REG+0*NUMREGS), $(CURRENT_VAL_REG+0*NUMREGS),
	$(PROCESSED_VAL_REG+0*NUMREGS), $(IN_RANGE_REG+0*NUMREGS)
	selb $(CURRENT_VAL_REG+1*NUMREGS), $(CURRENT_VAL_REG+1*NUMREGS),
	$(PROCESSED_VAL_REG+1*NUMREGS), $(IN_RANGE_REG+1*NUMREGS)
.align 3   ####Переходим к началу групповой операции
	#Следующие две команды запущены одновременно
	selb $(CURRENT_VAL_REG+2*NUMREGS), $(CURRENT_VAL_REG+2*NUMREGS),
	$(PROCESSED_VAL_REG+2*NUMREGS), $(IN_RANGE_REG+2*NUMREGS)
	stqd $(CURRENT_VAL_REG+0*NUMREGS), 0*REGBYTES($BUFFER_REG)
	#Следующие две команды запущены одновременно
	selb $(CURRENT_VAL_REG+3*NUMREGS), $(CURRENT_VAL_REG+3*NUMREGS),
	$(PROCESSED_VAL_REG+3*NUMREGS), $(IN_RANGE_REG+3*NUMREGS)
	stqd $(CURRENT_VAL_REG+1*NUMREGS), 1*REGBYTES($BUFFER_REG)
	stqd $(CURRENT_VAL_REG+2*NUMREGS), 2*REGBYTES($BUFFER_REG)
	stqd $(CURRENT_VAL_REG+3*NUMREGS), 3*REGBYTES($BUFFER_REG)

Путем простого упорядочивания кода программы в групповые операции и перемещения одной инструкции (последняя инструкция selb) в более подходящее место вы можете сэкономить два такта. Заметьте, что без инструкции .align 3, если инструкции selb находились бы в нечетной позиции в то время, когда инструкции stqd находились бы в четной позиции, вы не смогли бы активировать режим dual-issue, поскольку этот режим работает только тогда, когда каждая из двух инструкций надлежащим образом упорядочена и выровнена.


Прогнозирование ветвлений

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

Прогнозирование ветвлений является необходимым для SPU, поскольку неверно спрогнозированные ветвления дорого обходятся. Для того чтобы выйти из состояния ошибочного прогноза, требуется 18-19 тактов. В дополнение к этому, по умолчанию предполагается, что каждое ветвление, с которым сталкивается SPU, включая безусловные ветвления, не выбрано. Прогнозирование ветвления указывает процессору, на какой адрес назначения (hint-trigger address) нужно перейти с большей вероятностью для определенной инструкции ветвления (branch target address). Это позволяет процессору подготовиться к досрочному переходу (например, предварительно выбрать инструкции). Прогнозирование ветвлений никогда не затрагивает логический результат программы, а влияет только на количество тактов, требуемых для ее выполнения.

Существует три инструкции для прогнозирования ветвлений:

  • hbr hint_trigger, $register – указывает процессору, что инструкцию ветвления, расположенную по относительному адресу hint_trigger, с большой вероятностью нужно переместить по адресу, указанному в регистре $register.
  • hbrr hint_trigger, branch_target – указывает процессору, что инструкцию ветвления, расположенную по относительному адресу hint_trigger, с большой вероятностью нужно переместить по относительному адресу branch_target (оба адреса указаны относительно текущей инструкции).
  • hbra hint_trigger, branch_target – то же самое, что и инструкция hbrr, за исключением того, что параметр branch_target указан в качестве абсолютного адреса.

Для того чтобы прогнозирование ветвления было наиболее эффективным (таким, чтобы при ветвлении вообще не использовались остановы), оно должно находиться, по меньшей мере, за четыре групповых операции плюс 11 тактов до инструкции ветвления. Как минимум, прогнозирование ветвления должно находиться за четыре групповых операции до инструкции ветвления, в противном случае оно не будет иметь эффекта. Также оно не может располагаться более чем за 255 инструкций (физически) от прогнозируемого ветвления (сама инструкция может хранить лишь 8 битов плюс знаковый бит для относительного сдвига триггера прогнозирования, который в это время имеет два нуля, объединенные в конце). Например, если прогнозирование ветвления находится за четыре групповых операции плюс 3 такта до инструкции ветвления, то эта инструкция войдет в состояние останова ветвления на 8 тактов, что, хотя и не оптимально, но все-таки лучше, чем те 18 тактов останова в случае отсутствия прогнозирования. В каждый момент времени может быть активно только одно прогнозирование ветвления, и инструкции sync, помимо прочего, очищают любое активное прогнозирование.

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

Листинг 11. Ветвление с прогнозированием
	hbrr loop_branch_instruction, loop_start
loop_start:

	##... здесь выполняются преобразования ... ##

	#Инкремент указателя
	ai $BUFFER_REG, $BUFFER_REG, 4*REGBYTES

	#Выход при необходимости
	cgt $IS_FINISHED_REG, $BUFFER_REG, $BUFFER_END_REG
loop_branch_instruction:
	brz $IS_FINISHED_REG, loop_start

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

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

#Предполагается, что $lr содержит правильный адрес прямо сейчас (в нашем случае это так)
hbr end_function, $lr

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


Заключение

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

Листинг 12. Полностью оптимизированная функция преобразования
.data
.equ CONVERSION_FACTOR, 'a' - 'A'
.align 4
conversion_bytes:
	.fill 16, 1, CONVERSION_FACTOR

.text
.global convert_buffer_to_upper
.type convert_buffer_to_upper, @function
	.equ BUFFER_REG, 3
	.equ BUFFER_SZ_REG, 4
	.equ BUFFER_END_REG, 5
	.equ CONVERSION_BYTES_REG, 6
	.equ IS_FINISHED_REG, 7

	.equ CURRENT_VAL_REG, 8
	.equ BOOL_TMP1_REG, 9
	.equ BOOL_TMP2_REG, 10
	.equ IN_RANGE_REG, 11
	.equ PROCESSED_VAL_REG, 12

	.equ NUMREGS, 5
	.equ REGBYTES, 16
convert_buffer_to_upper:
	a $BUFFER_END_REG, $BUFFER_SZ_REG, $BUFFER_REG
	lqr $CONVERSION_BYTES_REG, conversion_bytes

	hbrr loop_branch_instruction, loop_start
loop_start:
	lqd $(CURRENT_VAL_REG+0*NUMREGS), 0*REGBYTES($BUFFER_REG)
	lqd $(CURRENT_VAL_REG+1*NUMREGS), 1*REGBYTES($BUFFER_REG)
	lqd $(CURRENT_VAL_REG+2*NUMREGS), 2*REGBYTES($BUFFER_REG)
	lqd $(CURRENT_VAL_REG+3*NUMREGS), 3*REGBYTES($BUFFER_REG)
	absdb $(PROCESSED_VAL_REG+0*NUMREGS), $(CURRENT_VAL_REG+0*NUMREGS), 
	$CONVERSION_BYTES_REG
	absdb $(PROCESSED_VAL_REG+1*NUMREGS), $(CURRENT_VAL_REG+1*NUMREGS), 
	$CONVERSION_BYTES_REG
	absdb $(PROCESSED_VAL_REG+2*NUMREGS), $(CURRENT_VAL_REG+2*NUMREGS), 
	$CONVERSION_BYTES_REG
	absdb $(PROCESSED_VAL_REG+3*NUMREGS), $(CURRENT_VAL_REG+3*NUMREGS), 
	$CONVERSION_BYTES_REG
	cgtbi $(BOOL_TMP1_REG+0*NUMREGS), $(CURRENT_VAL_REG+0*NUMREGS), 'a'-1
	cgtbi $(BOOL_TMP1_REG+1*NUMREGS), $(CURRENT_VAL_REG+1*NUMREGS), 'a'-1
	cgtbi $(BOOL_TMP1_REG+2*NUMREGS), $(CURRENT_VAL_REG+2*NUMREGS), 'a'-1
	cgtbi $(BOOL_TMP1_REG+3*NUMREGS), $(CURRENT_VAL_REG+3*NUMREGS), 'a'-1
	cgtbi $(BOOL_TMP2_REG+0*NUMREGS), $(CURRENT_VAL_REG+0*NUMREGS), 'z'
	cgtbi $(BOOL_TMP2_REG+1*NUMREGS), $(CURRENT_VAL_REG+1*NUMREGS), 'z'
	cgtbi $(BOOL_TMP2_REG+2*NUMREGS), $(CURRENT_VAL_REG+2*NUMREGS), 'z'
	cgtbi $(BOOL_TMP2_REG+3*NUMREGS), $(CURRENT_VAL_REG+3*NUMREGS), 'z'
	xor $(IN_RANGE_REG+0*NUMREGS), $(BOOL_TMP1_REG+0*NUMREGS), 
	$(BOOL_TMP2_REG+0*NUMREGS)
	xor $(IN_RANGE_REG+1*NUMREGS), $(BOOL_TMP1_REG+1*NUMREGS), 
	$(BOOL_TMP2_REG+1*NUMREGS)
	xor $(IN_RANGE_REG+2*NUMREGS), $(BOOL_TMP1_REG+2*NUMREGS),
	$(BOOL_TMP2_REG+2*NUMREGS)
	xor $(IN_RANGE_REG+3*NUMREGS), $(BOOL_TMP1_REG+3*NUMREGS),
	 $(BOOL_TMP2_REG+3*NUMREGS)
	selb $(CURRENT_VAL_REG+0*NUMREGS), $(CURRENT_VAL_REG+0*NUMREGS),
	$(PROCESSED_VAL_REG+0*NUMREGS), $(IN_RANGE_REG+0*NUMREGS)
	selb $(CURRENT_VAL_REG+1*NUMREGS), $(CURRENT_VAL_REG+1*NUMREGS),
	$(PROCESSED_VAL_REG+1*NUMREGS), $(IN_RANGE_REG+1*NUMREGS)
.align 3
	selb $(CURRENT_VAL_REG+2*NUMREGS), $(CURRENT_VAL_REG+2*NUMREGS),
	$(PROCESSED_VAL_REG+2*NUMREGS), $(IN_RANGE_REG+2*NUMREGS)
	stqd $(CURRENT_VAL_REG+0*NUMREGS), 0*REGBYTES($BUFFER_REG)
	selb $(CURRENT_VAL_REG+3*NUMREGS), $(CURRENT_VAL_REG+3*NUMREGS),
	$(PROCESSED_VAL_REG+3*NUMREGS), $(IN_RANGE_REG+3*NUMREGS)
	stqd $(CURRENT_VAL_REG+1*NUMREGS), 1*REGBYTES($BUFFER_REG)
	stqd $(CURRENT_VAL_REG+2*NUMREGS), 2*REGBYTES($BUFFER_REG)
	stqd $(CURRENT_VAL_REG+3*NUMREGS), 3*REGBYTES($BUFFER_REG)

	ai $BUFFER_REG, $BUFFER_REG, REGBYTES
	cgt $IS_FINISHED_REG, $BUFFER_REG, $BUFFER_END_REG
loop_branch_instruction:
	brz $IS_FINISHED_REG, loop_start

end_function:
	bi $lr

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

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

Ресурсы

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



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

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux
ArticleID=336616
ArticleTitle=Программирование высокопроизводительных приложений на процессоре Cell BE: Часть 4. Программирование SPU с расчетом на производительность
publish-date=09092008