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

Используйте расширения языка для улучшения своих приложений

В этой части серии статей Программирование высокопроизводительных приложений на процессоре Cell BE вы примените ваши знания о SPU для программирования процессора Cell Broadband Engine™ (Cell BE) на языке C/C++. Узнайте, как использовать векторные расширения, настроить компилятор на предсказание ветвлений и выполнять передачи DMA, используя язык C/C++.

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

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



11.09.2008

Предыдущее обсуждение SPU было сфокусировано на рассмотрении языка ассемблера SPU с целью дать вам подробное представление о процессоре. Теперь я перейду к рассмотрению языка C/C++, чтобы вы могли увидеть, как заставить компилятор выполнять большую часть работы за вас. Для того чтобы вы могли использовать SPU-расширения языка C/C++, в начало вашего кода должен быть включен заголовочный файл spu_intrinsics.h.

Основы векторов SPU

Главное отличие векторных процессоров от невекторных заключается в том, что векторные процессоры имеют большие регистры, которые позволяют им хранить несколько значений (называющихся элементами) одного и того же типа и обрабатывать их за раз с помощью одной операции. В векторных процессорах регистр рассматривается и как простое устройство, и как составное. Для представления этой концепции в языке C/C++ добавлено ключевое слово vector , которое берет простой тип данных и использует его в пределах всего регистра. Например, инструкция vector unsigned int myvec; создает вектор из четырех целочисленных элементов, которые загружаются, обрабатываются и сохраняются в совокупности, а переменная myvec относится ко всем четырем элементам одновременно. Ключевое слово signed/unsigned необходимо использовать для объявлений типов данных, отличных от floating point. Векторные константы создаются путем помещения типа вектора в круглые скобки, за которыми следует содержимое вектора в фигурных скобках. Например, вы можете присвоить значения элементам вектора с именем myvec следующим образом:

vector unsigned int myvec = (vector unsigned int){1, 2, 3, 4};

Помимо непосредственного присвоения существуют четыре основных примитива, которые используются для выполнения преобразований между скалярными и векторными данными: spu_insert, spu_extract, spu_promote и spu_splats. spu_insert используется для помещения скалярного значения в определенный элемент вектора spu_insert(5, myvec, 0) возвращает копию вектора myvec, в котором первый элемент (элемент 0) равен 5. spu_extract извлекает определенный элемент из вектора и возвращает его в качестве скалярной величины. spu_extract(myvec, 0) возвращает в качестве скалярной величины первый элемент вектора myvec. spu_promote преобразовывает значение в вектор, но определяет только один элемент. Тип вектора зависит от типа преобразовываемого значения. spu_promote((unsigned int)5, 1) создает вектор типа unsigned int со значением 5 во втором элементе (элемент 1), а остальные элементы остаются не определены. spu_splats работает подобно spu_promote за исключением того, что значение копируется во все элементы вектора. spu_splats((unsigned int)5) создает вектор типа unsigned int со значением 5 в каждом элементе.

Заманчиво было бы представлять себе векторы, как небольшие массивы, но фактически они ведут себя иначе в некоторых отношениях. Векторы, по существу, рассматриваются в качестве скалярных значений, тогда как массивы обрабатываются как ссылки. Например, spu_insert не изменяет содержимое вектора. Вместо этого возвращается совершенно новая копия вектора со вставленным элементом. Это выражение, результатом которого является значение, а не изменение самого значения. Например, также как myvar + 1 возвращает новое значение вместо изменения вектора myvar, команда spu_insert(1, myvec, 0) не изменяет вектор myvec, а вместо этого возвращает новое значение вектора, которое эквивалентно вектору myvec за исключением того, что первый элемент содержит значение 1.

Ниже приведена небольшая программа, использующая эти концепции (введите как vec_test.c):

Листинг 1. Программа, которая знакомит вас с SPU-расширениями языка C/C++
#include <spu_intrinsics.h>

void print_vector(char *var, vector unsigned int val) {
	printf("Vector %s is: {%d, %d, %d, %d}\n", var, spu_extract(val, 0),
	 spu_extract(val, 1), spu_extract(val, 2), spu_extract(val, 3));
}

int main() {
	/* Создание четырех векторов */
	vector unsigned int a = (vector unsigned int){1, 2, 3, 4};
	vector unsigned int b;
	vector unsigned int c;
	vector unsigned int d;

	/* вектор b идентичен вектору a, но последний элемент изменен на 9 */
	b = spu_insert(9, a, 3);

	/* вектор c содержит все четыре значения, установленные в 20 */
	c = spu_splats((unsigned int) 20);

	/* вектор d содержит второе значение, 
	установленное в 5, остальные – мусор */
	/* (в данном случае они все будут установлены в 5, 
	но на это не стоит полагаться) */
	d = spu_promote((unsigned int)5, 1);

	/* Вывод результатов */
	print_vector("a", a);
	print_vector("b", b);
	print_vector("c", c);
	print_vector("d", d);

	return 0;
}

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

spu-gcc vec_test.c -o vec_test
./vec_test

Встроенные функции для работы с векторами

Расширения языка C/C++ включают в себя типы данных и встроенные функции, которые обеспечивают программисту почти полный доступ к инструкциям языка ассемблера SPU. Однако, существует множество встроенных функций, которые весьма упрощают язык ассемблера SPU путем объединения многих похожих инструкций в одну функцию. Инструкции, которые различаются лишь типом операнда (такие как a, ai, ah, ahi, fa и, в дополнение, dfa) представлены одной функцией C/C++, которая выбирает подходящую инструкцию на основании типа операнда. Вдобавок к этому, когда в качестве параметров указаны два параметра типа vector unsigned int, инструкция spu_add генерирует инструкцию a (32-bit add). Тем не менее, если в качестве параметров указаны два параметра типа vector float, будет сгенерирована инструкция fa (float add). Обратите внимание на то, что встроенные функции в основном имеют те же ограничения, что и соответствующие им инструкции языка ассемблера. Однако в случаях, когда непосредственное значение оказывается слишком большим для соответствующей инструкции непосредственного (immediate-mode) режима, компилятор переведет непосредственное значение в вектор и выполнит соответствующую операцию вектор/вектор. Например, spu_add(myvec, 2) генерирует инструкцию ai (add immediate), тогда как spu_add(myvec, 2000) сначала загружает значение 2000 в собственный вектор, используя инструкцию il, а затем выполняет инструкцию a (add).

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

Ниже перечислены некоторые наиболее распространенные встроенные функции SPU (их типы не указаны, поскольку большинство из них являются полиморфными).

  • spu_add(val1, val2)
    Добавляет каждый элемент val1 к соответствующему элементу val2. Если val2 не является векторным значением, оно добавляется к каждому элементу val1.
  • spu_sub(val1, val2)
    Вычитает каждый элемент val2 из соответствующего элемента val1. Если val1 не является векторным значением, оно дублируется во всех элементах вектора, после чего из него вычитается val2.
  • spu_mul(val1, val2)
    Поскольку инструкции умножения действуют по-другому, встроенные функции SPU не объединяют их так, как это происходит с другими операциями. spu_mul выполняет умножение типа floating point (одинарной и двойной точности). Результатом является вектор, в котором каждый элемент является результатом умножения соответствующих элементов val1 и val2.
  • spu_and(val1, val2), spu_or(val1, val2), spu_not(val), spu_xor(val1, val2), spu_nor(val1, val2), spu_nand(val1, val2), spu_eqv(val1, val2)
    Логические операции выполняются поразрядно, поэтому типы получаемых ими операндов имеют значение лишь при вычислении типа возвращаемого ими значения. spu_eqv является поразрядной, а не поэлементной операцией эквивалентности.
  • spu_rl(val, count), spu_sl(val, count)
    spu_rl выполняет циклический сдвиг каждого элемента val влево на число битов, указанное в соответствующем элементе count. Биты, вытесненные за пределы, переносятся вправо. Если count является скалярным значением, оно используется в качестве счетчика для всех элементов val. spu_sl действует точно также, но вместо циклического сдвига выполняется последовательный сдвиг.
  • spu_rlmask(val, count), spu_rlmaska, spu_rlmaskqw(val, count), spu_rlmaskqwbyte(val, count)
    Названия этих операций очень запутаны. Операции называются "циклический сдвиг влево и маскирование" (rotate left and mask), но на самом деле они выполняют сдвиг вправо (эти операции реализованы путем комбинации сдвигов влево и маскирования, однако интерфейс программирования предназначен для сдвигов вправо). spu_rlmask и spu_rlmaska сдвигают каждый элемент val вправо на число битов, указанное в соответствующем элементе count (или на значение count, если count является скаляром). spu_rlmaska дублирует знаковый разряд по мере сдвига битов. spu_rlmaskqw оперирует целым четверным словом за раз, но только до 7 битов (вычисляется модуль count для помещения его в соответствующий диапазон). spu_rlmaskqwbyte работает точно также за исключением того, что count является числом байтов, а не битов, а также является модулем 16, а не 8.
  • spu_cmpgt(val1, val2), spu_cmpeq(val1, val2)
    Эти инструкции выполняют поэлементное сравнение двух операндов. Результаты сохраняются в качестве всех единиц (в случае совпадения) и всех нулей (в случае несовпадения) в результирующем векторе в соответствующем элементе. spu_cmpgt выполняет сравнение "больше чем", а spu_cmpeq – сравнение эквивалентности.
  • spu_sel(val1, val2, conditional)
    Эта инструкция соответствует инструкции selb языка ассемблера. Сама инструкция является битовой, поэтому все типы используют одну и ту же базовую инструкцию. Тем не менее, вспомогательная функция возвращает значение того же типа, что и операнды. Так же, как и в языке ассемблера, spu_sel проверяет каждый бит, содержащийся в операнде conditional. Если этот бит нулевой, результирующий соответствующий бит выбирается из соответствующего бита val1; в противном случае он выбирается из соответствующего бита val2.
  • spu_shuffle(val1, val2, pattern)
    Эта интересная инструкция позволяет вам переставлять байты в val1 и val2 в соответствии с шаблоном, указанном в pattern. Инструкция проверяет каждый байт в pattern, и если байт начинается с битов 0b10, соответствующий результирующий байт устанавливается равным 0x00; если байт начинается с битов 0b110, соответствующий результирующий байт устанавливается равным 0xff; если байт начинается с битов 0b111, соответствующий результирующий байт устанавливается равным 0x80; наконец (и это важнее всего), если не выполняется ни одно из предыдущих условий, последние пять битов байта шаблона используются для выбора того, какой байт из val1 или val2 должен использоваться в качестве значения для текущего байта. Два значения объединены, и пятибитовое значение используется в качестве индекса байта объединенного значения. Это используется для вставки элементов в векторы, а также для выполнения быстрого табличного поиска.

Все инструкции, начинающиеся с префикса spu_, будут пытаться подобрать наилучшее совпадение, основываясь на типах операндов. Тем не менее, не все векторные типы поддерживаются всеми инструкциями – данная функциональность основана на возможностях обработки этих типов инструкциями языка ассемблера. В дополнение к этому, если вы хотите использовать инструкцию, отличную от той, что выбирает компилятор, вы можете выполнять почти все инструкции, не связанные с ветвлениями, с помощью специальных встроенных функций. Все специальные встроенные функции имеют форму si_assemblyinstructionname, где assemblyinstructionname – это имя инструкции языка ассемблера, определенное в спецификации SPU Assembly Language Specification. Таким образом, команда si_a(a, b) дополнительно приводит к выполнению инструкции a. Все операнды специальных встроенных функций приводятся к специальному типу, который называется qword и является, по существу, непрозрачным типом регистрового значения. Возвращаемые специальными встроенными функциями значения также имеют тип qword, который впоследствии можно привести к любому выбранному вами векторному типу.


Использование встроенных функций

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

  1. Преобразовать все значения с использованием преобразования в верхний регистр.
  2. Выполнить сравнения вектора со всеми байтами и выяснить, попадают ли они в диапазон между 'a' и 'z'.
  3. Путем сравнения выбрать между преобразованными и не преобразованными значениями, используя инструкцию выбора.

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

Итак, ниже приведен код функции convert_buffer_to_upper на языке C/C++ (введите как convert_buffer_c.c, сохранив в той же папке, в которой находятся файлы из предыдущих статей – эти файлы понадобятся вам для компиляции полного приложения).

Листинг 2. Преобразование в верхний регистр на языке C/C++
#include <spu_intrinsics.h>

unsigned char conversion_value = 'a' - 'A';

inline vec_uchar16 convert_vec_to_upper(vec_uchar16 values) {
	/* Обработка всех символов */
	vec_uchar16 processed_values = spu_absd(values, spu_splats(conversion_value));
	/* Выясним, какие из символов нуждаются в обработке 
	(те, что находятся между 'a' and 'z') */
	vec_uchar16 should_be_processed = spu_xor(spu_cmpgt(values, 'a'-1), 
	spu_cmpgt(values, 'z'));
	/* Использование should_be_processed для выбора 
	между исходными и обработанными значениями */
	return spu_sel(values, processed_values, should_be_processed);
}

void convert_buffer_to_upper(vec_uchar16 *buffer, int buffer_size) {
	/* Найдем конец буфера (сначала необходимо привести к 
	определенному виду, поскольку размер – это байты) */
	vec_uchar16 *buffer_end = (vec_uchar16 *)((char *)buffer + buffer_size);

	while(__builtin_expect(buffer < buffer_end, 1)) {
		*buffer = convert_vec_to_upper(*buffer);
		buffer++;
		*buffer = convert_vec_to_upper(*buffer);
		buffer++;
		*buffer = convert_vec_to_upper(*buffer);
		buffer++;
		*buffer = convert_vec_to_upper(*buffer);
		buffer++;
	}
}

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

spu-gcc convert_buffer_c.c 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

Как вы, вероятно, заметили, эта программа использует слегка отличающуюся форму записи для имен векторных типов, нежели раньше. В документации по встроенным функциям SPU (обратитесь к разделу Ресурсы) определены упрощенные имена векторных типов, начинающиеся с префикса vec_. Для целочисленных типов следующим символом является u (для подписанных) или s (для неподписанных). Далее следует имя используемого базового типа (char, int, float и так далее). Наконец, в конце указывается количество элементов этого типа, содержащихся в векторе. Например, vec_uchar16 является вектором из 16 элементов типа unsigned char, а vec_float4 – вектором из 4 элементов типа float. Эта форма записи существенно упрощает объем вводимой информации.

При вычислении значения buffer_end программа выполняет некоторые преобразующие действия. Поскольку переменная size измерялась в байтах, мне пришлось привести указатель к типу char * таким образом, чтобы, когда я изменяю размер, он бы изменялся в байтах, а не в четверных словах. Поскольку длина значения, на которое указывают указатели вектора, составляет 16 байтов, их инкремент составляет 16 байтов, тогда как инкремент указателей типа char составляет один байт. Вот почему работает инструкция buffer++ – она увеличивается на длину одного вектора, равную 16 байтам.

Другой интересной вещью в коде на C/C++ является конструкция __builtin_expect, которая помогает компилятору выполнять прогнозирование ветвлений. В C/C++ вы не можете напрямую выполнять прогнозирование ветвлений, потому что вы не имеете ни адреса ветвления, ни конечного адреса. Поэтому вы оставляете данную задачу компилятору, который затем может сгенерировать соответствующее прогнозирование ветвлений. __builtin_expect(buffer < buffer_end, 1) генерирует код ветвления на основе первого аргумента, buffer < buffer_end, но выполняет прогнозирование ветвления на основе второго аргумента, 1. Это указывает компилятору сгенерировать прогнозирования, которые ожидают, что значение buffer < buffer_end будет равно 1.

На этом этапе для программирования SPU доступны два компилятора, и, как можно предположить, каждый из них имеет преимущества в определенной области. GCC, например, выполняет фантастическую работу по чередованию инструкций между вызовами convert_vec_to_upper, таким образом, минимизируя задержки. Тем не менее, в этой программе конструкция __builtin_expect оказывается для нас практически бесполезной. Компилятор IBM XLC, с другой стороны, действует наоборот. Он вообще не организует чередование инструкций между вызовами convert_vec_to_upper, но структурирует цикл таким образом, что мы получаем максимальный эффект от прогнозирования ветвлений. Неудивительно, что ни один из этих компиляторов не обеспечивает такой же быстрой работы, как код из предыдущей статьи, вручную созданный на языке ассемблера, однако для этой программы компилятор XLC работает лучше, чем GCC. Компилирование кода без каких-либо флагов оптимизации приводит к тому, что приложение работает примерно в пять раз медленнее, поэтому не забывайте всегда использовать при компиляции опцию -O2 или -O3.


Составные встроенные функции и программирование контроллера MFC

Составные встроенные функции – это те функции, которые компилируются в несколько инструкций. Составные встроенные функции инкапсулируют широко используемые шаблоны элементов SPE для упрощения их программирования. Две самые важные составные встроенные функции – это spu_mfcdma64 и spu_mfcstat. spu_mfcdma64 почти в точности повторяет функцию dma_transfer, которую я написал и использовал в предыдущей статье, за исключением того, что верхние и нижние части адреса основной памяти разделены на два 32-разрядных параметра (dma_transfer использует один 64-разрядный параметр для адреса основной памяти).

spu_mfcdma64 принимает шесть параметров:

  1. адрес основной памяти для передачи
  2. старшие 32 бита адреса основной памяти
  3. младшие 32 бита адреса основной памяти
  4. размер передачи
  5. "тэг", назначаемый передаче
  6. передаваемая команда DMA

Часто адрес основной памяти будет представлен в виде 64-разрядного значения. Для того чтобы разделить его на части, используйте инструкцию mfc_ea2h для получения старших битов и инструкцию mfc_ea2l – для получения младших битов. Тэг – это число от 0 до 31, назначенное программистом для идентификации передачи или группы передач в запросах статуса и операциях упорядочивания. Команда DMA может принимать ряд значений (обратитесь к разделу Ресурсы для получения информации о том, где искать не перечисленные здесь значения). Передача DMA называется PUT, если данные передаются из локальной памяти SPU в основную память, и GET – если передача происходит в обратном направлении. Имена этих команд DMA начинаются либо с префикса MFC_PUT, либо с префикса MFC_GET, соответственно. Далее, команды MFC работают либо по отдельности, либо в списке. Если команда DMA работает в списке, к ее имени добавляется символ L (обратитесь к разделу Ресурсы для получения дополнительной информации о командах DMA, работающих в списках). Команды DMA также могут иметь определенные уровни синхронизации, применяемые к ним. Для барьерной синхронизации добавьте символ B, для fence-синхронизации добавьте символ F, а в случае отсутствия синхронизации вам не нужно добавлять ничего. Наконец, все имена команд DMA имеют суффикс _CMD. Таким образом, команда для выполнения одной передачи из локальной памяти в основную с использованием fence-синхронизации будет называться MFC_PUTF_CMD.

По умолчанию команды DMA на контроллере MFC совершенно неупорядочены – контроллер MFC может обрабатывать их в любой последовательности, на свое усмотрение. Тем не менее, для наложения упорядочивающих ограничений на передачи MFC DMA можно использовать тэги, ограничители и барьеры. Ограничитель (fence) устанавливает ограничение, приводящее к тому, что передача DMA выполняется только после завершения выполнения всех предыдущих команд с тем же самым тэгом. Барьер (barrier) устанавливает ограничение, приводящее к тому, что указанная передача DMA выполняется только после завершения выполнения всех предыдущих команд с тем же тэгом (как и в случае с ограничителем), но до начала выполнения всех последующих команд с тем же тэгом.

Ниже приведены несколько примеров spu_mfcdma64:

Листинг 3. Использование spu_mfcdma64
typedef unsigned long long uint64;
typedef unsigned long uint32;
uint64 ea1, ea2, ea3, ea4, ea5; /* предполагаем, что 
все переменные содержат разумные значения */
void *ls1, *ls2, *ls3, *ls4; /* предполагаем, что все
 переменные содержат разумные значения */
uint32 sz1, sz2, sz3, sz4; /* предполагаем, что все 
переменные содержат разумные значения */
int tag = 3; /* Случайное значение, но оно должно быть
 одинаковым для всех синхронизированных передач */

/* Передача 1: Системная память -> Локальная память, без упорядочивания */
spu_mfcdma64(ls1, mfc_ea2h(ea1), mfc_ea2l(ea1), sz1, tag, MFC_GET_CMD);

/* Передача 2: Локальная память -> Системная память, должна быть 
выполнена после предыдущих передач */
spu_mfcdma64(ls2, mfc_ea2h(ea2), mfc_ea2l(ea2), sz2, tag, MFC_PUTF_CMD);

/* Передача 3: Локальная память -> Системная память, без упорядочивания */
spu_mfcdma64(ls3, mfc_ea2h(ea3), mfc_ea2l(ea3), sz3, tag, MFC_PUT_CMD);

/* Передача 4: Локальная память -> Системная память, должна быть синхронизована */
spu_mfcdma64(ls4, mfc_ea2h(ea4), mfc_ea2l(ea4), sz4, tag, MFC_PUTB_CMD);

/* Передача 5: Системная память -> Локальная память, без упорядочивания */
spu_mfcdma64(ls4, mfc_ea2h(ea5), mfc_ea2l(ea5), sz4, tag, MFC_GET_CMD);

В вышеприведенном примере могли использоваться несколько возможных вариантов упорядочивания - они перечислены ниже:

  • 1, 2, 3, 4, 5
  • 3, 1, 2, 4, 5
  • 1, 3, 2, 4, 5

Поскольку передача 2 использует только ограничитель, а в передаче 3 порядок выполнения вообще не указан, то передача 3 может выполняться в любом месте до барьера (передача 4). Единственным требованием для первых трех передач является то, что передача 2 должна быть выполнена после передачи 1. Передача 4, тем не менее, требует полной синхронизации передач, расположенных до и после нее.

Присмотритесь внимательнее к передачам 4 и 5. Это полезный прием, который можно взять на заметку – сохранение и перезагрузка. Если вы по частям передаете данные из системной памяти в локальную память и сохраняете их назад в системную память, вы можете запланировать одновременно сохранение и загрузку, используя ограничитель или барьер для их упорядочивания. Этот прием помещает всю логическую часть передачи в контроллер MFC, разгружая вашу программу и позволяя ей выполнять другие вычисления, пока буфер ожидает новые данные. Мы воспользуемся этим в следующей статье, когда будем говорить о двойной буферизации.

spu_mfcdma64 является достаточно удобным инструментом, но немного утомительным, особенно когда вам приходится продолжать использовать команды mfc_ea2h и mfc_ea2l для преобразования адресов. Поэтому спецификацией дополнительно предусмотрен ряд сервисных утилит, сокращающих необходимый объем ручного набора кода. Функции класса mfc_ принимают все те же параметры, что и функция spu_mfcdma64 за исключением того, что адрес основной памяти является простым 64-разрядным параметром, а команда DMA зашифрована в имени функции. Кроме того, эти функции принимают два дополнительных параметра: идентификатор класса передачи и идентификатор класса замены. Оба из них могут быть безо всяких последствий приравнены к нулю в приложениях, не работающих в режиме реального времени (обратитесь к разделу Ресурсы для получения ссылок на дополнительную информацию по этим двум параметрам). Таким образом, передачу 2 из вышеприведенного листинга можно переписать следующим образом:

mfc_putf(ls2, ea2, sz2, tag, 0, 0);

Тэги оказываются полезными не только для синхронизации передач данных, но также и для проверки их статуса. В элементе SPE существуют следующие каналы: канал маски тэгов, указывающий, какой тэг используется в настоящий момент для проверок статуса; канал для формирования запросов статуса и еще один канал для обратного считывания статуса канала. Хотя это достаточно простые операции, спецификация также содержит специальные методы для их выполнения. mfc_write_tag_mask принимает 32-разрядное целое число и использует его в качестве маски канала для последующих обновлений статуса. Установите в этой маске разряд каждого тэга, статус которого вы хотите проверить, равным 1. Так, для проверки статуса каналов 2 и 4 следует использовать инструкцию mfc_write_tag_mask(20), а для лучшего восприятия, вы можете использовать команду mfc_write_tag_mask(1<<2 | 1<<4);. Для фактического обновления статуса вам необходимо выбрать команду для обработки статуса и послать ее с помощью инструкции spu_mfcstat(unsigned int command). Команды для обработки статуса приведены в следующем списке:

  • MFC_TAG_UPDATE_IMMEDIATE
    Эта команда заставляет элемент SPE немедленно возвратить статус каналов DMA. Каждый канал, указанный в маске каналов, будет установлен в 1, если в очереди не осталось команд с этим тэгом (другими словами, если все операции, которые могли быть активны ранее, завершены), и установлен в 0, если в очереди присутствуют команды.
  • MFC_TAG_UPDATE_ANY
    Эта команда заставляет элемент SPE ожидать, пока не завершатся все команды хотя бы с одним из указанных в маске тэгом, а затем возвращает статус каналов DMA, указанных в маске тэгов.
  • MFC_TAG_UPDATE_ALL
    Эта команда заставляет элемент SPE ожидать, пока не завершатся все команды со всеми тэгами, указанными в маске тэгов, и только затем возвращает статус. Возвращаемым значением будет являться 0.

Для использования этих констант вам необходимо включить в код заголовочный файл spu_mfcio.h.

Использование spu_mfcstat позволяет вам выполнять проверку статуса и ожидание запросов DMA. Использование MFC_TAG_UPDATE_ANY позволяет вам выполнять несколько запросов DMA, предоставляет контроллеру MFC возможность обрабатывать их в наилучшем с его точки зрения порядке, на основании которого происходит соответствующее взаимодействие с вашим кодом.


Пример программы для контроллера MFC

Теперь я применю знания об этих составных встроенных функциях MFC в программе преобразования в верхний регистр. Ранее в этой статье я переписал на языке C главную функцию преобразования, а теперь я перепишу на C главный цикл. Ниже приведен новый код (введите как convert_driver_c.c):

Листинг 4. Код передачи MFC для преобразования в верхний регистр
#include <spu_intrinsics.h>
#include <spu_mfcio.h>
typedef unsigned long long uint64;

#define CONVERSION_BUFFER_SIZE 16384
#define DMA_TAG 0

void convert_buffer_to_upper(char *conversion_buffer, int current_transfer_size);

char conversion_buffer[CONVERSION_BUFFER_SIZE];

typedef struct {
	int length __attribute__((aligned(16)));
	uint64 data __attribute__((aligned(16)));
} conversion_structure;

int main(uint64 spe_id, uint64 conversion_info_ea) {
	conversion_structure conversion_info; /* 
	Information about the data from the PPE */

	/* В этой программе мы используем только один тэг */
	mfc_write_tag_mask(1<<DMA_TAG);

	/* Соберем информацию о преобразовании */
	mfc_get(&conversion_info, conversion_info_ea, 
	sizeof(conversion_info), DMA_TAG, 0, 0);
	spu_mfcstat(MFC_TAG_UPDATE_ALL); /* Wait for Completion */

	/* Получим реальные данные */
	mfc_get(conversion_buffer, conversion_info.data, conversion_info.length, 
	DMA_TAG, 0, 0);
	spu_mfcstat(MFC_TAG_UPDATE_ALL);

	/* Выполним преобразование */
	convert_buffer_to_upper(conversion_buffer, conversion_info.length);

	/* Поместим данные обратно в системную память */
	mfc_put(conversion_buffer, conversion_info.data, conversion_info.length, 
	DMA_TAG, 0, 0);
	spu_mfcstat(MFC_TAG_UPDATE_ALL); /* Wait for Completion */
}

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

spu-gcc convert_buffer_c.c convert_driver_c.c -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

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

Листинг 5. Цикл в коде передачи MFC
#include <spu_intrinsics.h>
#include <spu_mfcio.h> /* объявление константы для MFC */
typedef unsigned long long uint64;
typedef unsigned int uint32;

/* CONVERSION_BUFFER_SIZE переименована в MAX_TRANSFER_SIZE, поскольку теперь она 
главным образом используется для ограничения размера передач DMA */
#define MAX_TRANSFER_SIZE 16384

void convert_buffer_to_upper(char *conversion_buffer, int current_transfer_size);

char conversion_buffer[MAX_TRANSFER_SIZE];

typedef struct {
	uint32 length __attribute__((aligned(16)));
	uint64 data __attribute__((aligned(16)));
} conversion_structure;

int main(uint64 spe_id, uint64 conversion_info_ea) {
	conversion_structure conversion_info; /* Информация о данных из PPE */
	/* Новые переменные для отслеживания нашего местоположения в данных */
	uint32 remaining_data; /* Сколько данных осталось в целой строке */
	uint64 current_ea_pointer; /* Наше местоположение в системной памяти */
	uint32 current_transfer_size; /* Насколько велик размер текущей передачи
	 (может быть 
	меньше, чем MAX_TRANSFER_SIZE) */


	/* В этой программе мы используем только один тэг */
	mfc_write_tag_mask(1<<0);

	/* Соберем информацию о преобразовании */
	mfc_get(&conversion_info, conversion_info_ea, sizeof(conversion_info), 0, 0, 0);
	spu_mfcstat(MFC_TAG_UPDATE_ALL); /* Wait for Completion */

	/* Настройка цикла */
	remaining_data = conversion_info.length;
	current_ea_pointer = conversion_info.data;

	while(remaining_data > 0) {
		/* Определим, сколько данных осталось передать */
		if(remaining_data < MAX_TRANSFER_SIZE)
			current_transfer_size = remaining_data;
		else
			current_transfer_size = MAX_TRANSFER_SIZE;

		/* Получим реальные данные */
		mfc_getb(conversion_buffer, current_ea_pointer,
		 current_transfer_size, 0, 0, 0);
		spu_mfcstat(MFC_TAG_UPDATE_ALL);

		/* Выполним преобразование */
		convert_buffer_to_upper(conversion_buffer, 
		current_transfer_size);

		/* Поместим данные обратно в системную память */
		mfc_putb(conversion_buffer, current_ea_pointer, 
		current_transfer_size, 0, 0, 0);

		/* Перейдем к следующей части данных */
		remaining_data -= current_transfer_size;
		current_ea_pointer += current_transfer_size;
	}
	spu_mfcstat(MFC_TAG_UPDATE_ALL); /* Wait for Completion */
}

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

spu-gcc convert_buffer_c.c convert_driver_c.c -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

Итак, вы только что увеличили размер данных, которые вы можете обрабатывать, до 4 гигабайтов, хотя вы с легкостью могли бы еще более увеличить его, используя для работы с данными 64-разрядные переменные вместо 32-разрядных. Заметьте, что ваш код не содержит явных указаний контроллеру MFC ожидать завершения операций PUT прежде, чем выполняется повторный запуск операций GET. Это возможно благодаря тому, что при работе с передачами вы используете барьеры и одинаковый тэг DMA. В результате передачи переводятся в последовательный режим самим контроллером MFC, и, таким образом, он всегда ожидает, пока для текущего преобразования завершится операция PUT и данные будут помещены в системную память, прежде чем в буфер будет загружена (GET) следующая часть данных. Помните только о том, что нужно дождаться завершения последней операции (обратите внимание на команду spu_mfcstat за пределами цикла), иначе передача последнего бита ваших данных может не завершиться прежде, чем он будет использован в программе!

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


Полезные советы по программированию SPE на языке C

Воспользуйтесь следующими советами при разработке приложений SPE на языке C:

  • Векторы могут быть преобразованы в векторы других типов. Также можно выполнять прямые и обратные преобразования между типами векторов и специальным типом quad. Однако никакое из этих преобразований не приводит к какому-либо преобразованию данных. Если вам необходимо выполнить преобразование типов, используйте соответствующие встроенные функции SPU.
  • Векторные и не векторные указатели могут быть преобразованы из одного типа в другой, но при преобразовании скалярного указателя в векторный программист должен самостоятельно убедиться в том, что указатель выровнен по четверному слову.
  • При выделении памяти объявленные векторы всегда выровнены по четверному слову.
  • Помните, что передачи DMA размером в 16 или более байтов должны быть кратными 16 байтам и выровненными в пределах 16-байтовых границ как для SPE, так и для PPE. Передачи меньшего размера должны являться степенями двойки и быть естественно выровненными. Оптимальными передачами являются кратные 128 байтам передачи, которые выровнены в пределах 128-байтовый границ.
  • Если вы не уверены насчет выравнивания данных в PPE, используйте инструкцию memalign или posix_memalign для выделения памяти выровненному указателю из кучи, и используйте инструкцию memcpy или подобную ей для перемещения данных в выровненную область.
  • Всегда компилируйте программы с ключом -Wall и в особенности уделяйте внимание сообщениям о пропущенных прототипах. Неправильно определенные прототипы (в особенности это касается 32- и 64-разрядных типов) могут привести к непредсказуемым сбойным ситуациям.
  • Всегда сохраняйте адреса основной памяти в виде unsigned long long как для PPE, так и для SPE. При таком подходе адреса могут интерпретироваться в качестве унифицированной формы для PPE и SPE независимо от того, был ли код PPE скомпилирован для выполнения в 32- или 64-разрядной среде.
  • Избегайте операций умножения целочисленных типов (особенно 32-разрядных операций умножения). Для выполнения таких операций требуется пять инструкций. Если вам необходимо выполнить умножение, предварительно преобразуйте данные к типу unsigned short.
  • Объявление скалярных значений в качестве векторов и векторных указателей в скалярном коде для SPE (даже если вы не используете их в качестве векторов) может ускорить выполнение кода, поскольку в этом случае не требуется выполнять невыровненные загрузки и сохранения.
  • При работе с SPE остерегайтесь следующего: типы данных float и double реализованы по-разному и округляются также по-разному. В частности, значения типа float отклоняются от стандарта C99. В следующей статье мы рассмотрим это более подробно.

Заключение

Встроенные функции языка C позволяют программистам наилучшим образом использовать знания языков 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=337340
ArticleTitle=Программирование высокопроизводительных приложений на процессоре Cell BE: Часть 5. Программирование процессора SPU на C/C++
publish-date=09112008