IBM®
메인 컨텐츠로 가기
    Korea [국가변경]    이용약관
 
 
   
        제품    서비스 & 솔루션    고객지원 & 다운로드    회원 서비스    
메인 컨텐츠로 가기

한국 developerWorks  >  파워 아키텍처 | 리눅스  >

Cell BE 프로세서의 고성능 애플리케이션 프로그래밍, Part 2: Sony PLAYSTATION 3의 Synergistic Processing Elements 프로그래밍 (한글)

SPE 개요

developerWorks
문서 옵션

JavaScript가 필요한 문서 옵션은 디스플레이되지 않습니다.

영어원문

영어원문


제안 및 의견
피드백

난이도 : 중급

Jonathan Bartlett, Director of Technology, New Medio

2007 년 5 월 22 일

Cell BE 프로세서의 고성능 애플리케이션 프로그래밍 시리즈를 통해 Sony PLAYSTATION 3의 Synergistic Processing Elements를 활용하는 방법을 설명하고 있습니다. Part 1에서는 PS3에 리눅스를 설치하는 방법과 간단한 예제 프로그램을 설명했습니다. Part 2에서는 Cell Broadband Engine™ 프로세서의 SPE를 자세히 살펴보고, 이러한 엘리먼트들이 어떻게 작동하는지를 설명합니다.

이전 기술자료에서는 Cell Broadband Engine (Cell BE) 프로세서에 대해 살펴보았다. (참고자료) Part 2는 Cell BE 칩의 SPE부터 설명하도록 하겠다. (Power processing element (PPE) 프로그래밍에 대한 자세한 내용은 developerWorks 리눅스 존의 Power Architecture용 어셈블리 언어 시리즈를 참조하라.) SPE는 다른 아키텍처를 사용하기 때문에 어셈블리 언어에서 이들을 살펴보는 것이 도움이 된다. 나중에, C로 프로그래밍 하는 방법도 설명하겠지만, 어셈블리 언어가 이 프로세서의 특성을 더욱 잘 나타낸다. C 프로그래밍을 통해서는 다양한 코딩 결정이 성능에 어떻게 영향을 미치는지 이해할 수 있을 것이다. 이 글에서는 SPE 어셈블리 언어의 기본적인 신택스와 사용법, ABI (application binary interface, 또는 플랫폼의 규약(convention)을 호출하는 함수)에 초점을 맞춰 설명하겠다. 후속 두 편의 기술자료에서는 SPE와 PPE간 통신에 대해 설명하고, SPE 어셈블리 언어의 고유한 기능들을 사용하여 코드를 최적화 하는 방법을 설명하겠다.

이전 글에서 언급했듯이, Cell BE 칩은 여러 SPE들을 갖고 있는 하나의 PPE로 구성된다. PPE는 OS 실행, 리소스 관리, 인풋/아웃풋을 담당한다. SPE는 데이터 처리를 담당한다. SPE는 주 메모리로 직접 액세스 할 수 없고, 다만 독립적인 32-bit 어드레스 공간에 있는 작은 (PS3의 경우 256K) local store(LS)로만 액세스 할 수 있다. local store의 어드레스 공간 안에 있는 어드레스를 local store address(LSA)라고 하고, PPE 상의 제어 프로세스 내의 어드레스를 effective address(EA)라고 한다. SPE에는 memory flow controller(MFC)가 포함된다. SPE는 MFC를 사용하여 로컬 스토어, 주 메모리, 기타 SPE들 간 데이터를 전송한다.

Synergistic Processing Unit (SPU)는 코드를 실제로 실행하는 SPE 부분이다. SPU는 128 개의 범용 레지스터를 갖고 있고, 각각 128-bit 이다. 하지만, 이 SPU의 포인트는 128-bit 값에 대해 연산을 수행하지 않는다. 대신, 프로세서는 벡터(vector) 프로세서이다. 각 레지스터는 여러 개의, 작은 값들로 나뉘고, 명령어들은 이 모든 값에 대해 동시에 연산을 수행한다는 의미이다. 일반적으로, 레지스터는 네 개의 구분된 32-bit 값(32-bit는 SPU의 단어 크기로 간주된다.)으로 취급된다. 이들은 16 개의 8-bit 값(byte), 8개의 16-bit 값(halfword), 두 개의 64-bit 값(doubleword), 또는 하나의 128-bit 값(quadword)으로 취급될 수도 있다. 이 글에 소개된 코드는 실제로 non-vector (scalar) 코드이며, 한번에 한 개의 값으로만 작동한다. 일부 벡터 연산들을 사용하지만, 우리는 각 레지스터에 있는 하나의 값에 대해서만 다루도록 하겠다. 다른 부분들은 무시하도록 한다. 본 시리즈의 후속 기술자료에서 벡터 연산에 대해 다루도록 하겠다.

여러분이 어셈블리 언어를 다루어 보았다면 도움이 되겠지만, 그러한 경험이 꼭 필요한 것은 아니다. SPE의 일부 기능들은 PPE의 기능들과 비교 및 대조되겠지만, PPE를 잘 알지 못해도 된다. Power Architecture에 기반한 PPE의 기능에 대해서는 Power Architecture용 어셈블리 언어 시리즈를 참조하라.

본 글의 빌드 명령어들은 Yellow Dog Linux 기반이다. (Part 1) 다른 배포판을 사용하고 있다면 일부 명령어 이름과 플래그가 바뀔 수 있다. 예를 들어, 1.2 SDK(IBM은 2.0 SDK를 배포했지만, YDL에 포함된 것은 1.2 SDK 이다.)의 IBM System Simulator를 사용하고 있다면 모든 gcc 레퍼런스를 ppu-gcc로, 모든 embedspu 레퍼런스를 ppu-embedspu로 변경해야 한다. 라이브러리와 헤더 파일들이 설치된 장소에 따라, 이들을 찾는데 추가 플래그들이 실행될 수도 있다.

간단한 예제 프로그램

재귀(recursive) 알고리즘을 사용하여 32-bit 의 팩토리얼을 계산하는 간단한 프로그램을 통해 SPU 어셈블리 언어를 살펴보기로 한다. 이 알고리즘의 재귀적 특성은 표준 ABI를 설명하기에 알맞다.

다음은 같은 기능을 수행하는 C 코드이다.


Listing 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로 입력한다.


Listing 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가 리소스를 관리해야 한다. 하지만, 리눅스가 SPE용으로 작성된 프로그램이고 elfspe 패키지가 설치된다면, 리눅스는 최소한의 PPE 프로세스를 자동 생성하여 SPE용 리소스를 관리하고 SPE 프로세서의 수퍼바이저로서 작동한다. 따라서, elfspe 패키지를 설치했다면 SPE 프로그램을 정상적으로 실행할 수 있다.

./factorial

이것이 실행되지 않는다면, elfspe 패키지가 올바르게 설치되었는지를 확인하라. (이전 기술자료 참조)

이제 각각의 명령어와 선언이 어떻게 작동하는지를 보자.

프로그램은 전형적인 .data 선언으로 시작한다. 어셈블리 언어에서, 정적 데이터와 글로벌 변수들은 메모리에서 분리된다. 데이터와 코드 섹션들을 전환할 수 있지만, 프로그램이 어셈블링 될 때, 모든 섹션을 하나의 단위가 된다. .data는 데이터 섹션으로, .text는 코드 섹션으로 전환된다.

데이터 섹션에는 number라고 레이블링이 된 팩토리얼 값이 있다. 라인의 시작에 스트링 리터럴을 놓고, 뒤에 콜론을 붙이면 다음의 선언이나 명령어의 어드레스가 그 레이블에 의해서 프로그램 전반에 걸쳐 참조될 수 있다는 것을 나타낸다. 따라서, 코드에서 number가 있는 곳 어디에서나 그 다음 값의 주소를 참조하게 된다. .long은 32-bit 공간에 값을 저장하는 선언이다. 이 경우, 숫자 4를 저장한다.

number를 정의하기 전에 .align 4를 사용하여 이를 정렬한다. .align 연산은 어셈블러에게 다음 명령어 또는 선언을 특정 영역에서 정렬하도록 명령한다. .align 4는 그 다음의 메모리 위치를 16-byte (2^4) 바운더리로 정렬한다. SPU는 정확히 한번에 16-byte만 로딩할 수 있기 때문에 정확히 16-byte 바운더리로 정렬되는 것이 중요하다. 16-byte 바운더리가 아닌 곳에서 로딩할 주소가 주어진다면 이를 로딩하기 전에 마지막 4 비트의 어드레스를 0으로 만들어서 정렬된 로드가 될 수 있도록 한다. 따라서, 값이 올바르게 정렬되지 않으면 레지스터 아무데서나 로딩될 수 있다. 아마도 여러분이 예상할 수 없는 장소가 될 것이다. 이것을 16-byte 바운더리로 정렬하면, 이것이 레지스터의 첫 번째 네 개의 바이트로 로딩될 것이라는 것을 안다. 아웃풋을 제공하는 스트링의 시작에 대한 또 다른 정렬 문이 된다. .ascii 선언은 어셈블러에게 뒤에 오는 것이 ASCII 스트링이고, 이는 \0으로 명확히 끝났다라는 것을 알려준다.

이것 다음에, 스택 프레임을 위한 여러 상수들을 정의한다. 프로그램이 함수 호출을 할 때 스택에 리턴 어드레스와 로컬 변수를 저장해야 한다. C와 다른 고급 언어에서, 언어 자체는 런타임 스택을 관리한다. 어셈블리 언어에서, 이것은 프로그래머에 의해 핸들된다. 스택은 프로그램이 시작되면 OS에 의해 설정된다. 스택은 그 영역에서 높은 숫자가 매겨진 어드레스로 시작하고, 스택 프레임이 추가되면서 낮은 숫자가 매겨진 어드레스로 간다. 여러분은 각 스택에 대한 공간을 할당하고 올바른 값을 그 공간으로 이동시켜야 한다. 이 프로그램에서, 여러분은 두 개의 스택 프레임 사이즈를 갖게 되는데, 하나는 main용이고, 하나는 factorial용이다. 각 스택 프레임에는 이전 스택 프레임에 대한 포인터(back chain pointer라고 함.)와 다른 함수를 호출할 때를 대비한 리턴 어드레스 공간을 보유하고 있다. 이들 각각이 워드 사이즈 (4-byte) 값을 갖고 있으면서, 쉬운 로딩과 저장을 위해 16-byte로 정렬된다. (SPU는 16-byte로 정렬된 어드레스에서 로딩한다는 것을 기억하라. 나머지 공간은 레지스터를 저장하고 로컬 변수들을 저장하는데 사용된다. main의 스택은 최소 32 바이트일 것이고, factorial은 48 바이트이다. factorial은 저장할 로컬 변수를 갖고 있기 때문이다. 프로그램 내에서 이를 정량화 하여 보다 가독성 있는 코드를 만들려면, .equ 연산을 통해 이 값에 심볼을 준다. 이것은 어셈블러에게 주어진 심볼과 값을 동일시 하라는 명령이다. 스택 프레임 크기는 각각 MAIN_FRAME_SIZEFACT_FRAME_SIZE 심볼로 할당된다. LR_OFFSET는 리턴 어드레스의 스택 프레임에 대한 오프셋이다. LCL_NUM_VALUE는 로컬 변수 num의 스택 오프셋이다. 이들은 모두 코드의 메인 바디에서 보다 명확하게 스택 프레임 오프셋으로 액세스 하는데 사용된다.

코드 섹션에서, 위 글로벌 변수에 대한 어드레스를 정의했던 것과 같은 방식으로 함수의 어드레스를 정의한다. 이름 다음에 콜론을 배치하면 된다. 함수의 어드레스는 그 다음 명령어의 어드레스가 된다. .type을 사용하여 이 값이 함수로서 사용된다는 것을 링커에게 알리고, .global을 사용하여 이 심볼이 연결되는 현재 파일의 밖에서 참조될 수 있다는 것을 알려준다. main은 글로벌로 선언되어야 한다. 왜냐하면 이것은 프로그램에 대한 엔트리 포인트로서 사용되기 때문이다. 그런 다음, 실제 어셈블리 명령으로 간다.

factorial 함수를 검토하면서 프롤로그가 무엇을 수행하는지를 설명하겠다. 지금은 이것이 스택 프레임을 설정한다는 사실만 알면 된다.

첫 번째 명령어는 lqr $3, number이다. 이는 "load quadword relative"를 의미한다. "quadword" 부분은 약간 장황하다. quadword가 로딩하고 저장하는 것만 SPU에 허용된다. 이는 number 어드레스(현재 명령어에서 상대 어드레스로서 인코딩 됨)에 있는 값을 레지스터 3에 로딩한다. PPE 어셈블리 언어와는 달리, SPE 어셈블리 언어 레지스터들에는 달러 부호가 앞에 붙는다. 따라서 코드에서 레지스터를 보다 쉽게 찾을 수 있다. SPU에 있는 모든 레지스터들은 16-바이트이기 때문에, 첫 4 바이트에만 관련이 있더라도, 전체 16-바이트 quadword를 레지스터에 로딩한다.

레지스터 3에서 이 값을 사용하여 해야 되는 일은 이것의 팩토리얼을 계산하는 것이다. 따라서, 이것을 factorial 함수에 대한 첫 번째(유일한) 매개변수로서 전달한다. PPU ABI와 마찬가지로, SPU ABI는 레지스터를 사용하여 값을 함수로 전달한다. 레지스터 3에는 첫 번째 매개변수를, 레지스터 4에는 두 번째 매개변수를 갖고 있어야 한다. 따라서, 여러분이 레지스터 3으로 로딩했던 값은 이 함수의 경우 완벽한 부분에 이미 존재한다. 레지스터가 여러 값들을 갖고 있을 수 있지만(이 경우, 네 개의 320비트 값), 매개변수를 함수로 전달할 때, 각 매개변수 값은 고유의 레지스터로 전달된다.

그렇다면 레지스터는 어디에 사용되는가? 레지스터는 값을 계산하기 위해 프로세서가 사용하는 임시 스토리지이다. SPU는 128 개의 레지스터를 갖고 있기 때문에, 다른 아키텍처처럼 메모리로 로딩 및 저장할 필요 없이 임시 및 중간 값들을 저장할 수 있다. 따라서 프로그래밍이 더 쉽고, 실행도 더 빠르다. 레지스터가 사용되는 방법에 있어서 SPU에서는 어떤 차이도 없지만, ABI 표준에는 차이가 있다. 다음은 SPU 내에서 ABI가 각 레지스터를 사용하는 방법을 나타내는 테이블이다.

SPU ABI에서의 레지스터 사용

레지스터 범위유형목적
0Dedicated링크 레지스터
1Dedicated스택 포인터
2Volatile환경 포인터(이를 필요로 하는 언어에만 해당함).
3-79Volatile함수 인자, 리턴 값, 일반적인 사용.
80-127Non-volatile로컬 변수에 사용됨. 함수 호출 동안 보유됨.

링크 레지스터는 기본적으로 리턴 어드레스의 임시 스토리지에 사용된다. 스택 포인터는 현재 스택 프레임의 끝이 어디에 있는지를 알려준다. 환경 포인터는 대부분의 언어에서는 사용되지 않는다. volatile로 표시된 모든 레지스터들은 하나의 함수 내에서 자유롭게 변경될 수 있다. 하지만, 함수가 함수 호출을 할 때, Volatile 레지스터에 있는 모든 레지스터들이 오버라이트 될 것이다. 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-bit unsigned 값을 로딩하는데, 이는 PS3에서 로컬 스토어 어드레스에 대한 완벽한 사이즈이다. 마지막으로, 원래 숫자가 레지스터 4로 로딩된다. printf 함수는 brsl $lr, printf를 사용하여 호출된다. 하지만 printf 는 SPE에 대해서는 실행되지 않는다. SPE는 인풋과 아웃풋을 할 수 없기 때문이다. 실제로 SPE 프로세서를 중지하고, PPE에 신호를 보내는 stub 함수로 가고, PPE는 함수 호출을 수행한다. 이후에 컨트롤은 SPE로 리턴된다.

마지막으로 factorial 코드를 분석하겠지만, 기본적으로 스택 프레임에 대해서 살펴보기로 하자.

factorial 함수를 논하기 전에 스택 프레임의 레이아웃을 보도록 하자. 다음은 ABI에 따라 스택이 레이아웃 되는 방법을 나타낸 것이다.

ContainsSizeBeginning Stack Offset
Register Save AreaVaries (multiple of 16 bytes)Varies
Local Variable SpaceVaries (multiple of 16 bytes)Varies
Parameter ListVaries (multiple of 16 bytes)32($sp)
Link Register Save Area16 bytes16($sp)
Back Chain Pointer16 bytes0($sp)

Back Chain Pointer는 이전 스택 프레임의 Back Chain Pointer를 가리킨다. Link Register Save Area에는 현재 함수 보다는 호출되는 함수의 링크 레지스터 콘텐트가 포함된다. Parameter List는 이 함수가 다른 함수 호출로 보내는 매개변수에 대한 것이지, 고유의 매개변수에 대한 것은 아니다. 하지만, PPE와는 달리, 이는 매개변수의 수가 매개변수에 사용되는 레지스터의 수 보다 클 경우에만 사용된다. Local Variable Space는 함수에 대한 일반 스토리지 영역으로서 사용되고, Register Save Area은 함수가 사용하는 non-volatile 레지스터의 값을 저장하는데 사용된다.

따라서, 이 함수에서, 우리는 Back Chain Pointer, Link Register Save Area, 그리고 하나의 로컬 변수를 사용하고 있다. 프레임 크기는 16 * 3 = 48 바이트이다. 앞서 언급했듯이, LR_OFFSET은 스택의 끝에서 Link Register Save Area로의 오프셋이다. LCL_NUM_VALUE는 스택의 끝에서 로컬 변수 num으로의 오프셋이다.

프롤로그는 함수에 대한 스택 프레임을 설정한다. 프롤로그에서, 여러분이 해야 할 첫 번째 일은 링크 레지스터를 저장하는 것이다. 여러분은 자신의 스택 프레임을 아직 정의하지 않았으므로, 오프셋은 호출하는 함수의 스택 프레임의 끝부터이다. 링크 레지스터는 함수 고유의 스택 프레임이 아닌 호출하는 함수의 스택 프레임에 저장된다. 따라서, 스택 공간을 보유하기 전에 이를 저장하는 것이 좋다. 이는 D-Form 스토어 (D-Form은 명령 포맷이다.)를 사용하여 수행된다. Power Architecture용 어셈블리 언어, Part 2(SPU 포맷은 PPU 포맷과 매우 유사한 포맷을 따른다.)에서 일반적인 PPU 포맷에 대해 검토하라. 저장 명령에 대한 코드는 stqd $lr, LR_OFFSET($sp)이다. stqd는 "store quadword D-Form"을 의미한다. D-Form 명령은 첫 번째 피연산자로서 레지스터를 취한다. 이는 저장 또는 로딩되는 레지스터이고, 두 번째 피연산자로서 하나의 상수와 하나의 레지스터의 결합이다. 상수는 레지스터에 추가되어 로딩과 저장에 사용할 어드레스를 계산한다. 다른 유명한 포맷으로 X-Form(두 개의 레지스터들이 함께 추가됨) 또는 A-Form(상수 또는 constant relative offset address가 있음)이 있다. 따라서, 이 명령에서 $sp는 스택 포인터($1의 앨리어스)이다. LR_OFFSET($sp) 식은 LR_OFFSET$sp의 합을 계산하고 이것을 목표 어드레스로서 사용한다. 따라서, 이 명령은 (리턴 어드레스를 보유하고 잇는)링크 어드레스를 호출하는 함수의 스택 프레임의 올바른 위치에 저장한다.

그런 다음, 현재 스택 프레임 포인터는 여러분이 스택 프레임을 아직 만들지 않았더라도, 다음 스택 프레임에 대한 백(back) 포인터로서 저장된다. SPU는 PPU 같이 자동 store/update 명령이 없기 때문에, 백 포인터가 일관성이 있는지를 확인해야 하고, 스택 포인터를 옮기기 전에 백 포인터를 저장해야 한다. 마지막으로, 스택 포인터는 ai $sp, $sp, -FRAME_SIZE 명령을 사용하여 필요한 모든 스택 공간을 보유하기 위해 이동한다. ai는 "add immediate"을 의미하고, 이것은 immediate-mode 값을 레지스터에 추가하고, 이를 레지스터에 저장한다. 두 번째 피연산자에 있는 레지스터와 세 번째 피연산자에 있는 상수를 함께 추가하고 첫 번째 피연산자에 지정된 레지스터에 결과를 저장한다. 대부분의 명령어들은 비슷한 포맷을 따르고, 첫 번째 피연산자에 지정된 결과를 보유하고 있는 레지스터를 갖고 있다.

"add immediate" 명령은 벡터 연산이라는 점에 주목하라. SPU 레지스터들은 128-bit이지만, 이 예제의 값들은 32-bit 길이 정도이다. 레지스터는 로컬에서 다중 값들로 취급되는데, 이는 한꺼번에 연산이 된다. "add immediate" 명령어는 실제로 레지스터를 네 개의 개별 32-bit 값으로 취급하며, 각각 -FRAME_SIZE가 추가되며, 모두 목표 레지스터로 저장된다. SPU에서 권장되는 값 사이즈는 32-bit word이지만 byte, halfword, doubleword도 지원된다. 피연산자의 크기가 명령어에서 지정되지 않는다면 크기가 문제가 되지 않는다거나(논리적 명령어일 경우), 32-bit 값 크기를 사용하고 있다는 의미이다. 바이트는 명령어에 b가, halfword는 h, doubleword는 d가 들어있다. doubleword는 부동 소수점 명령어에서만 사용된다. (가끔 d는 doubleword가 아닌 D-Form을 나타내기도 한다.) 하지만 이 경우, 레지스터의 첫 번째 단어에 대해서만 생각하기로 한다. 다른 값들은 ABI에서 문제가 되지 않는다.

그런 다음, 첫 번째 매개변수를 stqd $3, LCL_NUM_VALUE($sp)를 가진 로컬 변수로 복사한다. 매개변수는 재귀 함수 호출에 대해 실행되고 여러분은 나중에 여기에 액세스 해야 하므로 이를 수행해야 한다.

그런 다음 immediate-mode를 실행하여 레지스터 3을 number 0과 비교하고, 그 결과를 ceqi $4, $3, 0으로 레지스터 4에 저장한다. PPU에서는, 조건 결과를 보유하는 특별한 레지스터가 있다. 하지만, SPU에서는, 결과는 범용 레지스터(이 경우 레지스터 4)에 저장된다. 이것은 벡터 프로세서라는 것을 기억하라. 여러분은 실제로 레지스터 3과 숫자 0을 비교하는 것이 아니다. 대신, 레지스터 3의 각 단어를 숫자 0과 비교하고 있다. 따라서, 여러분은 실제로 내 개의 응답을 갖게 된다. 결과는 다음과 같은 방법으로 저장된다. 단어의 조건이 true이고, 목표 단어의 모든 비트가 설정된다. 그 단어의 조건이 false 라면, 목표 단어의 모든 비트는 설정되지 않는다. 따라서, 이 명령의 경우, 네 개의 결과가 있고, 모두 1이거나 모두 0이 된다.

다음 명령어는 brnz $4, case_zero이다. brnz는 "branch relative if not zero"를 뜻한다. 레지스터 4는 이전 비교의 결과이기 때문에 이것은 ZERO와 NON-ZERO에 대한 이전 비교 결과를 검사한다. 결과 레지스터는 ZERO에 대한 이전 테스트가 TRUE라면 NON-ZERO가 될 것이다. 여러분이 ZERO에 대해 테스트 하기 때문에 이전 두 개의 명령어가 하나의 명령으로 합쳐질 수 있지만(brz $3, case_zero), 나는 이들을 두 개의 명령으로 분리하여 일반적인 경우에 작업을 비교하고 분기하는 방법을 더욱 잘 볼 수 있다.

일부 비교들이 true 또는 false의 결과를 가진다면 어떻게 될까? 여러분은 하나의 128-bit 값 보다는 32-bit 값을 다루기 때문에 다른 값들마다 다른 결과를 가질 수 있다. 따라서, 결과가 다르다면 분기하겠는가? 여러 SPU 명령어들은 레지스터 값의 단 하나만 처리한다. 이 경우, 사용되는 값은 레지스터의 선호하는 슬롯(preferred slot)에 있는 것이다. 64-bit 값의 경우, 그 레지스터의 첫 번째 반이다. 32-bit 값의 경우, 선호하는 슬롯은 그 레지스터의 첫 번째 단어이다. 8-bit의 경우, 선호하는 슬롯은 레지스터의 네 번째 바이트이다. 기본적으로 첫 번째 단어가 권장 단어이고, 다른 것은 가장 중요하지 않은 바이트 이거나 그 단어의 halfword이다. 조건 분기를 실행하고, 값을 함수로 전달하고, 함수에서 값을 리턴할 때, 선호하는 슬롯의 값이 중요하다. 이 경우, 함수로 전달된 값이 레지스터의 선호하는 슬롯에 있는 것으로 간주된다. .data 섹션에서 number의 정렬을 본다면 이것이 선호하는 슬롯으로 로딩될 것이라는 것도 알 수 있다. 따라서, 값이 레지스터의 선호하는 슬롯에 있는 한 분기는 정확하게 발생한다.

레지스터 3에서 작업하는 넘버가 0이 아니라고 가정해 보자. 여러분은 재귀 단계를 수행해야 한다는 것을 의미한다. 재귀 C 코드는 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"를 의미한다. 목표 어드레스는 상대 어드레스로서 인코딩 되고, 리턴 어드레스는 지정된 레지스터의 선호하는 슬롯에 저장되고, 컨트롤은 목표 어드레스로 가고, 이 경우 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 상의 정수 곱하기 명령은 다소 문제가 있고, 특히 signed multiplication (mpy 명령을 사용함.)이 그렇다. 문제는, 곱하기 명령의 결과가 피연산자의 두 배가 될 수 있다는 점이다. 두 개의 32-bit 값을 곱한 결과는 실제로 64-bit 값이 된다. 이렇게 된다면, 목표 레지스터는 소스 레지스터의 두 배가 되어야 한다. 이 문제를 해결하려면, 곱하기 명령은 모든 32-bit 값의 16-bit를 사용하여 결과가 전체 32-bit 레지스터에 맞춘다. 곱하기가 소스 레지스터를 32-bit 길이로 취급하지만, 이중 16-bit만 사용한다. 따라서, 32-bit 보다 길다면 값이 절단된다. 이것이 signed multiply라면, 부호 역시 바뀐다. 따라서 곱하기 연산을 성공적으로 실행하려면, 소스 값은 16-bit가 되어야 하고, 32-bit 레지스터에 저장되어야 한다. (나머지 32-bit에 대한 부호 확장일 경우 곱하기에는 문제가 안된다.) 이것은 팩토리얼 함수의 길이를 제한한다. 부동 소수점 곱하기는 이러한 문제가 없다.

이제 결과가 나왔고 레지스터 3에 저장되는데, 이 곳은 리턴 값을 위해 필요하다. 이제 남은 일은 이전 스택 프레임을 복원하고 리턴하는 일이다. ai $sp, $sp, FRAME_SIZE를 사용하여 스택 프레임 크기를 스택 포인터에 추가하여 스택 포인터를 이동해야 한다. 그런 다음, lqd $lr, LR_OFFSET($sp)을 사용하여 링크 레지스터를 복원한다. 마지막으로 bi $lr("branch indirect")는 링크 레지스터에 지정된 주소로 분기하고 그 함수에서 리턴한다.

기본적인 경우(함수의 매개변수가 zero일 경우)는 더 쉽다. factorial(0)의 결과는 1이기 때문에 il $3, 1을 사용하여 숫자 1을 레지스터 3에 로딩하면 된다. 그런 다음, 스택 프레임을 복원하고 리턴한다. 하지만, 기본 경우가 다른 함수를 호출하지 않으므로, 스택 프레임에서 링크 레지스터를 로딩할 필요는 없다. 값은 여전히 존재한다.

여기 까지가 함수 작동 방식이다. SPE에서 재귀 함수를 작성하는 것은 문제가 많다. SPE에는 스택 오버플로우 보호 장치가 없고 로컬 스토어는 작기 때문이다.




위로


결론

소셜 북마크

mar.gar.in mar.gar.in
digg Digg
del.icio.us del.icio.us
Slashdot Slashdot

PLAYSTATION 3의 Cell BE 프로세서 기반으로 어셈블리 언어 프로그래밍의 기본 개념을 설명했다. 다음 번에는 SPE와 PPE간 기본 통신 모드를 설명하도록 하겠다.



참고자료

교육

제품 및 기술 얻기

토론


필자소개

Jonathan Bartlett은 리눅스 어셈블리 언어를 사용한 프로그래밍 개요서인 Programming from the Ground Up 의 저자이다. New Medio의 책임 개발자이며, 웹, 비디오, 키오스크, 데스크탑 애플리케이션을 개발하고 있다.




기사에 대한 평가


보다 나은 서비스를 제공하기 위함이오니 잠시 짬을 내어 이 양식을 제출하여 주십시오.



아니오잘 모르겠음
 


 


12345
 



위로


Sony and all Sony-based trademarks are trademarks of Sony Corporation of America. Cell Broadband Engine is a trademark of Sony Computer Entertainment, Inc. 기타 회사, 제품, 및 서비스명은 다른 상표나 서비스 마크일 수 있습니다.

developerWorks 콘텐트를 다른 사이트에 전재하기:
developerWorks 콘텐트에 대한 저작권은 IBM에 있습니다. IBM의 서면 허가나 원본 저자의 허락이 없이는 전재를 금합니다. 저희 콘텐트를 전재하시려면 IBM developerWorks 담당자 에게 문의하십시오.
    IBM 소개 개인정보 보호정책 문의