Программирование высокопроизводительных приложений на процессоре Cell BE, Часть 2: Программирование процессоров SPE (synergistic processing elements)

Обзор процессоров SPE

В этой части серии статей Программирование высокопроизводительных приложений на процессоре Cell BE мы продолжаем рассказывать о преимуществах synergistic processing elements (SPE) на Sony® PLAYSTATION® 3 (PS3). В первой части было показано, как установить Linux® на PS3 и в качестве примера была рассмотрена небольшая программа. Во второй части мы глубже познакомимся с SPE процессора Cell Broadband Engine™ и рассмотрим механизмы работы SPE на низшем уровне.

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

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



04.09.2007

Предыдущая статья в этой серии посвящена обзору процессора Cell Broadband Engine (Cell BE). (Другие обзоры см. в разделе Ресурсы в конце этой статьи). Часть 2 начинается с подробного обсуждения SPE-чипа Cell BE. (Углубленное обсуждение программирования Power processing element (PPE) ищите в серии статей Ассемблер для Power-архитектуры в разделе Linux сайта developerWorks.) Поскольку процессоры SPE построены по иной архитектуре, полезно взглянуть на них с точки зрения ассемблера, чтобы понимать то, что происходит. Позже будет показано, как программировать их на языке С, но язык ассемблера дает более полное представление о своеобразии этого процессора. Тогда, когда вы потом перейдете на С, вы уже будете понимать, как по-разному написанный код может влиять на исполнение. Эта статья посвящена базовому синтаксису и использованию ассемблера SPE, а также ABI (application binary interface (бинарный интерфейс приложений) обеспечивает переносимость между платформами). В двух последующих статьях будут исследоваться связи между SPE и PPE и будет рассказано о том, как использовать уникальные особенности ассемблера SPE для оптимизации кода.

Как уже отмечалось в предыдущей статье, чип Cell BE состоит из PPE, который имеет несколько SPE. PPE отвечает за функционирование операционной системы, управляет ресурсами и вводом/выводом. Процессоры SPE отвечают за задачи, связанные с обработкой данных. Процессоры SPE не имеют прямого доступа к основной памяти, а имеют только к небольшой (256Кб на PS3) локальной памяти (local store, LS), которая располагается в независимом 32-битном адресном пространстве. Адрес внутри адресного пространства локальной памяти называется local store address (LSA), а адрес для контролирующего процесса PPE называется effective address (EA). Процессоры SPE включают в себя присоединенный контроллер потока памяти (memory flow controller или MFC). Процессоры SPE используют MFC для перемещения данных между локальной памятью, основной памятью и другими SPE.

Синергический обрабатывающий элемент (synergistic processing unit, SPU) – это часть процессора SPE, которая, собственно, и исполняет код. SPU имеет 128 регистров общего назначения, каждый на 128 бит. Однако задача SPU не в обработке 128-битных величин. Вместо этого процессор является векторным (vector) процессором. Это означает, что каждый регистр разделен на несколько меньших по размеру полей, и команды действуют на все эти величины одновременно. Обычно регистры рассматриваются как четыре независимых 32-битных числа (32 бита считается размером слова для SPU), хотя они могут также рассматриваться как шестнадцать 8-битовых чисел (байтов), восемь 16-битовых чисел (halfword), два 64-битных числа (doubleword) или как отдельное 128-битное число (quadword). Код в этой статье, на самом деле, не векторный (также говорят скалярный (scalar)), что означает, что он работает одновременно только с одним числом. Будут использоваться несколько векторных операций, но мы будем рассматривать только числа внутри каждого регистра, другие мы будем попросту игнорировать. Позже статьи в этой серии будут затрагивать и векторные операции.

Для понимания материала этой статьи не требуется наличия у вас опыта в использовании языка ассемблера, хотя это было бы полезно. Некоторые особенности процессоров SPE будут сравниваться и противопоставляться особенностям PPE, но знание PPE также не требуется. Обсуждение тех особенностей PPE, которые основаны на архитектуре Power, вы найдете в серии статей Ассемблер для Power-архитектуры.

Команды сборки, описанные в данной статье, предполагают, что у вас имеется Yellow Dog Linux, установленный в соответствии с инструкциями в Части 1. Если вы используете другой дистрибутив, некоторые команды и флаги могут быть другими. Например, если вы используете IBM System Simulator от 1.2 SDK (1.2 SDK поставляется вместе с YDL, но IBM уже выпустила 2.0 SDK), тогда вы должны изменить все обращения к gcc на ppu-gcc и все обращения к embedspu на ppu-embedspu. В зависимости от того, где установлены библиотеки и файлы заголовков, для того чтобы найти их, может возникнуть необходимость передать дополнительные флаги.

Пример простой программы

Чтобы начать рассмотрение языка ассемблера SPU, я приведу здесь простую программу для вычисления факториала 32-битного числа, которая использует рекурсивный алгоритм. Рекурсивная природа алгоритма поможет нагляднее объяснить стандартный ABI.

Для сравнения привожу здесь код на языке С, который выполнял бы ту же самую операцию:

Листинг 1. Программа, вычисляющая факториал (версия на языке С)
int number = 4;
int main() {
	printf("The factorial of %d is %d\n", number, factorial(number);
}

int factorial(int num) {
	if(num == 0) {
		return 1;
	} else {
		return num * factorial(num - 1);
	}
}

Теперь мы приведем здесь версию этой же программы на ассемблере и затем обсудим, что означает каждая строка. Не удивляйтесь размеру текста программы – это в основном комментарии и объявления (сама по себе функция вычисления факториала занимает всего 16 строк). Введите следующий текст как factorial.s.

Листинг 2. Первая программа для SPE
###DATA SECTION###
.data

##GLOBAL VARIABLE##
#Alignment is _critical_ in SPU applications.
#This aligns to a 16-byte (128-bit) boundary
.align 4
#This is the number
number:

        .long 4

.align 4
output:
	.ascii "The factorial of %d is %d\n\0"

##STACK OFFSETS##
#Offset in the stack frame of the link register
.equ LR_OFFSET, 16
#Size of main's stack frame (back pointer + return address)
.equ MAIN_FRAME_SIZE, 32
#Size of factorial's stack frame (back pointer + return address + local variable)
.equ FACT_FRAME_SIZE, 48
#Offset in the factorial's stack frame of the local "num" variable
.equ LCL_NUM_VALUE, 32


###CODE SECTION###
.text

##MAIN ENTRY POINT
.global main
.type main,@function
main:
	#PROLOGUE#
	stqd $lr, LR_OFFSET($sp)
	stqd $sp, -MAIN_FRAME_SIZE($sp)
	ai $sp, $sp, -MAIN_FRAME_SIZE

	#FUNCTION BODY#
        #Load number as the first parameter (relative addressing)
        lqr $3, number

        #Call factorial
        brsl $lr, factorial

	#Display Factorial
	#Result is in register 3 - move it to register 5 (third parameter)
	lr $5, $3
	#Load output string into register 3 (first parameter)
	ila $3, output
	#Put original number in register 4 (second parameter)
	lqr $4, number
	#Call printf (this actually runs on the PPE)
	brsl $lr, printf

	#Load register 3 with a return value of 0
	il $3, 0

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

##FACTORIAL FUNCTION
factorial:
        #PROLOGUE#
        #Before we set up our stack frame,
        #store link register in caller's frame
        stqd $lr, LR_OFFSET($sp)
        #Store back pointer before reserving the stack space
        stqd $sp, -FACT_FRAME_SIZE($sp)
        #Move stack pointer to reserve stack space
        ai $sp, $sp, -FACT_FRAME_SIZE
        #END PROLOGUE#

        #Save arg 1 in local variable space
        stqd $3, LCL_NUM_VALUE($sp)
        #Compare to 0, and store comparison in reg 4
        ceqi $4, $3, 0
        #Do we jump? (note that the "zero" we are comparing
        #to is the result of the above comparison)
        brnz $4, case_zero

case_not_zero:
        #remove 1, and use it as the function argument
        ai $3, $3, -1
        #call factorial function (return value in reg 3)
        brsl $lr, factorial
        #Load in the value of the current number
        lqd $5, LCL_NUM_VALUE($sp)
        #multiply the last factorial answer with the current number
        #store the answer in register 3 (the return value register)
        mpyu $3, $3, $5

	#EPILOGUE#
        #Restore previous stack frame
        ai $sp, $sp, FACT_FRAME_SIZE
        #Restore link register
        lqd $lr, LR_OFFSET($sp)
        #Return
        bi $lr

case_zero:
        #Put 1 in reg 3 for the return value
        il $3, 1
	##EPILOGUE##
        #Restore previous stack frame
        ai $sp, $sp, FACT_FRAME_SIZE
        #Return
        bi $lr

Чтобы собрать программу, используйте компилятор С:

spu-gcc -o factorial factorial.s

Процессор Cell BE не может запускать SPE-программы напрямую. В действительности требуется, чтобы основной код был написан так, чтобы ресурсами управлял процессор PPE. Однако, если в Linux есть программа, написанная именно для SPE, и пакет elfspe правильно установлен, то Linux автоматически создаст минимальный PPE-процесс, чтобы управлять ресурсами для SPE, и будет выступать в роли супервизора для процессора SPE. Таким образом, если у вас установлен пакет elfspe, вы можете запустить SPE-программу обычным образом:

./factorial

Если программа не работает, убедитесь, что пакет elfspe установлен соответствующим образом (см. инструкции в предыдущей статье).

А теперь посмотрим, что делает каждая команда и объявление.

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

Секция с данными содержит число, факториал которого мы хотим вычислить, в месте, помеченном как number. Помещение строкового литерала в начало строки и двоеточия за ним означает, что адреса следующих команд и объявлений могут использоваться в любом месте программы через указание этой метки. Таким образом на протяжении всего кода, если вы где-нибудь увидите number, это будет отсылать вас к адресу следующей величины. .long – это объявление 32-битной величины, хранимой в 32-битном пространстве. В данном случае мы сохраняем число 4.

Заметим однако, что до определения number вы выравниваете его, используя .align 4. Операция .align дает ассемблеру указание выровнять следующую команду или объявление по определенной границе. .align 4 выравнивает следующее местоположение памяти по 16-байтной (2^4) границе. Это принципиально, так как SPU может загружать только 16 байтов одновременно, выровненных в точности до 16-байтной границы. Если данный адрес не является ограниченным 16 байтами, до его загрузки последние четыре бита в адресе просто обнуляются, так что он будет загружаться выровненным. Следовательно, если ваше число не является правильно выровненным, оно могло бы загрузиться куда-нибудь во внутренний регистр, вероятно, не в тот регистр, в который вы ожидаете. Выравнивая число по 16-байтной границе, вы получаете уверенность, что оно будет загружаться в первые 4 байта регистра. После чего следует другая выравнивающая установка в начале строки, которая управляет вашим выводом. Объявление .ascii информирует ассемблер, что далее следует ASCII-последовательность, которая в явном виде завершается символом \0.

После этого вы определяете несколько констант для своих стековых фреймов. Запомните, что когда программа выполняет вызов функции (особенно если функция рекурсивная), она должна сохранить ее адрес возврата и локальные переменные в стек. В языке C и в других языках высшего уровня язык сам управляет стеком. В ассемблере это делает программист в явной форме. Стек выделяется для вас операционной системой при запуске программы. Стек начинается с адресов со старшими номерами и растет в сторону младших адресов по мере добавления стековых фреймов. Вы должны зарезервировать для каждого стека место, куда будут размещаться соответствующие числа. В этой программе у вас будут стековые фреймы двух размеров, один для main и один для factorial. Каждый стековый фрейм содержит указатель на предыдущий стековый фрейм (называемый обратным указателем списка (back chain pointer)), а также пространство для адресов возврата, используемое при вызове из других функций. В то время как каждый из них занимает одно слово (4 байта), они все выровнены до 16 байт для упрощения загрузки и хранения (помните, что SPU загружает только из адресов, выровненных до 16 байтов). Оставшееся место используется для сохранения регистров и хранения локальных переменных. Стек main будет занимать минимум 32 байта, а стек factorial – 48, поскольку factorial использует локальные переменные, которые нужно хранить. Чтобы именовать эти величины внутри программы и сделать код более читабельным, мы присвоим этим числам символы посредством команды .equ. Она сообщает ассемблеру, что нужно поставить данный символ в соответствие данному числу. Размеры стековых фреймов приписываются MAIN_FRAME_SIZE и FACT_FRAME_SIZE, соответственно. LR_OFFSET – это смещение в стековом фрейме адреса возврата. LCL_NUM_VALUE – это смещение в стеке локальной переменной num. Все это будет использоваться, что бы сделать доступ к смещениям стекового фрейма в основной части кода более очевидным.

В этом разделе кода вы определяете адрес функции таким же образом, как вы выше определили адреса для глобальных переменных, – просто указанием имени и следующего за ним двоеточия. Это показывает, что адрес функции будет адресом следующей команды. Вы используете .type, чтобы сообщить компоновщику, что это число должно быть использовано как функция, и вы используете .global, чтобы сообщить компоновщику, что на этот символ можно сослаться извне текущего файла. Функция main должна быть объявлена глобальной, так как она используется как точка входа программы. Далее рассмотрим непосредственно сами команды ассемблера.

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

Первая настоящая команда, с которой вы сталкиваетесь, – это lqr $3, number. Это означает "загрузку четверного слова (quadword)". Размер "quadword" немного избыточен, но это потому, что в SPU разрешена загрузка и хранение только quadword. Эта команда загружает число в адресе number (зашифрованное как относительный адрес из рассматриваемой команды) в регистр 3. В отличие от ассемблера PPE, в ассемблере SPE регистры всегда предваряются символом доллара. Это упрощает маркировку регистров в коде. Так как все регистры в SPU имеют длину в 16 байт, они будут загружать полное 16-байтное четверное слово (quadword) в регистр, даже несмотря на то, что вас на самом деле интересуют из него только первые 4 байта.

То, что вы хотите сделать с числом в регистре 3 – так это сосчитать его факториал. Следовательно, вам нужно передать его как первый (и единственный) параметр функции factorial. SPU ABI, также как и PPU ABI, имеет регистры, чтобы передавать числа функциям. Регистр 3 должен содержать первый параметр, регистр 4 – второй и так далее. Следовательно, число, которое вы загрузили в регистр 3, уже правильно размещено для использования функцией. Хотя регистры могут хранить несколько чисел (в данном случае четыре 32-битных числа), когда функции передаются параметры, каждое значение параметра передается через его собственный регистр.

Возникает вопрос, для чего же используются регистры? Для тех, кто никогда ранее не программировал на ассемблере, сообщаем, что регистры являются временным хранилищем, которое процессоры используют для операций с числами. Так как SPU имеет 128 регистров, он может хранить большое количество временных и промежуточных значений без загрузки обратно в память и хранения там, как это делается в других архитектурах. Это и упрощает программирование, и увеличивает скорость исполнения. В то время как SPU не делает различий при использовании регистров, в стандарте ABI дело обстоит иначе. В следующей таблице показано, как ABI использует каждый регистр в SPU:

Использование регистров в SPU ABI
Область регистровТип Назначение
0Выделенный (Dedicated)Регистр связи (Link Register)
1Выделенный (Dedicated)Указатель стека (Stack Pointer)
2Изменяющийся (Volatile)Указатель среды (Environment Pointer) (для языков, которые нуждаются в этом)
3-79Изменяющийся (Volatile)Аргументы функций, числа возврата и общее использование
80-127Не изменяющийся (Non-volatile) Используется для локальных переменных. Должен сохраняться в течение всех вызовов функции.

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

Так как вы хотите получить факториал числа 4, оно поступает в регистр 3, регистр, использованный для первого параметра. Затем управление передается функции при помощи brsl $lr, factorial. brsl означает "branch relative and set link". Эта инструкция выполняет переход к входной точке функции и устанавливает link register (LR) на следующую команду в качестве адреса возврата. Заметим, что когда вы выполняете brsl, вы оговариваете $lr для регистра. Это псевдоним для $0. Заметим также, что вы должны указать регистр связи в явном виде. В SPU нет специальных регистров. Регистр связи является выделенным только в силу соглашения, ассемблер SPU позволяет вам устанавливать связь в любой регистр на выбор. Тем не менее, для большинства задач это будет $lr.

После вычисления факториала вы хотите напечатать его, используя printf. Первым параметром для printf является адрес выходной строки. Следовательно, сначала нужно переместить результат из регистра 3 в регистр 5 (регистр 4 будет сохранять оригинальное число). Затем вам нужно переместить адрес output в регистр 3. ila является специальной командой загрузки, которая загружает статические адреса, в данном случае загружая адрес выходной строки в регистр 3. Она загружает 18-битовые беззнаковые величины, что является истинным размером для адресов локальной памяти в PS3. И наконец, требуемое число загружается в регистр 4. Функция printf вызывается с помощью brsl $lr, printf. Пожалуйста, заметьте, однако, что printfне выполняется на SPE, поскольку SPE не способен осуществлять операции ввода/вывода. В действительности выполняется stub-функция, которая останавливает SPE-процессор, подает сигнал процессору PPE, и уже процессор PPE реально выполняет вызов функции, после этого контроль возвращается к SPE.

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

Перед тем как перейти к обсуждению функции factorial, давайте внимательно рассмотрим схему стекового фрейма. Вот как предполагается распределить стековое пространство в соответствии с ABI:

СодержитРазмерНачало стекового смещения
Область сохранения регистров Изменяется (кратно 16 байтам) Изменяется
Область локальных переменных Изменяется (кратно 16 байтам) Изменяется
Список параметров Изменяется (кратно 16 байтам) 32($sp)
Область сохранения регистров связи 16 байтов16($sp)
Обратный указатель списка 16 байтов 0($sp)

Обратный указатель списка (back chain pointer) указывает на обратный указатель списка предыдущего стекового фрейма. Область хранения регистра связи хранит содержание регистра связи не текущей, а вызываемой функции. Это список параметров, которые эта функция посылает другим запросам функции, а не ее собственных параметров. Тем не менее, в отличие от PPE, этот механизм используется, только если число параметров больше, чем число регистров, которые можно использовать для параметров (не самая вероятная ситуация). Пространство локальных переменных используется как область оперативной памяти для функции, а область хранения регистров используется для хранения значений неизменяемых регистров, которые использует функция.

Так в этой функции мы используем обратный указатель списка, область хранения регистров связи и одну локальную переменную. Это дает размер фрейма 16 * 3 = 48 байтов. Как замечалось ранее, LR_OFFSET – это смещение от конца стека до области хранения регистров связи. LCL_NUM_VALUE – это смещение от конца стека до локальной переменной num.

Пролог устанавливает стековый фрейм для функции. В прологе первое, о чем следует подумать, это сохранение регистра связи. Так как вы еще не определили свой собственный стековый фрейм, смещение будет задаваться от конца стекового фрейма вызываемой функции. Запомните, что регистр связи сохраняется в стековом фрейме вызываемой функции, а не в собственном стековом фрейме функции. Следовательно, разумно сохранить его до резервирования стекового пространства. Это осуществляется при помощи так называемого накопителя D-Form (D-Form – это формат команд). Вы можете найти обзор форматов общих команд PPU в статье Ассемблер для Power-архитектуры, Часть 2 (форматы SPU достаточно близко придерживаются форматов PPU). Код для команды сохранения имеет вид stqd $lr, LR_OFFSET($sp). stqd означает "store quadword D-Form". Команды D-Form используют регистр в качестве первого операнда, который является регистром, куда предполагается загрузка или сохранение, и комбинацию константы и регистра в качестве второго операнда. Константа добавляется к регистру, чтобы вычислить адрес, использующийся для загрузки или хранения. Другими популярными форматами являются X-Form, где используются два регистра, которые суммируются, или A-Form, которая может сохранять константу или адрес относительного смещения константы. Так в этих командах $sp – это указатель стека (это псевдоним для $1). Выражение LR_OFFSET($sp) вычисляет значение LR_OFFSET плюс $sp и использует его как адрес назначения. Таким образом, эта команда сохраняет регистр связи (который содержит адрес возврата) в правильном месте в стековом фрейме вызываемой функции.

Теперь указатель текущего стекового фрейма сохраняется как обратный указатель для следующего стекового фрейма, хотя вы еще не установили стековый фрейм (это делается через отрицательное смещение). SPU не имеет элементарных команд сохранения/обновления как PPU, так что для уверенности в том, что обратные указатели всегда состоятельны, вы должны всегда сохранять обратный указатель до перемещения стекового указателя. И наконец, стековый указатель перемещается, чтобы резервировать все необходимое стековое пространство, используя команду ai $sp, $sp, -FRAME_SIZE. ai означает "add immediate", и он прибавляет в immediate-mode величину к регистру и сохраняет ее обратно в регистр. Он складывает вместе регистр во втором операнде и константу третьего операнда и сохраняет результат в регистре, описанном в первом операнде. Большинство команд придерживаются похожего формата, с регистром, который содержит результат, определенный в первом операнде.

Заметьте, что команда "add immediate" – это векторная операция. Помните, что регистры SPU имеют ширину 128 бит, но наша величина занимает только 32 бита. Регистр логично рассматривается как набор величин, которые обрабатываются одновременно. Команда "add immediate" действительно рассматривает регистр как четыре независимых 32-битных величины, каждая из которых добавляет к нему -FRAME_SIZE, и затем они все вновь сохраняются в заданном регистре. Предпочтительный размер слова для SPU составляет 32 бита (слово), но поддерживаются и другие размеры, включая byte, halfword и doubleword. Если размер операнда не оговорен в команде, это означает либо что размер не имеет значения (как, например, в логических командах), либо что используется 32-битный размер. Байты показываются включением в команду буквы b, halfword получает букву h, а doubleword получает d, хотя doubleword обычно используются только в командах с плавающей точкой (наиболее часто d используется в командах, отсылающих к D-Form адресации, а не к doubleword). Но в данном случае нас интересует только первое слово в регистре. Просто другие не представляют трудностей для ABI.

Далее вы копируете первый параметр в локальную переменную при помощи stqd $3, LCL_NUM_VALUE($sp). Вам необходимо сделать это, поскольку ваш параметр будет затерт вызовом рекурсивной функции, а впоследствии вам понадобится доступ к нему.

Далее сделайте сравнение в immediate-mode регистра 3 с числом 0 и сохраните результат в регистре 4 посредством ceqi $4, $3, 0. Заметьте, что у PPU (и, впрочем, у большинства процессоров) существует специальный регистр, содержащий результаты сравнений. Тем не менее в SPU результаты сохраняются в регистре общего назначения, в данном случае в регистре 4. Помните, что это векторный процессор. Так что вы не по-настоящему сравниваете регистр 3 с числом 0. Вместо этого вы сравниваете каждое слово в регистре 3 с числом 0. Так что в действительности вы имеете четыре ответа, даже если вас интересует только один из них. Результат сохраняется следующим образом: если условие для слова истинно, тогда все биты в заданном слове будут установлены; если же условие для слова ложно, тогда все биты в заданном слове сбрасываются в 0. Так что для таких команд будет четыре результата, каждый из которых состоит либо из одних единиц, либо из одних нулей, в зависимости от результата сравнения.

Следующая команда brnz $4, case_zero. brnz означает "branch relative if not zero". Помните, что регистр 4 – это результат предыдущего сравнения, так что это проверка результата предыдущего сравнения c нулем. В результирующем регистре будет установлено ненулевое значение (или true, когда все биты установлены в единицу), если предыдущий тест на ноль был истинным. Заметьте, что предыдущие две команды могли бы быть объединены в одну команду (brz $3, case_zero), так как вы только проверяли на ноль, но я разделил их на две команды, чтобы вы могли лучше видеть, как сравнение и ветвление работают в общем случае.

Что происходит, если результатом одних сравнений является истина, а других – ложь? Так как вы имеете дело с четырьмя 32-битными величинами, а не со 128-битной, то вы могли бы получить разные результаты для разных величин. Так если результаты разные, будет разветвление или нет? Оказывается, что несколько команд SPU имеют дело только с одним значением регистра. В таких случаях величина, которая используется, является одним из preferred (привилегированных) слотов регистра. Для 64-битной величины предназначена первая половина регистра; для 32-битной величины привилегированным слотом является первое слово регистра; для 16-битной величины привилегированным слотом являются вторые полслова (halfword) регистра; для 8-битной величины привилегированным слотом является четвертый байт регистра. В сущности, первое слово является привилегированным словом, и потом выравнивания происходят по самым младшим байтам или полусловам слова. Когда происходит условный переход, отправка функции величины, возвращение величины от функции и в некоторых других случаях предметом рассмотрения является именно величина в привилегированном слоте. В этом случае вы предполагаете, что величина, отданная функции, находится в preferred-слоте регистра. И если вы посмотрите на выравнивание числа number в секции .data, вы можете увидеть, что оно будет загружено в preferred-слот. Следовательно, ветвление будет осуществляться соответствующим образом, пока величина находится в preferred-слоте регистра.

Теперь предположим, что число, с которым вы работаете, и которое находится в регистре 3, ненулевое. Это означает, что исполнение должно пойти рекурсивно. Рекурсивный код на С будет представлять собой return num * factorial(num - 1). Внутренние вычисления требуют декремента num и передачи его в качестве параметра следующему вызову factorial. num уже находится в регистре 3, так что вам только нужно декрементировать его. Так что делаем сложение в immediate-mode: ai $3, $3, -1. Теперь вызываем следующий factorial. Для вызова функции в соответствии с SPU ABI все, что вам нужно сделать, это поместить параметр в регистр и затем вызвать brsl $lr, function_name. В этом случае первый и единственный параметр уже загружен в регистр 3. Теперь вы запускаете brsl $lr, factorial. Как я отмечал ранее, brsl означает "branch relative set link". Заданный адрес кодируется как относительный адрес, адрес возврата будет храниться в preferred-слоте оговоренного регистра, контроль перейдет к заданному адресу, который в данном случае возвращает исполнение к началу функции factorial.

Когда контроль исполнения вернется в эту точку, результат факториала должен оказаться в регистре 3. Теперь вы хотите умножить этот результат на текущее рассматриваемое значение. Следовательно, вы должны загрузить его обратно, так как он был затерт вызовом функции. lqd означает "load quadword D-Form". Первый операнд – это заданный регистр, а второй – это D-Form адрес для загрузки. Таким образом, команда lqd $5, LCL_NUM_VALUE($sp) будет считывать значение, которое вы ранее сохранили в регистре 5.

Теперь вам нужно помножить регистр 3 на регистр 5. Это делается посредством команды mpyu (multiply unsigned – беззнаковое умножение). Команда mpyu$3, $3, $5 перемножает регистр 3 и регистр 5 и сохраняет результат в первом из перечисленных регистров, то есть в регистре 3. В настоящее время команды для перемножения целых величин в SPU являются некоторым образом проблематичными, особенно умножения со знаком (с использованием команды mpy). Проблема в том, что результат умножения может быть вдвое длиннее его операнда. Результатом перемножения двух 32-битных величин является, в действительности, 64-битное число! В этом случае заданный регистр должен быть вдвое больше исходного. Чтобы решить эту проблему, команды умножения используют только младшие 16 бит от каждого 32-битового числа, так что результат будет соответствовать полному 32-битному регистру. Таким образом, хотя умножение рассматривает исходный регистр как 32-битный, из него используется только 16 битов. Таким образом, ваше число может быть урезано, если оно длиннее 32 битов. И если это умножение со знаком, то при урезании может даже измениться знак. Следовательно, чтобы успешно произвести умножение, ваша исходная величина должна иметь размер 16 битов, но храниться в 32-битном регистре (не имеет значения для умножения, если оно дополнено знаком на остальной части этих 32 битов). Это сильно ограничивает возможный размер вашей факториальной функции. Заметьте, что при умножении с плавающей точкой таких проблем не возникает.

Теперь у вас есть результат, он находится в регистре 3, там, где должна быть возвращаемая величина. Все, что осталось сделать, – это восстановить предыдущий стековый фрейм и возвратиться из функции. Вам просто нужно переместить указатель стека добавлением размера стекового фрейма при помощи ai $sp, $sp, FRAME_SIZE. Затем восстановите регистр связи при помощи lqd $lr, LR_OFFSET($sp). И, наконец, bi $lr ("branch indirect") передает управление по адресу, заданному в регистре связи (адрес возврата), осуществляя выход из функции.

В простейшем случае (это происходит, если в качестве параметра функции передается ноль) все гораздо проще. Результатом factorial(0) является 1, так что вы просто загружаете единицу в регистр 3 при помощи il $3, 1. Потом вы восстанавливаете стековый фрейм и выходите из функции. Однако, так как в этом случае не вызывается каких-либо других функций, вы не должны загружать регистр связи из стекового фрейма – значение все еще там.

Вот как работает функция! Заметьте только, что написание рекурсивных функций на SPE проблематично, поскольку в SPE нет защиты от переполнения стека, а локальная область хранения маловата.


Вывод

В этой статье рассматриваются основные концепции программирования на ассемблере процессора Cell BE на PLAYSTATION 3 под Linux. В следующий раз мы рассмотрим основные способы взаимодействия между SPE и PPE.

Ресурсы

Научиться

  • Programming high-performance applications on the Cell BE processor, Part 2: Program the synergistic processing elements of the Sony PLAYSTATION 3 – оригинал этой статьи на developerWorks.(EN)
  • Все команды SPU в деталях доступны в руководстве SPU Instruction Set Architecture Reference.(EN) Но обычно достаточно прочесть короткие резюме в руководстве SPU Assembly Language. Чтобы понять, на что способен SPU, я предлагаю прочесть руководство по языку ассемблера. Это руководство не длинное и весьма информативное. Если инструкция не дает понимания, обратитесь к полному документу ISA.(EN)
  • Подробнее об ABI смотрите в SPU ABI documentation, а также Linux extensions to the ABI.(EN)
  • Оптимальный источник информации о самом процессоре Cell BE – Cell BE Handbook.(EN)
  • Раздел Технической библиотеки полупроводниковых решений IBM (IBM Semiconductor Solutions Technical Library) Cell Broadband Engine documentation содержит множество описаний, руководств пользователя и др.(EN)
  • Вы найдете множество относящихся к Cell BE статей, форумов, материалов и других полезных вещей на IBM developerWorks Cell Broadband Engine resource center: оптимальный ресурс для всего, что касается Cell BE.(EN)

Получить продукты и технологии

Обсудить

Комментарии

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=253115
ArticleTitle=Программирование высокопроизводительных приложений на процессоре Cell BE, Часть 2: Программирование процессоров SPE (synergistic processing elements)
publish-date=09042007