在 Cell BE 处理器上编写高性能的应用程序,第 3 部分: 接触 SPU

在 Sony PLAYSTATION 3 的 SPE 上编程

继续对 Cell Broadband Engine™ (Cell BE) 处理器的 SPE 及其底层工作原理进行深入探讨。本期文章将展示其中的存储对齐问题及 SPE 间的通信。

Jonathan Bartlett (johnnyb@eskimo.com), 技术总监, New Medio

Jonathan Bartlett 是 Programming from the Ground Up 一书的作者,这本书对使用 Linux 汇编语言的编程进行了简介。他是 New Medio 的开发技术总监,为客户开发 Web 应用程序、视频应用程序、kiosk 应用程序和桌面应用程序。



2007 年 4 月 12 日

未对齐的加载和存储

因为 SPU 侧重的是向量处理而非标量处理,所以它只能一次从以 16 字节为边界对齐的本地存储位置加载和存储 16 字节(寄存器的大小)。这样一来,就不能从内存位置 12 加载一个字。要获得此字,应该先要从内存位置 0 加载一个四字,然后再移位以使所想要的值存在于首选槽内。初始的四字必须要被加载,适当的值插入到四字的适当位置,然后再将结果存储回去。由于上述原因,建议最好存储那些按 16 字节对齐的数据。要加载跨越 16 字节边界的值会十分困难,因为必须要将其存储到两个寄存器,进行移位然后再对它们进行掩码和组合。存储这样的值则更为困难,所以如果可能,最好永远不要使用跨 16 字节边界的值。

虽然使用不是按 16 字节边界对齐的数据是允许的,但我这里要讨论的加载和存储技术要求数据必须是自然对齐的,以防止跨 16 字节边界现象的产生。这意味着字将是 4 字节对齐的,半字将是 2 字节对齐的,而字节则不需要对齐。

根据数据大小的不同,进行未对齐加载需要两个或三个指令。其原因是如果您加载的是单一值,那么您就会希望它位于寄存器的首选槽。第一个指令负责加载,第二个指令旋转此值以便所请求的地址位于寄存器的开始。然后,如果数据比字小,就需要进行移位以将其从寄存器的开始位置移到首选槽(如果它是单字或双字,寄存器的开始就 首选槽)。以下就是实行字节加载的代码,它将地址放入到寄存器 3 的首选槽内并使用它来将字节加载进寄存器 4 的首选槽:

清单 3. 从未对齐的内存加载
###Load byte unaligned address $3 into preferred slot of register $4###

#Loads from nearest quadword boundary
lqd $4, 0($3)
#Rotate value to the beginning of the register
rotqby $4, $4, $3
#Rotate value to the preferred slot (-3 for bytes, -2 for halfwords, and nothing for
words or doublewords)
rotqbyi $4, $4, -3

记住,lqd 指令只能从 16 字节边界加载,所以它会在加载期间忽略最低有效的四位,而只会从内存加载已对齐的四字。因而,对于任意的地址,我们无从知道在加载的四字中我们想要的那个值位于何处。rotqby 指令(代表的是 “rotate (left) quadword by bytes” )使用加载自的地址以指示寄存器的旋转程度。它只使用了寄存器内地址的最低有效的四位(即加载时忽略的那四位)来决定旋转的程度。这将总会是它为了将指定地址移到寄存器的开始所需向左移位的字节数。最后,对于字节数,首选槽并 在寄存器的开始,却在右侧的 3 个字节。所以指令 rotqbyi 使用移位的即时模式进行移位操作。单字或双字大小的转移并不需要这最后的一条指令,原因是它们的首选槽总是在寄存器的开始。最后,寄存器 4 将拥有最终值,并且字节会移位进首选槽。

存储则更为复杂。下面的代码显示了如何将寄存器 $4 的首选槽内的字节存储进由寄存器 $3 指定的地址:

清单 4. 存储未对齐的地址
###Store preferred byte slot $4 into unaligned address $3

#Load the data into a temporary register
lqd $5, 0($3)
#Generate the controls for a byte insertion
cbd $6, 0($3)
#Shuffle the data in
shufb $7, $4, $5, $6
#Store it back
stqd $7, 0($3)

要理解这段有些晦涩的代码,需要记住 SPU 一次只能在按四字对齐的地址加载和存储一个四字。因此,如果希望只存储一个字节,且试图在未对齐的地址直接存储,那么它就会进入到不正确的位置并会截断四字内剩余的字节。要避免这些,需要首先从内存加载四字,将值插入到四字内合适的字节,然后再存储回去。这其中最为困难的是只基于地址将它插入到合适的位置。还好,有两个指令可以为我们提供一些帮助:cbd (代表的是 “generate control for byte insertion”) 和 shufb (意思是 “shuffle bytes”)。cbd 指令接受一个地址并会生成一个控制字,而该控制字可被 shufb 用来为该地址在四字里的合适地址插入一个字节。cbd $6, 0($3) 使用寄存器 3 内的地址来生成控制四字,然后将其存储进寄存器 6。指令 shufb $7, $4, $5, $6 使用寄存器 6 内的控制四字来生成一个新值并放入到寄存器 7 内,而寄存器 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 - temporary storage for final store
	# $13 - conversion factor

	#address of letter stored in unaligned address in $3
	#UNALIGNED LOAD
	lqd $4, 0($3)
	rotqby $4, $4, $3
	rotqbyi $4, $4, -3

	#IS IN RANGE 'a'-'z'?
	cgtbi $5, $4, 'a' - 1
	cgtbi $6, $4, 'z'
	nand $7, $6, $6
	and $8, $5, $7
	#Mask out irrelevant bits
	andi $8, $8, 255
	#Skip uppercase conversion and store if $4 is not lowercase (based on $8)
	brz $8, end_convert

is_lowercase:
	#Perform Conversion
	il $13, 'a' - 'A'
	absdb $4, $4, $13

	#Unaligned Store
	lqd $9, 0($3)
	cbd $10, 0($3)
	shufb $11, $4, $9, $10
	stqd $11, 0($3)

end_convert:
	#no stack frame, no return value, just return
	bi $lr

要编译和运行,执行以下命令:

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

main 函数与之前并无多大区别,不再详述。但注意这里是将字母的地址 传递到 convert_to_upper,而非字母本身。

convert_to_upper 函数接受任意字符的地址,转换为大写并将其存储回去,不返回任何东西。它不会调用另一个函数,所以不需要堆栈框架。

该函数首先进行到寄存器 4 的未对齐的加载(前面已经讨论过)。然后检查字节是否在 az 的范围内。方法是比较它是否大于 'a' - 1,然后再看它是否大于 'z.'。我并没有做“小于”比较,因为它们在 SPU 尚不可用!。SPU 只有“大于”比较和“等于”比较 ;因而如果需要进行“小于或等于”比较,必须要进行“大于”比较,然后在此基础上再进行取“非”,使用 nand 指令实现,且指令的两个源参数是相同的寄存器。可以通过 and 指令进行组合比较(注意可以用一个 xor 将所有逻辑指令结合成一个,但那样做的代码应该比这个要清晰很多)。最后,由于分支指令只作用于半字或单字值,所以必须将寄存器非相关的部分进行掩码处理(在这个阶乘的例子中,我不需要这么做,因为我所处理的是整个一个字)。

如果寄存器 8 的首选槽中的位均设为假,则跳到函数的尾部。如果均为真,则执行转换。SPU 上惟一一个面向字节的计算函数是 absdb(意思是 “absolute difference of bytes”),计算两个操作数间的差的绝对值。用该值结合大写和小写值间的差执行转换。最后,执行未对齐的存储。由于未调用任何函数,也未使用任何本地存储,所以您根本无需堆栈框架,并可通过链接寄存器退出。


与 PPE 通信

到目前为止,我一直侧重于讲述那些只有 SPE 的程序。现在,我要来看一下受 PPE 控制的程序,为此,我需要知道如何让 PPE 与 SPE 相互通信。

信道和 MFC

SPE 具有与处理器主内存分离的内存空间,称为本地存储。SPE 不能直接读取主存,相反地必须通过对内存流控制器(或 MFC)的单元使用 DMA 命令来在本地存储和主存之间导入和导出数据。本地存储地址空间限制为 32 位,但通常会更小(比如,在 Sony® PLAYSTATION® 3 中就只有 18 位)。其原因是为了让由 SPE 访问的主存成为可确定的。主存可被换出、搬移、缓存、不缓存或做内存映射处理。因而,某一内存访问所需的时间是完全未知的(如果内存被换出,所需时间的长短是不可知的)。 通过将 SPE 内存分到本地内存,SPE 针对任何它访问的内存就可以有一个确定的访问时间,且可以调度 MFC 在需要时异步将数据移入和移出主存。SPE 在本地存储中的地址称为本地存储地址 (LSA),而在主存内的地址称为有效地址 (EA)。对于学习如何使用内存流控制器的 DMA 工具而言,理解这些概念将会十分重要。

SPE 通过使用信道 与外界通信。信道是一个 32 位的区域,通过使用特殊指令可被重写或读取(但不可能二者兼备 —— 它们是无方向的)。信道具有深度属性,也称信道数。信道数是等待被读取的数据量(对于读信道而言)或仍能被写入的数据量(对于写信道而言)。信道用于所有的 SPE 输入和输出。它们可用来向内存流控制器发出 DMA 命令、处理 SPE 事件、向 PPE 和从 PPE 读写消息。我要展示的下一个程序使用了 MFC 和信道接口来在由 PPE 指定的数据上进行字符传输。

创建和运行 SPE 任务

到目前为止,main 函数还尚未使用任何参数。但它是从 PPE 程序运行的,实际上,它接收 3 个 64 位参数:寄存器 3 中的 SPE 任务标识符、到寄存器 4 中的应用程序参数的指针和到寄存器 5 中的运行时环境信息的指针。由应用程序和环境指针指定的区域的内容实际上是用户定义的。但请记住它们指向在应用程序主存储内 的内存(有效地址),而非到 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 程序本身的数据的结构的指针。通常数据可通过两种方式定义:一种是通过在 PPU 可执行程序内嵌入 SPU 应用程序的自动定义(稍后将详细介绍),在包含此 SPU 应用程序的库上使用 dlopen()/dlsym();另一种方法是借助 spe_open_image() 直接加载 SPU 应用程序。
  • argp
    是到特定于应用程序数据的指针,用于程序的初始化。如果不用,可将其设为 null。
  • envp
    是到此程序的环境数据的指针。如果不用,可将其设为 null。
  • mask
    是处理器亲和性掩码。将其设置为 -1 意味着将过程分配给任何可用的 SPE。否则,它将包含针对每个可用处理器准备的位掩码。1 意味着这个处理器应该被使用,0 意味着它不应被使用。大多数应用程序都将它设为 -1。
  • flags
    是一组位标志,用来修改 SPE 将如何被设置。上述这些参数均超出了本文的讨论范围。

使用 DMA 的 PPE/SPE 程序

作为 DMA 通信的一个例子,我将编写一个程序,其中 PPE 接受一个字符串,并调用在此字符串上进行复制的 SPE 程序、将其转换成大写,再复制回主内存。所有传递的数据都将使用 MFC 的 DMA 工具,由 SPE 信道控制。

主 SPE 程序将接收到包含字符串大小和指针的 struct 的有效地址指针。它随后会将其复制到其缓冲区,执行转换任务再将其复制回来。以下是对应的 SPE 代码(将其作为 convert_dma_main.s 输入):

清单 7. 为 PPU 程序执行大写转换的 SPU 代码
.data

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

.section .bss #Uninitialized Data Section
.align 4
.lcomm conversion_buffer, 16384

.text
.global main
.type main, @function

#MFC Constants
.equ MFC_GET_CMD, 0x40
.equ MFC_PUT_CMD, 0x20

#Stack Frame Constants
.equ MAIN_FRAME_SIZE, 80
.equ MAIN_REG_SAVE_OFFSET, 32
.equ LR_OFFSET, 16

main:
	#Prologue
	stqd $lr, LR_OFFSET($sp)
	stqd $sp, -MAIN_FRAME_SIZE($sp)
	ai $sp, $sp, -MAIN_FRAME_SIZE

	#Save Registers
	#Save register $127 (will be used for current index)
	stqd $127, MAIN_REG_SAVE_OFFSET($sp)
	#Save register $126 (will be used for base pointer)
	stqd $126, MAIN_REG_SAVE_OFFSET+16($sp)
	#Save register $125 (will be used for final size)
	stqd $125, MAIN_REG_SAVE_OFFSET+24($sp)

	##COPY IN CONVERSION INFORMATION##
	ila $3, conversion_info         #Local Store Address
	#register 4 already has address #64-bit Effective Address
	il $5, CONVERSION_STRUCT_SIZE   #Transfer size
	il $6, 0                        #DMA Tag
	il $7, MFC_GET_CMD              #DMA Command
	brsl $lr, perform_dma

	#Wait for DMA to complete
	il $3, 0
	brsl $lr, wait_for_dma_completion

	##COPY STRING IN TO BUFFER##
	#Load buffer data pointer
	ila $3, conversion_buffer #Local Store
	lqr $4, conversion_data   #64-bit Effective Address
	lqr $5, conversion_length #SIZE
	il $6, 0                  #DMA Tag
	il $7, MFC_GET_CMD        #DMA Command
	brsl $lr, perform_dma

	#Wait for DMA to complete
	il $3, 0
	brsl $lr, wait_for_dma_completion

	#LOOP THROUGH BUFFER
	#Load buffer size
	lqr $125, conversion_length
	#Load buffer pointer
	ila $126, conversion_buffer
	#Load buffer index
	il $127, 0
loop:
	ceq $7, $125, $127
	brnz $7, loop_end

	#Compute address for function parameter
	a $3, $127, $126
	#Next index
	ai $127, $127, 1

	#Run function
	brsl $lr, convert_to_upper

	#Repeat loop
	br loop

loop_end:
        #Copy data back
        ila $3, conversion_buffer   #Local Store Address
        lqr $4, conversion_data     #64-bit effective address
        lqr $5, conversion_length   #Size
        il $6, 0                    #DMA Tag
        il $7, MFC_PUT_CMD          #DMA Command
	brsl $lr, perform_dma

        #Wait for DMA to complete
	il $3, 0
	brsl $lr, wait_for_dma_completion

	#Return Value
	il $3, 0

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

这段代码依赖于一些实用函数来处理 DMA 命令。将这些函数作为 dma_utils.s 输入:

清单 8. DMA 传输的实用工具
##UTILITY FUNCTION TO PERFORM DMA OPS##
#Parameters - Local Store Address, 64-bit Effective Address, Transfer Size, 
DMA Tag, DMA Command
.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:
	#We receive a tag in register 3 - convert to a tag mask
	il $4, 1
	shl $4, $4, $3
	wrch $MFC_WrTagMask, $4
	#Tell the DMA that we only want it to inform us on DMA completion
	il $5, 2
	wrch $MFC_WrTagUpdate, $5
	#Wait for DMA Completion, and store the result in the return value
	rdch $3, $MFC_RdTagStat
	#Return
	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 可链接项,它允许 SPE 的对象文件嵌入到 PPE 应用程序内并在需要时加载。

以下是使用了 SPU 代码的 PPU 代码(将其作为 ppu_dma_main.c 输入):

清单 9. 利用了 SPU 应用程序的 PPU 代码
#include <stdio.h>
#include <libspe.h>
#include <errno.h>
#include <string.h>

/* embedspu actually defines this in the generated object file,
 we only need an extern reference here */
extern spe_program_handle_t convert_to_upper_handle;

/* This is the parameter structure that our SPE code expects */
/* Note the alignment on all of the data that will be passed to the SPE is 16-bytes */
typedef struct {
	int length __attribute__((aligned(16)));
	unsigned long long data __attribute__((aligned(16)));
} conversion_structure;

int main() {
	int status = 0;
	/* Pad string to a quadword -- there are 12 spaces at the end. */
	char *tmp_str = "This is the string we want to convert to uppercase.            ";
	/* Copy it to an aligned boundary */
	char *str = memalign(16, strlen(tmp_str) + 1);
	strcpy(str, tmp_str);
	/* Create conversion structure on an aligned boundary */
	conversion_structure conversion_info __attribute__((aligned(16)));

	/* Set the data elements in the parameter structure */
	conversion_info.length = strlen(str) + 1; /* add one for null byte */
	conversion_info.data = (unsigned long long)str;

	/* Create the thread and check for errors */
	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;
	}

	/* Wait for SPE thread completion */
	spe_wait(spe_id, &status, 0);

	/* Print out result */
	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 命令的命令行被设置的。那个变量就是到程序代码的句柄,用来创建 SPE 任务。

接下来,定义结构,该结构将被用作到 SPE 程序的参数。这需要您提供字符串的长度和到字符串本身的指针。而且它们都是按四字节对齐的以便能够将其复制进您的主程序并在 DMA 传输时使用值。注意所使用的指针被声明为 unsigned long long 而非指针。这是因为传输的地址被按同样的方式存储,而不管它是被按 32 位模式还是 64 位模式编译的。对于指针,如果它是在 32 位的模式编译的,指针将被在结构内按不同方式对齐。另外,还需要使用 memalign 函数和 strcpy 将数据复制到合适对齐的区域。如果不断收到 “bus error”,那么很可能是因为进行的是一个 DMA 转换,该转换要么没有按 16 字节对齐,要么不是 16 字节的整数倍。

在主程序,声明变量。注意所有声明的变量(使用 DMA 复制的)都是按四字边界对齐的且都是四字的整数倍。那是由于大多数 DMA 传输(只有几个小的传输有些例外)都必须在源地址和目的地址按四字对齐(如果源和目的都是按 128 字节对齐的,程序将可获得更好的性能)。接下来,SPE 任务用 spe_create_thread 创建,传递进参数结构。现在,只需用 spe_wait 等待 SPE 任务完成,然后打印出最终的值。正如您所猜测的那样,程序大多数有趣的部分都发生在 SPE,包括所有的 DMA 传输。DMA 传输基本上总由 SPE 而非 PPE 完成,原因是 SPE 较 PPE 而言可以处理更多的数据和更多的当前 DMA 操作。

在深入介绍主程序细节之前,我要先来探讨一下 DMA 实用函数。第一个函数是 perform_dma,毫无疑问,该函数执行 DMA 命令。Cell BE 手册的第 450 到 456 页定义了执行 DMA 传输所需的信道操作的顺序(请参见 参考资料)。函数所做的第一件事情是将寄存器 4 内的 64 位有效地址转换为 2 个 32 位的组件 —— 一个高阶组件和一个低阶组件(记住,信道只有 32 位宽)。由于信道是通过寄存器单字大小的首选槽写入的,所以 64 位地址在首选槽内已经有了高阶位。因此,只需将内容向左移位四个字节到新的寄存器来获得在首选槽内的低阶位。然后,使用 wrch 指令将本地存储地址、有效地址的高阶位、有效地址的低阶位、转换的大小、DMA 命令的“标签”和命令本身写到它们合适的信道。当命令被写入时,DMA 请求会被排队放入 MFC,条件是 MFC 具有可用的槽 —— 本例中自然如此,原因是本例没有进行任何并发的 DMA 请求。“标签”是一个数值,可分配给一个或多个 DMA 命令。所有用相同的标签发出的 DMA 命令被认为是单一一个组,并且状态更新和排序操作都会作用于整个组。在这个应用程序中,一次只有一个 DMA 命令是激活的,所以所有的操作都将使用 0 作为 DMA 标签。DMA 命令可以是 MFC_GET_CMD,也可以是 MFC_PUT_CMD。另外还有其他的,但我们只关心这里的这些。MFC 命令都是从 SPE 的角度完成的,而不管它实际上是不是 SPE 发出的命令。所以,MFC_GET_CMD 将数据从内存移到本地存储,而 MFC_PUT_CMD 却反之。

由于 DMA 命令是异步的,所以如能等待一个命令完成,将十分有用。函数 wait_for_dma_completion 就可实现这样的功能。它接受标签作为惟一一个参数,将它转换为标签掩码、请求 DMA 状态然后再读取状态。但这是如何实现等待 DMA 操作完成的呢?当用值 2 向 $MFC_WrTagUpdate 信道写信息时,它会促使 $MFC_RdTagStat 无值存在,直到操作完成。这样,当试图使用 rdch 读取信道时,它将会一直阻塞,直到状态可用为止,只有在这时,传输才会完成。

现在,转到实际的程序本身。SPE 程序首先为应用程序参数数据保留空间。并也会对齐到四字边界(汇编语言中的 .align 4 与 C 中的 __attribute__((aligned(16))) 功能相同,原因是 2^4 = 16)。.octa 保留四字值(此缩写形式延用了 16 位时的用法)。随后为整个结构的大小定义常量 CONVERSION_STRUCT_SIZE

这之后,进入到 .bss 部分,此部分与 .data 部分基本相同,惟一的不同之处在于可执行程序本身不包含值,它只关注应该为它们保留多少空间。此部分为非初始化数据保留。 .lcomm conversion_buffer, 16384 保留 16K 空间,符号 conversion_buffer 定义开始地址。它针对保留 16K 空间定义,原因是 16K 是 MFC DMA 传输的最大大小。因此,如果字符串比 16K 长,PPE 就必须多次调用此程序(更好的做法是在 SPE 端将请求分解成小块)。

main 函数是程序的主要部分。它首先设置一个堆栈框架。然后保存 3 个用于进行程序的主要控制的非易失性寄存器。接下来,执行 DMA 传输以在参数结构内从 PPE 复制。记住,到此函数的第一个参数是传递自 PPE 的 64 位地址。然后使用 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 标签和 MFC_GET_CMD DMA 命令都将被传递给 perform_dma。程序会等待操作完成。

此时,所有数据都将被加载,只需对其进行传输即可。然后再将寄存器 127 用作循环计数器,将寄存器 126 用作基指针,并在每个值上执行 convert_to_upper,直到到达缓冲区的底部为止。

loop_end 中,所有数据都被传输,只需将其复制回来。使用相同的 DMA 参数作最后的传输,但这次使用的是 MFC_PUT_CMD 命令。一旦 DMA 完成,函数也即完成。需要用返回值加载寄存器 3 并执行函数尾声(epilogue)来恢复堆栈框架并返回。


借助邮箱进行 SPE/PPE 间的通信

虽然 DMA 传输是一个很好的在 SPE 和 PPE 传输大量数据的方法,但对于小型的数据传输来说,还有另一种更为简单的方法,即 邮箱,在这里简单介绍一下。对于 SPE 而言,邮箱就是一组信道集(包括写信道和读信道),用来将 32 位值写到 PPE。

为了说明这个概念,我特意编写了一个非常简单的 SPE 服务器程序,该服务器程序等待邮箱中的无符号整数并返回该数值的平方值。以下就是所需的代码(将其作为 square_server.s 输入):

清单 10. SPU 求平方的服务器程序
.text
.global main
.type main, @function
main:
	#Read the value from the inbox (stalls if no value until one is available)
	rdch $3, $SPU_RdInMbox
	#Square the value
	mpyu $3, $3, $3
	#Write the value back
	wrch $SPU_WrOutMbox, $3
	#Go back and do it again
	br main

就这些!它完全可以用来等待请求并处理请求了。当父程序退出,它才会退出。而且,如果在收件箱没有可用数据,rdch 指令就会一直暂停,直到可用数据出现。

PPE 端的程序也不负责(作为 square_client.c 输入):

清单 11. PPE 求平方的客户程序
#include <libspe.h>
#include <stdio.h>

extern spe_program_handle_t square_server_handle;

int main() {
	int status = 0;

	/* Create SPE thread */
	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;
	}

	/* Request a square */
	spe_write_in_mbox(spe_id, 4);
	/* Wait for result to be available */
	while(!spe_stat_out_mbox(spe_id)) {}
	/* Read and display result */
	printf("The square of 4 is %d\n", spe_read_out_mbox(spe_id));

	/* Do it again */
	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 的邮箱)都是从 SPE 的角度定义的。所以如果是 PPE,就会写入到收件箱,读取自发件箱。与 SPE 不同, PPE 在读写时并不会停下来等待值的出现。相反,程序必须使用 spe_stat_out_mbox 来等待值的出现,并用 spe_stat_in_mbox 来检查是否有剩余的槽可供写邮箱之用。您无需使用后者,因为您一次只处理一个值。

当程序组合使用了邮箱和 DMA 方法时,才会显出邮箱的真正实力。例如,可以创建 SPE 任务来侦听其邮箱上的缓冲区地址,然后使用该地址拉入所有要通过 DMA 处理的数据。


结束语

至此,本系列已涵盖了在安装了 Linux® 的 PLAYSTATION 3 的 Cell BE 处理器上进行汇编语言编程的几乎所有的主要概念。所讨论的主题包括基本的架构、SPU 汇编语言的基本语法及 SPE 与 PPE 间的主要通信模式。下一篇文章将深入探讨一下如何充分挖掘 Cell BE 处理器 SPE 的性能。这之后的文章会将这一知识应用到 C 中的 SPE 编程,以使编程工作更加轻松。

参考资料

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Multicore acceleration, Linux
ArticleID=208919
ArticleTitle=在 Cell BE 处理器上编写高性能的应用程序,第 3 部分: 接触 SPU
publish-date=04122007