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

Программирование элементов SPE приставки Sony PLAYSTATION 3

Продолжаем углубленное рассмотрение процессорных элементов SPE (Synergistic processor elements) процессора Cell Broadband Engine™ (Cell BE) и их работы на самом низком уровне. В этой части рассматриваются вопросы упорядочивания данных в памяти и средств взаимодействия элементов SPE.

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

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



26.08.2008

Загрузка и хранение невыровненных данных

Поскольку синергический процессор (Synergistic processing unit, SPU) ориентирован на выполнение векторных, а не скалярных операций, он способен одновременно загружать и хранить 16 байтов (которым равен размер регистра) данных из адресов ячеек памяти, выровненных до 16-байтных четверных слов (quadwords). По этой причине вы не можете загрузить данные размером в одно обычное двухбайтное слово, скажем, из ячейки памяти с адресом 12. Чтобы получить эти данные, вам необходимо загрузить данные размером в четверное слово из ячейки памяти с адресом 0, а затем сдвинуть биты таким образом, чтобы требуемое значение оказалось в привилегированном (preferred) слоте регистра. Таким образом, нужно загрузить исходное четверное слово, вставить требуемое значение в нужную позицию в этом слове и затем сохранить результат. С учетом этого механизма обычно рекомендуется хранить данные выровненными до 16 байтов. Загрузка значения, которое пересекает границу в 16 байтов, является еще более сложной задачей, поскольку вам придется загружать его в два регистра, сдвигать эти регистры, а затем выполнять их маскировку и объединение. Хранить такие значения оказывается еще сложнее, поэтому лучше всего никогда не использовать значения, пересекающие 16-байтные границы.

Хотя вам позволено использовать данные, не выровненные до 16-байтных границ, методы их загрузки и хранения, которые я буду обсуждать, требуют, чтобы эти данные были естественно выровнены для предотвращения пересечения 16-байтных границ. Это означает, что слова будут выровнены до 4 байтов, полуслова – до 2 байтов, а байты вообще не будут нуждаться в выравнивании.

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

Листинг 3. Загрузка из невыровненной памяти
###Загрузка невыровненного адреса байта из регистра $3 в preferred–слот регистра###

#Загрузка из ближайшей 16-байтной границы
lqd $4, 0($3)
#Циклический сдвиг значения в начало регистра
rotqby $4, $4, $3
#Циклический сдвиг значения в preferred-слот (-3 для байтов, -2 для полуслов
# и ничего для слов или двойных слов)

rotqbyi $4, $4, -3

Помните, что инструкция lqd выполняет загрузку только в пределах 16-байтных границ. Поэтому в процессе загрузки будут проигнорированы четыре самых младших бита, и из памяти загрузится только выровненное четверное слово. Следовательно, в случае произвольных адресов мы не имеем никакого представления о том, где в загруженном четверном слове находится нужное нам значение. Инструкция rotqby (rotate (left) quadword by bytes – побайтный циклический сдвиг четверного слова влево) использует адрес, из которого произошла загрузка, для того, чтобы указать, насколько следует сдвигать регистр. Чтобы определить, в каких пределах нужно выполнять сдвиг, данная инструкция использует только четыре самых младших бита адреса в регистре (те, что были проигнорированы при загрузке). Этим значением всегда будет количество байтов, на которое необходимо выполнить сдвиг влево для того, чтобы указанный адрес переместился в начало регистра. Наконец, в случае байтов привилегированный слот расположен notв начале регистра, а на три байта правее. Поэтому инструкция rotqbyi выполняет сдвиг, используя значение непосредственного режима, на которое нужно выполнить этот сдвиг. В случае перемещений данных длиной в обычное и двойное слово выполнение этой последней инструкции не требуется, поскольку их привилегированный слот всегда расположен в начале регистра. В результате всех этих действий регистр 4 содержит окончательное значение с размещенным в привилегированном слоте байтом данных.

Хранение является более сложной задачей. Ниже приведен код для сохранения байта данных, расположенного в preferred-слоте регистра $4, в адрес, указанный регистром $3:

Листинг 4. Сохранение в невыровненный адрес
###Сохранение байта preferred-слота $4 в невыровненный адрес $3

#Загрузка данных во временный регистр
lqd $5, 0($3)
#Создание элементов управления для вставки байта
cbd $6, 0($3)
#Перестановка данных
shufb $7, $4, $5, $6
#Сохранение данных
stqd $7, 0($3)

Для того чтобы разобраться в этой непонятной на первый взгляд последовательности, снова вспомните о том, что SPU в каждый момент времени загружает и хранит только одно четверное слово, выровненное в пределах 16-байтных адресов. Поэтому, если вы хотите сохранить только один байт или если вы пытались сохранить его непосредственно по невыровненному адресу, в обоих случаях он будет располагаться по неверному адресу и затрет остальные байты в четверном слове. Чтобы избежать этой ситуации, вам необходимо сначала загрузить четверное слово из памяти, вставить значение в подходящий байт четверного слова и сохранить его назад. Сложной частью является вставка значения в нужную позицию, основанную только на адресе. К счастью, нам на помощь приходят две инструкции – cbd (generate control for byte insertion – создать элемент управления для вставки байта) и shufb (shuffle bytes – перестановка байтов). Инструкция cbd берет адрес и генерирует управляющее слово, которое может быть использовано инструкцией shufb для вставки байта в нужную позицию четверного слова для этого адреса. Команда cbd $6, 0($3) использует адрес в регистре 3 для генерации управляющего четверного слова, после чего сохраняет его в регистре 6. Команда shufb $7, $4, $5, $6 использует управляющее четверное слово из регистра 6 для генерации в регистре 7 нового значения, состоящего из исходного четверного слова, которое находилось в памяти (сейчас в регистре 5), и байта из регистра 4 в привилегированном слоте, и сохраняет результат в регистре 7. После того, как перестановка байта выполнена, значение сохраняется назад в память.

Чтобы продемонстрировать технику, я напишу функцию, которая берет адрес ASCII символа, загружает его, преобразует в верхний регистр и сохраняет назад. Я помещу функцию convert_to_upper в отдельный файл, отдельно от функцииmain чтобы впоследствии ее можно было использовать в других программах. Ниже приведен код функции main (сохраните ее как convert_main.s):

Листинг 5. Начало программы преобразования в верхний регистр
.data

string_start:
.ascii "We will convert the following letter, "
letter_to_convert:
.ascii "q"
remaining:
.ascii ", to uppercase\n\0"

.text
.global main
.type main, @function

main:
	.equ MAIN_FRAME_SIZE, 32
	.equ LR_OFFSET, 16
	#PROLOGUE
	stqd $lr, LR_OFFSET($sp)
	stqd $sp, -MAIN_FRAME_SIZE($sp)
	ai $sp, $sp, -MAIN_FRAME_SIZE

	#MAIN FUNCTION
	ila $3, letter_to_convert
	brsl $lr, convert_to_upper
	ila $3, string_start
	brsl $lr, printf

	#EPILOGUE
	ai $sp, $sp, MAIN_FRAME_SIZE
	lqd $lr, LR_OFFSET($sp)
	bi $lr

Теперь введем функцию, которая непосредственно выполняет преобразование в верхний регистр (введите как convert_to_upper.s):

Листинг 6. Функция преобразования в верхний регистр
.text
.global convert_to_upper
.type convert_to_upper, @function
convert_to_upper:
	#Register usage
	# $3 - parameter 1 -- address of byte to be converted
	# $4 - byte value to be converted
	# $5 - $4 greater than 'a' - 1?
	# $6 - $4 greater than 'z'?
	# $7 - $4 less than or equal to 'z'?
	# $8 - $4 between 'a' and 'z' (inclusive)?
	# $9 through $12 - временное хранилище окончательного результата
	# $13 - фактор преобразования

	#адрес символа, хранящегося в невыровненном адресе в регистре $3
	#UNALIGNED LOAD
	lqd $4, 0($3)
	rotqby $4, $4, $3
	rotqbyi $4, $4, -3

	#НАХОДИТСЯ ЛИ В ДИАПАЗОНЕ 'a'-'z'?
	cgtbi $5, $4, 'a' - 1
	cgtbi $6, $4, 'z'
	nand $7, $6, $6
	and $8, $5, $7
	#Вычислим ненужные биты
	andi $8, $8, 255
	#Пропуск преобразования в верхний регистр и сохранение,
	# если $4 не в нижнем регистре (на основании $8)

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

	#Невыровненное сохранение
	lqd $9, 0($3)
	cbd $10, 0($3)
	shufb $11, $4, $9, $10
	stqd $11, 0($3)

end_convert:
	#нет стекового фрейма, нет возвращаемого значения, просто возврат
	bi $lr

Для компиляции и запуска выполните следующие команды:

spu-gcc convert_main.s convert_to_upper.s -o convert
./convert

Работа функции main не слишком отличается от того, что было рассмотрено раньше, так что я не буду обсуждать это здесь. Однако обратите внимание на то, что на вход функции convert_to_upper передается address буквы, а не сама буква.

Функция convert_to_upper получает адрес случайного символа, преобразует его в верхний регистр и затем сохраняет его назад, ничего не возвращая. Эта функция никогда не вызывает никаких других функций, поэтому для нее нет необходимости использовать стековый фрейм.

Первым делом эта функция выполняет невыровненную загрузку (как это было описано ранее) в регистр 4. Затем выполняется проверка того, попадает ли байт в диапазон от a до z. Данная проверка производится путем сравнения байта со значением 'a' - 1, (операция "больше чем") и последующего сравнения со значением 'z (снова операция "больше чем"). Я не использую операцию сравнения "меньше чем" по той простой причине, что, данная операция не поддерживается процессором SPU! SPUs Процессоры SPU поддерживают только операции сравнения "больше чем" и "равно". Поэтому, если вы хотите выполнить сравнение "меньше или равно", вы должны выполнить сравнение "больше чем", а затем выполнить для полученного результата операцию "не" ("not") с помощью инструкции nand с обоими исходными аргументами, находящимися в одинаковом регистре. Затем вы комбинируете операции сравнения, используя инструкцию and (вы могли бы объединить все логические инструкции в одну инструкцию с использованием xor, но тогда код оказался бы менее понятным). Наконец, поскольку команды перехода оперируют только значениями типа полуслово или слово, вам необходимо выявить несоответствующие части регистра (мне не пришлось делать это в примере, поскольку я имею дело с целым словом).

Если все биты данных в привилегированном слоте регистра 8 установлены в false, вы переходите в конец функции. Если они установлены в true, вы выполняете преобразование. Единственной байтовой арифметической функцией SPU является функция absdb, (absolute difference of bytes – абсолютная разница байтов), которая выдает абсолютное значение разницы между двумя операндами. Эту функцию вместе с разницей между символами в верхнем и нижнем регистрах вы используете для выполнения преобразования. И наконец, вы выполняете невыровненное сохранение. Поскольку вы не вызывали никаких функций, не использовали локальных хранилищ и стековый фрейм, теперь вы можете просто выйти через регистр связи.


Взаимодействие с элементом PPE

До сих пор я концентрировал внимание только на программах для SPE. Теперь я обращу внимание на программы, работающие под управлением PPE, и для этого мне необходимо знать, как заставить взаимодействовать элементы PPE и SPE.

Каналы и контроллер MFC

Помните, что элементы SPE имеют собственную память, которая располагается отдельно от основной памяти процессора и называется локальной памятью(local store). Элемент SPE не может считывать данные из основной памяти напрямую, а вместо этого импортирует и экспортирует данные между локальной и основной памятью, обращаясь посредством команд DMA к устройству, называющемуся контроллером потоков памяти (Memory Flow Controller, MFC). Адресное пространство локальной памяти ограничено 32 битами, но обычно этот объем еще меньше (например, Sony® PLAYSTATION® 3, располагает только 18 битами). Причиной этого является то, что обращения к памяти со стороны кода SPE могут быть детерминированы. С основной памятью можно выполнять следующие действия: выгружать на диск, перемещать, кэшировать и удалять из кэша и распределять. По этой причине время, необходимое для любого отдельного обращения к памяти, совершенно неизвестно (кто знает, сколько времени может занять, например, выгрузка памяти на диск). Выделяя локальную память для каждого SPE можно обеспечить то, что элемент SPE может иметь детерминированное время доступа к любой области памяти, к которой он обращается, и запланировать контроллер MFC на асинхронное перемещение данных из основной памяти или в основную память по мере необходимости. Адресное пространство локальной памяти элементов SPE состоит из адресов локальной памяти (Local Store Addresses, LSAs), а адресное пространство основной памяти – из адресов, которые называются effective addresses (EAs). Эта информация окажется важной при изучении возможностей DMA контроллера потоков памяти.

Элементы SPE взаимодействуют с окружающим миром посредством использования каналов. Канал – это 32-разрядная область, которая может быть считана или в которую с помощью специальных инструкций могут быть записаны данные (но не то и другое одновременно, поскольку эти каналы однонаправленные). Канал также имеет глубину, или channel count. Глубина канала – это количество данных, ожидающих чтения (для каналов чтения), или количество данных, которые могут быть записаны (для каналов записи). Каналы используются для всех входных и выходных данных SPE. Они используются для передачи команд DMA контроллеру MFC, обработки событий SPE и выполнения операций чтения и записи сообщений в элементы PPE. Следующая программа, которую я покажу вам, использует контроллер MFC и интерфейс канала для символьных преобразований данных, указанных элементом PPE.

Создание и выполнение заданий SPE

До сих пор функция main не использовала никаких параметров. Однако когда она выполняется из программы PPE, фактически она получает три 64-битных параметра – идентификатор задания SPE, содержащийся в регистре 3, указатель на параметры приложения, содержащийся в регистре 4, и указатель на информацию среды выполнения, содержащийся в регистре 5. Содержимое областей, на которые указывают указатели приложения и среды выполнения, фактически определяется пользователем. Тем не менее, помните, что указатели указывают на область в основной памяти приложений (effective address), а не на область локальной памяти элемента SPE. Поэтому доступ к ним не может быть получен напрямую, а необходимо переместить их через DMA.

Задания SPE создаются с помощью функции speid_t spe_create_thread(spe_gid_t spe_gid, spe_program_handle_t *spe_program_handle, void *argp, void *envp, unsigned long mask, int flags). Параметры работают следующим образом:

  • spe_gid
    Этот параметр определяет группу потока SPE, которому назначается задание. Этот параметр можно просто установить равным нулю.
  • spe_program_handle
    Этот параметр является указателем на структуру, которая содержит данные о самой программе SPE. Эти данные обычно определяются автоматически одним из следующих способов: путем внедрения приложения SPU внутрь исполняемого файла PPU (это будет показано позже) с помощью команды dlopen()/dlsym() для библиотеки, содержащей приложение SPU, или путем использования команды spe_open_image() для прямой загрузки приложения SPU.
  • argp
    Этот параметр является указателем на относящиеся к приложению данные, необходимые для инициализации программы. Если вы не планируете использовать этот параметр, установите его равным нулю.
  • envp
    Этот параметр является указателем на данные среды выполнения программы. Если вы не планируете использовать этот параметр, установите его равным нулю.
  • mask
    Этот параметр является числом, в котором каждый бит отмечает процессор, на котором разрешено запускать процесс (affinity mask). Установите этот параметр равным -1, чтобы процесс мог запускаться на любом доступном SPE. В противном случае параметр содержит битовую маску для каждого доступного процессора. 1 означает, что процессор должен использоваться, 0 означает, что процессор использоваться не должен. Для большинства приложений этот параметр устанавливается равным -1.
  • flags
    Этот параметр представляет собой набор битовых флагов, которые влияют на настройку SPE. Рассмотрение этого вопроса выходит за рамки данной статьи.

Приложение PPE/SPE, использующее DMA

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

Основная программа SPE будет получать указатель, указывающий на структуру в основной памяти, содержащую размер и указатель строки. Затем приложение скопирует этот указатель в свой буфер, выполнит преобразование и скопирует данные назад. Ниже приведен SPE-код (введите как convert_dma_main.s):

Листинг 7. SPU-код выполнения преобразования в верхний регистр для программы PPU
.data

.align 4
conversion_info:
conversion_length:
	.octa 0
conversion_data:
	.octa 0
.equ CONVERSION_STRUCT_SIZE, 32

.section .bss #Раздел неинициализированных данных
.align 4
.lcomm conversion_buffer, 16384

.text
.global main
.type main, @function

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

#Константы стекового фрейма
.equ MAIN_FRAME_SIZE, 80
.equ MAIN_REG_SAVE_OFFSET, 32
.equ LR_OFFSET, 16

main:
	#Пролог
	stqd $lr, LR_OFFSET($sp)
	stqd $sp, -MAIN_FRAME_SIZE($sp)
	ai $sp, $sp, -MAIN_FRAME_SIZE

	#Сохранение регистров
	#Сохранение регистра $127 (будет использоваться для текущего индекса)
	stqd $127, MAIN_REG_SAVE_OFFSET($sp)
	#Сохранение регистра $126 (будет использоваться для базового указателя)
	stqd $126, MAIN_REG_SAVE_OFFSET+16($sp)
	#Сохранение регистра $125 (будет использоваться для окончательного размера)
	stqd $125, MAIN_REG_SAVE_OFFSET+24($sp)

	##КОПИРОВАНИЕ ИНФОРМАЦИИ ПРЕОБРАЗОВАНИЯ##
	ila $3, conversion_info         #Адрес локальной памяти
	#register 4 already has address #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

	#ЦИКЛ ПО БУФЕРУ
	#Загрузка размера буфера
	lqr $125, conversion_length
	#Загрузка указателя буфера
	ila $126, conversion_buffer
	#Загрузка индекса буфера
	il $127, 0
loop:
	ceq $7, $125, $127
	brnz $7, loop_end

	#Вычисление адреса для параметра функции
	a $3, $127, $126
	#Следующий индекс
	ai $127, $127, 1

	#Запуск функции
	brsl $lr, convert_to_upper

	#Повтор цикла
	br loop

loop_end:
        #Копирование данных назад
        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. Введите эти функции в качестве dma_utils.s:

Листинг 8. Утилиты передачи DMA
##ФУНКЦИЯ ДЛЯ ВЫПОЛНЕНИЯ ОПЕРАЦИЙ DMA##
#Параметры: Адрес локальной памяти, 64-битный адрес основной памяти, 
Размер передачи, Тэг DMA, Команда DMA
.global perform_dma
.type perform_dma, @function
perform_dma:
	shlqbyi $9, $4, 4  #Get the low-order 32-bits of the address
	wrch $MFC_LSA, $3
	wrch $MFC_EAH, $4
	wrch $MFC_EAL, $9
	wrch $MFC_Size, $5
	wrch $MFC_TagID, $6
	wrch $MFC_Cmd, $7
	bi $lr

.global wait_for_dma_completion
.type wait_for_dma_completion, @function
wait_for_dma_completion:
	#Мы получаем тэг в регистре 3 – преобразуем в маску тэга
	il $4, 1
	shl $4, $4, $3
	wrch $MFC_WrTagMask, $4
	#Указываем DMA, что нужно только проинформировать нас
	 о завершении выполнения работы DMA
	il $5, 2
	wrch $MFC_WrTagUpdate, $5
	#Ожидание завершения работы DMA и сохранение результата в возвращаемом значении
	rdch $3, $MFC_RdTagStat
	#Возврат
	bi $lr

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

spu-gcc convert_dma_main.s dma_utils.s convert_to_upper.s -o spe_convert
embedspu -m64 convert_to_upper_handle spe_convert spe_convert_csf.o

Эти команды производят то, что называется CESOF Linkable, и позволяет объектному файлу для SPE быть внедренным в приложение PPE и загруженным по мере необходимости.

Ниже приведен код PPU, позволяющий использовать код SPU (введите как ppu_dma_main.c):

Листинг 9. Код PPU для использования приложения SPU
#include <stdio.h>
#include <libspe.h>
#include <errno.h>
#include <string.h>

/*  фактически embedspu определяет это в генерируемом объектном файле, 
   здесь нам нужна лишь внешняя ссылка
 */
extern spe_program_handle_t convert_to_upper_handle;

/* Это структура параметров, которую ожидает наш код SPE */
/* Обратите внимание, что все передаваемые SPE данные будут выровнены до 16 байтов */
typedef struct {
	int length __attribute__((aligned(16)));
	unsigned long long data __attribute__((aligned(16)));
} conversion_structure;

int main() {
	int status = 0;
	/* Заполним строку до четверного слова - в конец добавлено 12 знаков пробела. */
	char *tmp_str = "This is the string we want to convert to uppercase.            ";
	/* Скопируем ее в выровненную границу */
	char *str = memalign(16, strlen(tmp_str) + 1);
	strcpy(str, tmp_str);
	/* Создание структуры преобразования на выровненной границе */
	conversion_structure conversion_info __attribute__((aligned(16)));

	/* Задание элементов данных в структуре параметров  */
	conversion_info.length = strlen(str) + 1; /* add one for null byte */
	conversion_info.data = (unsigned long long)str;

	/* Создание потока и проверка на отсутствие ошибок */
	speid_t spe_id = spe_create_thread(0, &convert_to_upper_handle, 
	&conversion_info, NULL, -1, 0);
	if(spe_id == 0) {
		fprintf(stderr, "Unable to create SPE thread: errno=%d\n", errno);
		return 1;
	}

	/* Ожидание завершения работы потока SPE */
	spe_wait(spe_id, &status, 0);

	/* Вывод результата */
	printf("The converted string is: %s\n", str);

	return 0;
}

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

gcc -m64 spe_convert_csf.o ppu_dma_main.c -lspe -o dma_convert
./dma_convert

В этом коде происходят самые различные вещи, и моя цель – представить весь необходимый материал так, чтобы мы не увязли в нем при изучении секретов оптимизации в следующей статье (оставайтесь со мной, и вы научитесь программировать процессоры SPU в два счета!). А сейчас я объясню, что происходит в вышеприведенном коде. Я начну рассмотрение с кода PPU, поскольку он немного проще.

Первая интересная часть кода PPU заключается в добавлении заголовочного файла libspe.h содержащего все объявления функций для запуска программ на SPE. Затем происходит обращение к дескриптору под названием convert_to_upper_handle. Это всего лишь внешняя ссылка типа extern а не само объявление, поскольку дескриптор convert_to_upper_handle определен в модуле spe_convert_csf.o. Имя переменной было задано в командной строке команды embedspu command. Эта переменная является дескриптором к программному коду, который будет использоваться для создания ваших заданий SPE.

Далее вы определяете структуру, которая будет использоваться в качестве параметра вашей программы SPE. Вам необходимы длина строки и сам указатель на строку, которые должны быть выровнены до четверного слова, чтобы вы могли скопировать их в вашу основную программу и использовать значения с операциями DMA. Заметьте, что указатель, который вы использовали, был объявлен как unsigned long longа не как просто указатель. Это сделано для того чтобы передача адреса хранилась одинаково вне зависимости от того, была ли компиляция выполнена в 32- или 64-разрядном режиме. В случае использования обычного указателя при выполнении компиляции в 32-разрядном режиме, он будет выровнен в структуре иначе, чем при 64-разрядной компиляции. Также вам необходимо использовать функцию memalign и командуstrcpy для копирования данных в область соответствующего выравнивания. Вот вам подсказка, которая была получена путем долгих проб и ошибок при работе с этими вещами: если вы постоянно получаете сообщение об ошибке "bus error", то, вероятно, вы выполняете передачу DMA, которая либо не выровнена до 16 байтов, либо не является кратной 16 байтам.

В основной программе вы объявляете ваши переменные. Заметьте, что все объявленные переменные, которые будет скопированы с использованием DMA, выровнены до границ четверного слова и являются кратными четверному слову. Это обусловлено тем, что переносы DMA (за несколькими исключениями для случаев небольших переносов) , должны быть выровнены до четверного слова как в исходных, так и в конечных адресах (производительность программы будет еще лучше, если и исходные, и конечные адреса выровнены до 128- байтов ). Далее с помощью spe_create_thread, создается задание SPE. Теперь вы можете просто дождаться завершения задания SPE, используя spe_wait, и затем вывести конечное значение. Как вы могли догадаться, большинство интересных моментов программы происходит именно в SPE, включая все передачи DMA. Передачи DMA почти всегда выполняются элементами SPE, а не PPE, поскольку SPE могут обрабатывать намного больше данных и выполнять больше активных операций DMA, нежели PPE.

Прежде чем углубляться в детали основной программы, я объясню работу утилиты DMA. Первая функция, perform_dma, как и следует ожидать, выполняет команды DMA. Последовательность операций с каналами, необходимых для выполнения передачи DMA, определена в руководстве "Cell BE Handbook" на страницах 450-456 (обратитесь к разделу Ресурсы). В первую очередь функция преобразует 64-разрядный адрес основной памяти в регистре 4 в два 32-разрядных компонента высшего и низшего порядка (помните, что каналы являются лишь 32-разрядными). Поскольку запись каналов осуществляется с использованием привилегированного слота регистра, размером в слово, то биты высшего порядка 64-разрядного адреса уже находятся в привилегированном слоте. Поэтому для того чтобы биты низшего порядка оказались в привилегированном слоте, вы лишь выполняете циклический сдвиг содержимого влево на четыре байта в новый регистр. После этого вы записываете адрес локальной памяти, биты высшего порядка адреса основной памяти, биты низшего порядка основной памяти, размер передачи, "тэг" команды DMA и затем саму команду в их соответствующие каналы с помощью инструкции wrch. Когда команда записана, запрос DMA помещается в очередь контроллера MFC, который сообщает, что у него имеются свободные слоты (в вашем случае они наверняка имеются, поскольку вы не выполняете никаких других конкурирующих запросов DMA). "Тэг" – это число, которое можно назначить одной или нескольким командам DMA. Все команды DMA выполняются с одним и тем же тэгом и считаются одной группой; обновления статуса и операции упорядочения применяются к этой группе в целом. В этом приложении в каждый момент времени у вас будет активна только одна команда DMA, поэтому в качестве тэга DMA все ваши операции будут использовать значение 0. Командой DMA должна быть MFC_GET_CMD или MFC_PUT_CMD. Существуют и другие команды, но здесь они нам будут не интересны. Все команды контроллера MFC выполняются с перспективы элемента SPE независимо от того, была ли команда сгенерирована элементом SPE, или нет. Итак, MFC_GET_CMD перемещает данные из основной памяти в локальную, а MFC_PUT_CMD наоборот.

Поскольку команды DMA асинхронны, полезно иметь возможность дожидаться их завершения. Именно это делает функция wait_for_dma_completion Эта функция берет тэг в качестве своего единственного параметра, преобразует его в тэговую маску, запрашивает статус DMA и затем считывает его. Но как эта функция ожидает завершения операции DMA? Когда в канал $MFC_WrTagUpdate выполняется запись значения 2, канал $MFC_RdTagStatне получает значение до тех пор, пока операция не будет завершена. Таким образом, когда вы попытаетесь прочесть канал с помощьюrdch, он будет заблокирован до тех пор, пока не будет доступен статус, после чего передача будет завершена.

Теперь перейдем непосредственно к самой программе. Первая операция, которую выполняет наша программа SPE, это резервирование места для данных параметра приложения, которое выравнивается до границ четверного слова (инструкция .align 4 в языке ассемблера действует так же, как __attribute__((aligned(16)))в языке C, поскольку 2^4 = 16). Инструкция .octa резервирует 16-байтные значения (название инструкции – это пережиток прошлого со времен 16-разрядных операций). Затем вы определяете константуCONVERSION_STRUCT_SIZE для задания размера всей структуры.

После этого вы переходите к разделу .bss который подобен разделу.data исключением того, что сам исполняемый модуль не содержит значений, а только отмечает, сколько места должно быть зарезервировано для них. Этот раздел предназначен для неинициализированных данных. Инструкция .lcomm conversion_buffer, 16384 резервирует область размером в 16 Кбайт, начиная с адреса, определенного в идентификатореconversion_buffer. Данная область определена для хранения 16 Кбайт данных потому, что этот размер является максимальным размером передачи DMA контроллера потоков памяти. По этой причине, если длина какой-то строки превышает 16 Кбайт, элемент PPE будет вынужден вызвать программу несколько раз (более "умная" программа просто разделила бы запрос на несколько фрагментов на стороне SPE).

Функция main является главной частью программы. Она начинается с настройки стекового фрейма. Затем она сохраняет три энергозависимых регистра, которые будут использоваться для основного управления программой. После этого программа выполняет перенос DMA, чтобы скопировать данные в структуру параметра из элемента PPE. Помните, что первый параметр функции – это 64-разрядный адрес, который был передан ей из элемента PPE. Следующим шагом вы используете команду DMA, чтобы вызвать из памяти полную структуру, и ожидаете завершения работы DMA. После выполнения передачи вы используете данные в этой структуре, чтобы скопировать саму строку в ваш буфер локальной памяти, используя другой перенос DMA, а затем ожидаете завершения этой операции. Заметьте, что вы использовали инструкцию ila (immediate load address – немедленная загрузка адреса) для загрузки адреса в буфер. Инструкция ila полностью использует 18 битов, что работает для приставки PLAYSTATION 3. Тем не менее, если процессор Cell BE имеет больший размер локальной памяти, то вместо этого вы могли бы выполнить загрузку с помощью следующих двух инструкций:

ilhu $3, conversion_buffer@h #load high-order 16 bits of conversion_buffer
iohu $3, conversion_buffer@l #"or" it with the low-order 16 bits of conversion_buffer

Затем целевой адрес основной памяти, длина строки, тэг DMA и команда DMA MFC_GET_CMD передаются функции perform_dma. После этого программа ожидает завершения операции.

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

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


Взаимодействие между SPE и PPE с использованием очередей сообщений (mailboxes)

Хотя передачи DMA являются отличным способом для перемещения больших объемов данных между элементами SPE и PPE, другим, более простым методом для небольших переносов, который я вкратце рассмотрю, является метод с использованием очередей сообщений (mailboxes). Для SPE это просто набор каналов (канал чтения и канал записи), предназначенных для записи 32-разрядных значений в PPE. Чтобы продемонстрировать данную концепцию, я напишу очень простой сервер SPE, который ожидает в очереди сообщений целое число без знака, а затем записывает обратно квадрат этого числа. Ниже приведен код (введите как square_server.s):

Листинг 10. SPU-сервер возведения в квадрат
.text
.global main
.type main, @function
main:
	#Чтение из очереди входящих сообщений (остановлено до тех пор, 
	#пока не появится  какое-нибудь значение)

	rdch $3, $SPU_RdInMbox
	#Возведение полученного значения в квадрат
	mpyu $3, $3, $3
	#Запись значения обратно
	wrch $SPU_WrOutMbox, $3
	#Возвращаемся назад и делаем все снова

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

Код на стороне PPE не намного сложнее (введите как square_client.c):

Listing 11. PPE squaring client
#include <libspe.h>
#include <stdio.h>

extern spe_program_handle_t square_server_handle;

int main() {
	int status = 0;

	/* Создание потока SPE */
	speid_t spe_id = spe_create_thread(0, &square_server_handle, NULL, NULL, -1, 0);
	if(spe_id == 0) {
		fprintf(stderr, "Unable to create SPE thread!\n");
		return 1;
	}

	/* Запрос квадратичного значения */
	spe_write_in_mbox(spe_id, 4);
	/* Ожидание, когда результат будет доступен  */
	while(!spe_stat_out_mbox(spe_id)) {}
	/* Чтение и отображение результата */
	printf("The square of 4 is %d\n", spe_read_out_mbox(spe_id));

	/* Делаем все снова */
	spe_write_in_mbox(spe_id, 10);
	while(!spe_stat_out_mbox(spe_id)) {}
	printf("The square of 10 is %d\n", spe_read_out_mbox(spe_id));

	return 0;
}

Для компиляции и запуска этой программы выполните следующие команды:

spu-gcc square_server.s -o square_server
embedspu -m64 square_server_handle square_server square_server_csf.o
gcc -m64 square_client.c square_server_csf.o -lspe -o square
./square

Даже для PPE очереди сообщений (mailboxes) именуются в соответствии с перспективой SPE. Таким образом, вы помещаете данные в очередь входящих сообщений и считываете их из очереди исходящих сообщений так, будто вы являетесь элементом PPE. В отличие от SPE, при выполнении операций записи и чтения элемент PPE не переходит в режим ожидания и не ждет появления значения в очереди. Вместо этого программа должна использовать инструкцию spe_stat_out_mbox для ожидания появления значения и инструкцию spe_stat_in_mbox для проверки того, остались ли в очереди сообщений слоты для записи. Вы не используете последнюю инструкцию, поскольку в каждый момент времени вы работаете только с одним значением.

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


Заключение

До сих пор в этой серии рассматривались основные концепции программирования на языке ассемблера для процессора Cell BE приставки PLAYSTATION 3, работающей под управлением Linux®. В темах статей рассматривались базовая архитектура, синтаксис языка ассемблера SPU и основные режимы взаимодействия между элементами SPE и PPE. В следующей статье будет рассмотрено, как выжать из элементов SPE процессора Cell BE максимум производительности. В последующих статьях мы применим эти знания для программирования SPE на языке 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=332969
ArticleTitle=Программирование высокопроизводительных приложений на процессоре Cell BE: Часть 3. Знакомьтесь с процессором SPU
publish-date=08262008