在 Cell BE 处理器上编写高性能的应用程序,第 2 部分: 在 Sony PLAYSTATION 3 的 SPE 上编程

SPE 概览

本期的 在 Cell BE 处理器上编写高性能的应用程序 将向您介绍如何充分利用 Sony® PLAYSTATION® 3 (PS3) 的 SPE (synergistic processing element)。本系列的第 1 部分展示了如何在 PS3 上安装 Linux® 并给出了一个简短的示例程序。第 2 部分将对 Cell Broadband Engine™ 处理器的 SPE 做深入的探讨,研究一下这些 SPE 的底层工作原理。

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

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



2007 年 4 月 12 日

本系列的 前一期文章 总体介绍了 Cell Broadband Engine (Cell BE) 处理器(关于其他介绍性信息,参见该文章后面的 参考资料 部分)。第 2 部分将就 Cell BE 芯片的 SPE 做深入的探讨(有关编程实现 Power processing element (PPE) 的深入讨论,请参见 developerWorks Linux 专区的 “用于 Power 体系结构的汇编语言 ”系列)。由于 SPE 使用了极为不同的架构,所以要想整体了解其原理,很有必要将其放到汇编语言中进行审视。这之后,我会向您展示如何在 C 中对它们进行编程,但比较而言,汇编语言能够更好地体现该处理器的独特性。当转而使用 C 时,您将会理解不同的编码决定是如何影响性能的。本期文章重点放在基本的语法和 SPE 汇编语言及 ABI(application binary interface 或平台的函数调用约定)的用法上。后续的两篇文章将会深入探究 SPE 和 PPE 间的通信以及如何使用 SPE 汇编语言的独特特性来优化代码。

正如在 前一期文章 中提到的,Cell BE 芯片由 PPE 组成,而 PPE 又有几个 SPE。PPE 负责运行操作系统、管理资源和输入/输出。SPE 负责数据处理任务。SPE 不能直接访问主存,只能访问很小一部分(PS3 上 256K)的本地存储 (LS),LS 是一个独立的 32 位地址空间。本地存储地址空间中的地址称为本地存储地址 (LSA),而 PPE 上控制过程内的地址则称为有效地址 (EA)。SPE 包括一个附加的内存流控制器 (MFC)。SPE 通过 MFC 在本地存储、主存和其他 SPE 间传输数据。

SPU 是实际运行代码的 SPE 的一部分。SPU 具有 128 个通用寄存器,每个寄存器都是 128 位宽。但是,SPU 的主要用处并不是进行 128 位值的操作,它是一个向量 处理器。这就是说,每个寄存器都被分为多个较小的值,指令作用于所有的值上。通常,寄存器被视为 4 个 32 位值(32 位被认为是 SPU 的字大小),但也可将寄存器视为 16 个 8 位值(字节)、8 个 16 位值(半字)、2 个 64 位值(双字)或一个单一的 128 位值(四字)。本文中的代码实际上是非向量的(即标量)代码,这意味着一次只处理一个值。虽然代码也使用了一些向量运算,但我们只关注一个寄存器内的一个值,而其他值则被简单忽略。本系列 的后续文章会深入介绍向量运算。

本篇文章并不要求您具有汇编语言的经验,当然,如果有相关的经验会十分有帮助。本文会比较 SPE 和 PPE 的一些特性,但并不要求您必须十分了解 PPE。有关基于 Power Architecture 的 PPE 特性的一些讨论,请参见 “用于 Power 体系结构的汇编语言” 系列。

本文中的构建命令假设您已经根据 第 1 部分 中的指导安装了 Yellow Dog Linux。如果使用的是其他发布版,一些命令名和标志可能需要更改。比如,如果您使用的是 1.2 SDK (IBM 已经发布了 2.0 SDK,但 1.2 SDK 是 YDL 所附带的)的 IBM System Simulator,那么您需要将所有的 gcc 引用更改为 ppu-gcc,将所有的 embedspu 引用更改为 ppu-embedspu。根据库和头文件的安装位置,可能还需要传递额外的标志以查找它们。

一个简单的示例程序

在开始介绍 SPU 汇编语言之前,先来看一个通过递归算法计算 32 位数的阶乘的简单程序。该算法的递归特性将十分有助于展示标准 ABI。

以下给出了实现相同功能的 C 代码以便参考:

清单 1. C 版本的阶乘程序
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

要构建该程序,只需使用 C 编译器,如下所示:

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 位空间存储值。在本例中,存储的是数值 4。

注意,在定义 number 之前,需要使用 .align 4 进行对齐。.align 操作告诉编译器按特定的边界对齐下一个指令或声明。.align 4 会将下一个内存位置对齐到 16 字节 (2^4) 的边界。这十分关键,因为 SPU 一次只能加载 16 字节,并对齐到 16 字节的边界。如果要加载自的地址不是一个 16 字节的边界,它就会将该地址的最后四位截断然后再加载,以便使其能够加载。因而,如果值没有被正确对齐,它就会在寄存器的任意位置 被加载 —— 而且很可能是您不想加载的位置。通过将它对齐到 16 位边界,就可以确保它将会被加载进寄存器的前四个字节。这之后是另一个用来对齐输出字符串起始位置的对齐语句。.ascii 声明告诉编译器随后出现的是一个 ASCII 字符串,该字符串以 \0 结束。

之后,为堆栈框架定义几个常量。记住当程序进行函数调用(特别是针对递归函数)时,它必须在堆栈上存储返回地址和本地变量。在 C 和其他高级语言中,语言本身会管理运行时堆栈。而在汇编语言中,却需要程序员来显式处理。在程序开始时,由操作系统为您设置堆栈。堆栈的开始是一些高编号的地址,随着堆栈框架的添加,逐渐向低编号的地址发展。您必须为每个堆栈分配空间并将合适的值移到此空间。在本程序中,需要两个堆栈框架大小 —— 一个用于 main,一个用于 factorial。每个堆栈框架都有一个指向前一个堆栈框架的指针(称为回链指针),还有一个当它调用其他函数时用于存放返回地址的空间。这些地址中的每一个的大小都只有一个字(4 字节),但它们还是会被按 16 字节对齐以便于加载和存储(请记住 SPU 只能从 16 字节对齐的地址加载)。剩余的空间用来保存寄存器和存储本地变量。main 的堆栈最小,为 32 字节,而 factorial 的堆栈则为 48 字节,原因是 factorial 需要存储本地变量。要在程序内命名这些数量并使代码更易于阅读,可以通过 .equ 操作将符号赋予这些值。这就告诉编译器将给定的符号和给定的值对等起来。两个堆栈框架的大小分别赋给了符号 MAIN_FRAME_SIZEFACT_FRAME_SIZELR_OFFSET 是返回地址的堆栈框架的偏移量。LCL_NUM_VALUE 是本地变量 num 的堆栈偏移量。上述这些做法目的是使在代码的主体访问堆栈框架变得更为清楚明了。

在代码部分,定义了一个函数地址,方法同上述定义全局变量的相同 —— 只需在它们的名称之后跟上一个冒号。这表明函数地址将是下一个指令的地址。.type 用来告诉链接器该值应该被用作函数,而 .global 则用来告诉链接器该符号在链接时可以在当前文件之外被引用。main 必须被声明为全局的,因为它是程序的入口点。接下来,我将深入讨论一下实际的汇编指令本身。

我会在探讨 factorial 函数时再深入介绍此序言(prologue)的意义所在。就目前而言,只需知道它将设置堆栈框架就可以了。

您所看到的第一个实际的指令是 lqr $3, number,其含义是 “load quadword relative”。“quadword” 部分有些多余,因为 SPU 只允许加载和存储四字。该指令会把地址 number(编码为相对于当前指令的地址)内的值加载到寄存器 3。与 PPE 汇编语言不同,SPE 汇编语言寄存器总是以美元符号开始。这就使得寄存器在代码中更为醒目。因为所有 SPU 上的寄存器都是 16 字节长,因而这会将整个 16 字节的四字加载进寄存器,而我们所关心的只是它的前 4 个字节。

在寄存器 3 中想要做的是计算此值的阶乘。因而,需要将其作为第一个(也是惟一一个)参数传递给 factorial 函数。与 PPU ABI 一样,SPU ABI 也使用寄存器来将值传递给函数。寄存器 3 应该保存第一个参数,寄存器 4 应该保存第二个参数,以此类推。所以,加载进寄存器 3 的值就是该函数要用的值。尽管寄存器可以存储多个值(在本例中,4 个 32 位值),但当参数传递给函数时,每个参数值都会在其自己的寄存器内被传递。

这就带来了一个问题:寄存器究竟是用来做什么的?如果您以前从未用汇编语言编过程,寄存器就是处理器为计算值所使用的临时存储。由于 SPU 有 128 个寄存器,所以它可以存储大量临时值和中间值,而无需像其他架构一样,必须加载和向内存转存。这就使得编程变得更为容易,执行起来也更为迅速。SPU 对寄存器如何使用没有特殊规定,但 ABI 却与之不同。以下是 ABI 对 SPU 内的寄存器使用的特殊规定:

SPU ABI 内的寄存器使用
寄存器范围类型用途
0专用链接寄存器
1专用堆栈指针
2可变环境指针(针对有此需要的语言)
3-79可变函数参数、返回值和通用的一些使用
80-127非可变用于本地变量,必须跨函数调用保留

稍后,我会详细介绍链接寄存器,总的来说,它用于临时存储返回地址。堆栈指针给出的是当前堆栈框架的结束位置。环境指针在大多数语言中并未使用。所有标有可变 的寄存器都可以在函数内自由改变。但这意味着当函数进行函数调用时,所有在可变寄存器中的值都有可能会被重写。 所有标有非可变 的寄存器必须在使用前将它们之前的值保存起来并在从函数调用返回之前加以恢复。这就让您有一组寄存器可跨函数调用保留。但,这类寄存器使用起来比较麻烦,因为需要用代码实现先前值的保存和恢复。返回值返回至寄存器 3 。

由于需要对数值 4 进行阶乘,它进入的是用来保存第一个参数的寄存器 3。然后需要使用 brsl $lr, factorial 对函数进行分支。brsl 代表 “branch relative and set link”,用来分支到函数的入口点并将链接寄存器 (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。

该段代码的尾声(epilogue)将在分析 factorial 代码时再作讨论,但总的来说,它的作用是结束堆栈框架并返回到先前的函数。

在讨论 factorial 函数之前,先要了解堆栈框架的布局。以下是 ABI 的堆栈框架布局:

包含的部分大小开始堆栈偏移量
寄存器保存区可变(16 字节的整数倍)可变
本地变量空间可变(16 字节的整数倍)可变
参数列表可变(16 字节的整数倍)32($sp)
链接寄存器保存区16 字节16($sp)
回链指针16 字节0($sp)

回链指针指向前一个堆栈框架的回链指针。链接寄存器保存区存有被调用函数(而非当前函数)的链接寄存器内容。参数列表中的参数是该函数发送给其他函数调用的参数,而非其自身的参数。但是,与 PPE 不同,这只用在参数的数量大于参数可用的寄存器数量的情况下(这种场景并不常见)。本地变量空间用作该函数的通用存储空间,寄存器保存区用于保存函数所使用的非可变寄存器的值。

所以,在这个函数中,我们使用了回链指针、链接寄存器保存区和一个本地变量。这样一来,框架的大小就为 16 * 3 = 48 字节。 正如我先前提到的,LR_OFFSET 是堆栈末端到链接寄存器保存区的偏移量。LCL_NUM_VALUE 是堆栈末端到本地变量 num 的偏移量。

序言 为函数设置堆栈框架。在序言中,所要做的第一件事情是保存链接寄存器。由于您尚未定义自己的堆栈框架,所以偏移量是由调用函数的堆栈框架的末端算起的。请记住链接寄存器存储在调用函数的堆栈框架内,而非函数自身的堆栈框架。所以十分有必要在保留堆栈空间之前先保存它。这可以通过所谓的 D-Form 存储(D-Form 是一种指令格式)加以实现。在 Assembly language for Power Architecture, Part 2 中可以找到有关 PPU 指令格式的概览(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 的和,并使用它作为目的地址。所以该指令会将链接寄存器(存有返回地址)存储到调用函数堆栈框架的恰当位置。

接下来,当前堆栈框架指针会被存储为指向下一个堆栈框架的后向指针,虽然尚未建立堆栈框架(这是通过负的偏移量实现的)。与 PPU 不同,SPU 没有原子存储/更新指令,所以要确保后向指针是一致的,必须在移动堆栈指针之前存储后向指针。最后,移动堆栈指针来通过指令 ai $sp, $sp, -FRAME_SIZE 保留所有所需的堆栈空间。ai 代表的是 “add immediate”,用来向寄存器添加一个立即方式值并将其存储回寄存器。它将第二个操作数内的寄存器与第三个操作数内的常量加在一起,并将结果存储在第一个操作数内指定的寄存器中。大多数指令都遵循类似的格式,结果存储在第一个操作数内指定的寄存器中。

注意 ai 指令是个向量运算。SPU 寄存器均为 128 位宽,但我们的值只有 32 位长。寄存器逻辑上被视为多个值,这些值被同时操作。ai 指令实际上将寄存器视为 4 个 32 位值,每一个都添加了 -FRAME_SIZE,然后它们均被存储回目的寄存器。SPU 的首选值大小为 32 位字,但也支持其他的大小,包括字节、半字和双字。如果操作数的大小未在指令中指定,这就意味着操作数的大小并不十分重要(例如逻辑指令)或者它的大小为 32 位。如果指令中包含字母 b 就表明是字节,如果包含字母 h 就表明是半字,如果包含字母 d 就表明是双字,双字通常只用在浮点指令中(通常指令里的 d 指的是 D-Form 格式的寻址,而非双字)。但在本例中,我们只关心寄存器里的第一个字。其他值在 ABI 中的意义不大。

接下来,通过 stqd $3, LCL_NUM_VALUE($sp) 将第一个参数复制到本地变量。之所以需要这么做是因为参数会在递归函数调用上被截断,而以后却还需要访问它。

接下来,通过 ceqi $4, $3, 0 将寄存器 3 与数值 0 做立即模式对比并将结果存储在寄存器 4 中。注意如果是 PPU(和与之相关的大多数处理器),一种特殊用途的寄存器可用来保存条件结果。但,如果是 SPU,结果会存储在一种通用的寄存器内 —— 在本例中就是寄存器 4。请注意这是一个向量处理器,所以并非将寄存器 3 与数值 0 作实际对比,相反,所对比的是寄存器 3 的每一个字与数值 0。所以答案有 4 个,而我们只关心其中的一个。结果按如下方式存储:如果此字的条件为真,那么目标字的所有位都将被设置;如果此字的条件为假,那么目标字的所有位都将被清除设置。所以此指令会有 4 个结果,根据比较结果的不同,它们或者全是 1 或者全是 0。

下一个指令是 brnz $4, case_zerobrnz 代表的是 “branch relative if not zero”。寄存器 4 是前一个比较的结果,所以这个指令是用来检查前一个比较结果是 0 还是非 0 的。如果之前是否为零的测试的结果为真,那么结果寄存器将是非 0(为真,所有位均被置 1)。注意前两个指令可以合并为一个指令 (brz $3, case_zero),原因是所测试的只是是否为零,我之所以将它们分开为两个指令是为了能够让您更好地理解比较和分支是如何工作的。

如果这些比较的结果有的为真,而其余的为假,那么又该如何呢?由于所处理的是 4 个 32 位的值而非一个 128 位的值,所以针对不同的值就可能会有不同的结果。如果结果不同,是否需要进行分支处理呢?几个 SPU 指令只能处理这些寄存器值中的一个。在这些情况下,所使用的值是在寄存器的首选槽中的那个。对于 64 位值来说,就是寄存器的前半部分;对于 32 位值来说,就是寄存器的第一个字;对于 16 位值来说,就是寄存器的第二个半字;对于 8 位的值来说,就是寄存器的第四个字节。基本上,第一个字就是首选字,而其他字对齐到最低有效字节或半字上。当进行条件分支、向函数传递值、从函数返回值以及其他操作时,首选槽内的值才是关键的。在本例中,假设在函数中传递的值就处于寄存器的首选槽内。而且,.data 中的 alignment number 将会被加载进首选槽内。所以,只要值位于寄存器的首选槽之内,分支就会正常发生。

现在假设所处理的寄存器 3 中的数值为非零,这就意味着需要进行递归的步骤。递归的 C 代码是 return num * factorial(num - 1)。最里面的计算要求递减 num 并将其作为参数传递给下一个 factorial 调用。num 已经在寄存器 3 中,所以只需递减它即可。所以可以这样进行立即模式添加:ai $3, $3, -1。现在,调用下一个 factorial。要根据 SPU ABI 调用函数,所需做的是将参数放入寄存器,然后调用 brsl $lr, function_name。在本例中,第一个也是惟一一个参数已经加载进寄存器 3。所以,需要发起 brsl $lr, factorial。正如我之前提到过的,brsl 代表的是 “branch relative set link”;目标地址被编码为相对地址,返回地址则被存储在指定寄存器的首选槽内,而控制权将由目的地址获得,在本例中它就回到了. factorial 函数的开始部分。

至此,阶乘的结果就应该已经存在于寄存器 3 内了。现在您想用考虑中的当前值去乘该结果。所以必须将其加载回来,因为它在函数调用中被截断过了。lqd 代表的是 “load quadword D-Form”;第一个操作数是目标寄存器,第二个操作数是要加载的 D-Form 地址。因此 lqd $5, LCL_NUM_VALUE($sp) 会将之前堆栈中保存的那个值读取到寄存器 5 内。

现在需要用寄存器 3 乘以寄存器 5。这可以通过 mpyu 指令(无符号相乘)实现。mpyu$3, $3, $5 用寄存器 3 乘以寄存器 5 并将结果存储在所列出的第一个寄存器内,即寄存器 3。现在,SPU 上的整数相乘指令多少有点问题,特别是有符号乘法(使用 mpy 指令)。问题在于乘法指令的结果可能会有其操作数的两倍之长。两个 32 位数相乘的结果实际上是一个 64 位的值!如果的确如此,那么目的寄存器将会有源寄存器的两倍之长。为了解决这一问题,乘法指令只使用每个 32 位的最低有效的 16 位以便结果能够放进整个 32 位寄存器内。所以,当乘法将源寄存器视为 32 位宽时,它只使用了其中的 16 个位。因此,如果值的长度超过 32 位,该值就会被截断。而且,如果进行的是有符号乘法,符号还有可能会由于截断而改变!因此,要成功执行乘法指令,源值就需要为 16 位宽,但被存储在 32 位寄存器内(如果它被符号扩展到 32 位,对于乘法运算来说关系并不大)。这极大地限制了阶乘函数的可能范围。注意浮点乘法没有这样的问题。

现在结果出来了,在寄存器 3 内,这也是它应该存在于的位置。剩下要做的只是恢复先前的堆栈框架并返回。所以您只需通过使用 ai $sp, $sp, FRAME_SIZE 将堆栈框架大小加到堆栈指针来移动堆栈指针即可。然后使用 lqd $lr, LR_OFFSET($sp) 恢复链接寄存器。最后,bi $lr(branch indirect)分支跳转到在链接寄存器内指定的地址(返回地址),进而从函数返回。

基线条件(当函数参数为零时作何处理)十分简单。阶乘(0)的结果为零,所以只需通过 il $3, 1 将数值 1 加载到寄存器 3 中。之后再恢复堆栈框架并返回。但由于基线条件并不调用任何其他函数,所以无需从堆栈框架加载链接寄存器 —— 值还在原处。

这就是该函数的工作原理。请注意在 SPE 上编写深度递归函数有些问题,原因是在 SPE 上没有任何堆栈溢出保护,而且本地存储也很小。


结束语

本文涵盖了在安装了 Linux 的 PLAYSTATION 3 的 Cell BE 处理器上进行汇编语言编程的一些主要概念。在下一期文章,我将向您介绍 SPE 与 PPE 间的通信模式。

参考资料

学习

获得产品和技术

讨论

条评论

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=Linux
ArticleID=208921
ArticleTitle=在 Cell BE 处理器上编写高性能的应用程序,第 2 部分: 在 Sony PLAYSTATION 3 的 SPE 上编程
publish-date=04122007