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

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

Cell BE 프로세서의 고성능 애플리케이션 프로그래밍, Part 3: Meet the synergistic processing unit (한글)

Sony PLAYSTATION 3의 Synergistic Processing Elements(SPE) 프로그래밍

developerWorks
문서 옵션

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

영어원문

영어원문


제안 및 의견
피드백

난이도 : 중급

Jonathan Bartlett, Director of Technology, New Medio

2007 년 6 월 26 일

Cell Broadband Engine™ (Cell BE) 프로세서의 Synergistic Processing Elements(SPE)를 연구하고 이들이 최하위 레벨에서 어떻게 작동하는지를 배워봅시다. SPE의 스토리지 정렬 문제와 통신 장치를 설명합니다.

비 정렬(Non-aligned) 로드 및 스토어

소셜 북마크

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

Synergistic Processing Unit (SPU)이 스칼라가 아닌 벡터 프로세싱에 초점을 맞추고 있으므로, 16-byte 바운더리에 정렬된 로컬 스토어 위치에서 한번에 16-byte만 로딩 및 저장할 수 있다. 따라서, 메모리 위치 12에서는 한 단어도 로딩할 수 없다. 그 단어를 로딩하려면 메모리 위치 0에서 quadword를 로딩하고 비트를 이동하여 원하는 값이 선호하는 슬롯에 있도록 한다. 원래의 quadword가 로딩되고, 올바른 값이 quadword의 올바른 위치에 삽입되어야 하고, 결과가 저장되어야 한다. 이러한 문제 때문에 모든 데이터를 16-byte로 정렬하여 저장하는 것이 좋다. 16-byte 바운더리를 교차하는 값을 로딩하는 것은 훨씬 더 어렵다. 이것을 두 개의 레지스터에 로딩해야 하고, 이들을 이동한 다음, 마스크(mask) 및 결합해야 하기 때문이다. 이와 같은 값을 저장하는 것은 훨씬 더 어렵다. 16-byte 바운더리를 교차하는 값을 절대로 사용하지 않는 것이 최선의 방법이다.

16-byte 바운더리로 정렬되지 않은 데이터를 사용할 수도 있지만, 필자가 이 글에서 설명할 로딩 및 저장 기술은 데이터가 자연스럽게 정렬되어 16-byte 바운더리를 교차하지 않도록 하는 것이다. 다시 말해서, 단어들(words)은 4-byte로 정렬 될 것이고, halfword는 2-byte로 정렬되고, 바이트는 전혀 정렬될 필요가 없다.

비 정렬 로드를 수행하는 데에는 두 개 또는 세 개의 명령어가 필요한데 데이터의 크기에 따라 다르다. 단일 값을 로딩하고 있다면, 이것이 선호하는 레지스터에 두고 싶을 것이다. 첫 번째 명령어는 로딩을 수행하고, 두 번째 명령어는 값을 회전하여 등록된 주소가 레지스터의 처음 부분에 있도록 한다. 데이터가 단어 보다 작으면, 시작 부분에서 선호하는 슬롯으로 이동해야 한다. (한 단어 또는 두 단어(doubleword)라면, 레지스터의 시작은 선호하는 슬롯이 된다.) 바이트 로드용 코드는 레지스터 3의 슬롯에 주소를 취하고, 이를 사용하여 바이트를 레지스터 4의 슬롯으로 로딩한다.


Listing 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-byte 바운더리에서만 로딩한다. 따라서, 로딩하는 동안 네 개의 비트는 무시하고 메모리에서 정렬된 quadword만 로딩한다. 따라서, 임의의 주소의 경우, 로딩 된 quadword에 우리가 원했던 값이 어디에 있는지를 알 수가 없다. "바이트 별로 quadword를 (왼쪽으로) 회전하라"는 의미의 rotqby 명령어는 로딩했던 주소를 사용하여 레지스터를 얼마만큼 회전할 수 있는지를 나타낸다. 이것은 레지스터에서 네 개의 중요한 비트만 사용하여 얼마만큼 회전할 것인지를 결정한다. 레지스터의 처음에 지정된 주소로 옮기기 위해 남겨진 바이트의 수가 언제나 있다. 마지막으로, 바이트의 경우, 선호하는 슬롯이 레지스터의 처음에 없지만, 세 개의 바이트는 오른쪽에 있다. 따라서, rotqbyi 명령어는 immediate-mode 값을 사용하여 이동한다. Word- 및 doubleword-사이즈 전송은 마지막 명령어는 필요로 하지 않는다. 이들의 선호하는 슬롯은 레지스터의 처음 부분에 있기 때문이다. 이것의 끝에, 레지스터 4는 마지막 값을 갖고 있고, 선호하는 슬롯으로 이동한 바이트가 포함된다.

저장하기는 훨씬 더 어렵다. 레지스터 $4의 선호하는 슬롯에 레지스터 $3에 의해 지정된 주소를 저장하는 코드는 다음과 같다.


Listing 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는 quadword 정렬 주소에 한 번에 하나의 quadword만 로딩 및 저장한다는 사실을 기억하라. 따라서, 단 하나의 바이트를 저장하려면, 이를 비 정렬 주소에서 직접 수행하려면, 잘못된 위치로 가서 quadword에 나머지 바이트를 저장하게 된다. 이를 피하려면, 메모리에서 quadword를 먼저 로딩하고, 값을 quadword의 올바른 바이트에 삽입하고, 이를 다시 저장한다. 어려움 부분은 이것을 주소에만 기반하여 올바른 위치에 삽입하는 것이다. 다행히도, 두 개의 명령어 cbd ("generate control for byte insertion")와 shufb ("shuffle bytes")가 도움이 된다. cbd 명령어는 주소를 취하고 shufb에 의해 사용될 수 있는 컨트롤 단어를 생성하여 그 주소에 대한 quadword에 올바른 위치에 바이트를 삽입한다. cbd $6, 0($3)은 레지스터 3에 있는 수소를 사용하여 컨트롤 quadword를 만든 다음, 이를 레지스터 6에 저장한다. shufb $7, $4, $5, $6 명령어는 레지스터 6에 컨트롤 quadword를 사용하여 새로운 값을 레지스터 7에 생성한다. 이것은 메모리(현재 레지스터 5에 있음)에 있었던 원래의 quadword와 선호하는 슬롯에 있는 레지스터 4에서 온 바이트로 구성되고 결과를 레지스터 7에 저장한다. 바이트가 뒤섞이면 값은 메모리에 저장된다.

이 기술을 설명하기 위해 ASCII 문자의 조소를 취하고, 이를 로딩하여 대문자로 변환하고 다시 저장하는 함수를 작성할 것이다. convert_to_upper 함수를 main 함수의 개별 파일에 놓고 또 다른 프로그램에 이를 재사용 할 수 있도록 한다. 다음은 main 함수용 코드이다. (convert_main.s로서 저장한다.)


Listing 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):


Listing 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 함수는 전과 다름없이 수행되기 때문에 이곳에서는 다시 설명하지 않겠다. 하지만, 이 문자(letter)의 주소를 문자 자체가 아닌 convert_to_upper로 전달한다.

convert_to_upper 함수는 임의의 문자 주소를 취하고, 이를 대문자로 변환한 다음, 이를 다시 저장하고 어떤 것도 리턴하지 않는다. 또 다른 함수를 호출하지 않기 때문에 스택 프레임이 필요하지 않다.

이 함수가 하는 첫 번째 일은 비 정렬 로딩을 레지스터 4에 저장하는 것이다. 그리고 나서, 바이트가 a부터 z까지의 범위에 있는지를 확인한다. 'a' - 1 보다 큰지를 비교하고 'z.' 보다 큰지를 확인한다. 필자는 "less than" 비교를 수행하지 않았다. SPU에서는 이것이 불가능하기 때문이다! SPU는 "greater than"과 "equal to" 비교만 가능하다. 따라서, "less than or equal to" 비교를 하려면 "greater than" 비교를 한 다음 여기에 "not"을 수행한다. 이는 같은 레지스터가 되는 두 소스 인자에 nand 명령어를 사용한다. and 명령어를 사용하여 비교한 것을 결합한다. (모든 논리 명령어를 xor을 사용하여 하나로 결합할 수 있지만 이 코드는 명확하지 않다.) 마지막으로, 브랜치 명령어는 halfword나 단어(word) 값에 대해서만 연산을 수행하기 때문에 레지스터의 관련 없는 부분들은 가린다. (필자는 완전한 단어를 사용했기 때문에 그럴 필요가 없었다.)

레지스터 8의 선호 슬롯에 있는 비트가 모두 false로 설정되었다면 이 함수의 끝까지 건너뛰도록 한다. 이들이 true로 설정되었다면 변환을 수행한다. SPU상의 유일한 바이트 지향 함수는 absdb("absolute difference of bytes")이며, 이것은 두 개의 피연산자들 간 차이에 대한 절대 값을 제공한다. 소문자와 대문자 값 사이의 차이와 결합하여 이를 사용하여 변환을 수행한다. 마지막으로 비 정렬 스토어를 수행한다. 함수를 호출하거나 로컬 스토리지를 사용하지 않았기 때문에 스택 프레임이 필요하지 않았다. 따라서 링크 레지스터를 통해서 종료할 수 있다.




위로


PPE와 통신하기

지금까지, SPE 전용 프로그램에 초점을 맞추었다. 이제는 PPE 프로그램에 대해 살펴볼 것이다. PPE와 SPE가 통신하도록 하는 방법을 알아야 한다.

채널과 MFC

SPE는 로컬 스토어(local store)라고 하는 프로세서의 메인 메모리에서 분리된 메모리를 갖고 있다. SPE는 메인 메모리를 직접 읽을 수 없지만, DMA 명령어를 사용하여 로컬 스토어와 메인 메모리를 메모리 플로우 컨트롤러(memory flow controller) 또는 MFC라고 하는 단위로 반입 및 반출해야 한다. 로컬 스토어 어드레스 공간은 32비트로 제한되지만, 보통 이보다 훨씬 작다. (예를 들어, Sony® PLAYSTATION® 3의 경우 18비트 밖에 안된다.) 그 이유는 SPE 코드에 의한 메모리 액세스가 결정적(deterministic)이기 때문이다. 메인 메모리는 스와핑, 이동, 캐시, 비 캐시, 메모리 매핑이 될 수 있다. 따라서, 특정 메모리 액세스에 필요한 시간은 완벽하게는 알려져 있지 않다. (메모리가 스와핑 되면 이것이 얼마나 오래 걸리는지 어느 누구도 알 수 없다.) SPE 메모리를 로컬 스토어로 분리함으로써, SPE는 액세스 하는 메모리에 대한 결정적 액세스 시간을 가질 수 있고, MFC를 스케줄링 하여 필요할 때 메인 메모리에서 데이터를 비동기식으로 이동할 수 있다. SPE의 로컬 스토어 내의 어드레스를 로컬 스토어 어드레스(LSA)라고 하고, 메인 메모리 내의 어드레스를 effective addresses(EA)라고 한다. 이는 메모리 플로우 컨트롤러의 DMA 장치를 사용하는 방법을 배울 때 중요하다.

SPE는 채널(channel)을 사용하여 외부 세계와 통신한다. 한 채널은 특별한 명령어를 사용하여 쓰기 또는 읽을 수 있는(일방향) 32-bit 영역이다. 채널은 깊이 또는 채널 카운트(channel count)를 가질 수 있다. 채널 카운트는 읽히기를 기다리는 데이터의 양(읽기 채널) 또는 쓰일 수 있는 데이터의 양(쓰기 채널)이다. 채널은 모든 SPE 인풋과 아웃풋에 사용된다. 이들은 DMA 명령어를 메모리 플로우 컨트롤러에 실행하고, SPE 이벤트를 처리하고, 메시지를 PPE에 읽고 쓰는데 사용된다. 필자가 보여 줄 다음 프로그램은 MFC와 채널 인터페이스를 활용하여 PPE에 의해 지정된 데이터에 문자 변환을 수행한다.

SPE 태스크 생성 및 실행하기

지금까지, main 함수는 어떤 매개변수도 사용하지 않았다. 하지만, PPE 프로그램에서 실행될 때, 64-bit 매개변수들(레지스터 3의 SPE 태스크 식별자, 레지스터 4의 애플리케이션 매개변수에 대한 포인터, 레지스터 5의 런타임 환경 정보에 대한 포인터)를 받는다. 애플리케이션과 환경 포인터에 의해 지목된 영역의 콘텐트는 실제로는 사용자 정의이다. 하지만, 이들은 SPE의 로컬 스토어가 아닌, 애플리케이션의 메인 스토리지(effective address)에 있는 메모리를 가리킨다. 따라서, 이들은 직접 액세스 될 수 없고, 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 쓰레드 그룹이다. 0으로 설정될 수 있다.
  • spe_program_handle
    SPE 프로그램에 대한 데이터를 보유하고 있는 구조에 대한 포인터이다. 이 데이터는 SPU 애플리케이션을 PPU 실행 파일 내에 삽입함으로써 자동으로 정의된다. SPU 애플리케이션을 포함하고 있는 라이브러리에 dlopen()/dlsym()을 사용하거나 SPU 애플리케이션을 직접 로딩하기 위해 spe_open_image()를 사용한다.
  • argp
    프로그램 초기화를 위한 애플리케이션 스팩 데이터에 대한 포인터이다. 사용되지 않는다면 NULL로 설정한다.
  • envp
    프로그램용 환경 데이터에 대한 포인터이다. 사용되지 않는다면 NULL로 설정한다.
  • mask
    프로세서 유사성 마스크이다. -1로 설정하여 프로세서를 가용 SPE로 할당한다. 그렇지 않으면, 각각의 가용 프로세서마다 bitmask를 포함한다. 1은 프로세서가 사용되어야 한다는 것을, 0은 사용되어서는 안된다는 것을 의미한다. 대부분의 애플리케이션들은 이를 -1로 설정한다.
  • flags
    SPE가 설정되는 방법을 수정하는 비트 플래그 세트이다. 이들 모두 이 글의 범위를 벗어난다.

DMA를 사용한 PPE/SPE 프로그램

DMA 통신의 예로, PPE가 스트링을 취하고, 스트링을 통해 복사하는 SPE 프로그램을 호출하고, 이를 대문자로 변환하고, 다시 메인 스토리지에 복사하는 프로그램을 작성할 것이다. 모든 데이터 전송은 SPE 채널을 통해 제어되는 MFC의 DMA 장치들을 사용할 것이다.

메인 SPE 프로그램은 메인 메모리에 스트링의 크기와 포인터를 포함하고 있는 struct에 대한 유효 주소(effective address) 포인터를 받는다. 그리고 나서, 이것을 버퍼로 복사하고, 변환을 수행하고, 다시 복사한다. 다음은 SPE 코드이다. (convert_dma_main.s):


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


Listing 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 Linkable이라는 것을 만들어 내는데, 이는 SPE에 대한 객체 파일이 PPE 애플리케이션에 삽입되고 필요할 때 로딩될 수 있도록 한다.

다음은 SPU 코드를 사용하는 PPU 코드이다. (ppu_dma_main.c):


Listing 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_handlespe_convert_csf.o에 정의된다. 변수의 이름은 embedspu 명령어의 명령행에 설정되었다. 이 변수는 프로그램 코드에 대한 핸들이고, SPE 태스크를 생성하는데 사용될 것이다.

다음에는, SPE 프로그램에 대한 매개변수로서 사용될 구조를 정의한다. 스트링의 길이와 스트링에 대한 포인터가 필요하다. 이 모든 것은 quadword 정렬이 되어야 한다. 이것을 메인 프로그램에 복사하여 DMA 트랜스퍼와 함께 이 값을 사용해야 한다. 여러분이 사용했던 포인터는 포인터 보다는 unsigned long long을 선언한다. 이는 어드레스 전송이 32-bit 모드나 64-bit 모드에서 컴파일 되는 것과 같은 방식으로 저장되기 때문이다. 32-bit 모드에서 컴파일 될 경우, 포인터는 이 구조에서 다르게 정렬된다. 또한 memalign 함수와 strcpy를 사용하여 데이터를 해당 정렬 영역으로 복사한다. 여러분이 계속해서 "bus error"를 받는다면 아마도 16바이트 정렬이 아니거나 다중 16-byte가 아닌 DMA 트랜스퍼를 수행하고 있기 때문이다.

메인 프로그램에서, 변수를 선언한다. DMA를 사용하여 복사될 선언된 모든 변수들은 quadword 바운더리에 정렬되고 quadword의 배수이다. DMA 트랜스퍼는, 몇 가지 예외는 있지만, 소스와 목적지 어드레스 모두에서 quadword 정렬이 되어야 한다. (소스와 목적지에서 128-바이트로 정렬된다면 이 프로그램은 더 나은 성능을 보인다.) 다음에는 SPE 태스크는 spe_create_thread를 사용하여 생성되면서, 매개변수 구조로 전달된다. 이제 여러분은 SPE 태스크가 spe_wait을 사용하여 완료되기를 기다린 다음, 마지막 값을 프린트 한다. 여러분도 생각했겠지만, 이 프로그램에서 가장 재미있는 부분은 모든 DMA 전송을 포함하여, SPE에서 발생한다. DMA 전송은 PPE 보다는 SPE에 의해 수행된다. 이들은 PPE 보다 훨씬 더 많은 데이터를 처리하고 훨씬 역동적인 DMA 연산을 수행하기 때문이다.

메인 프로그램으로 들어가기에 앞서, DMA 유틸리티 함수를 살펴보도록 하겠다. 첫 번째 함수는 perform_dma이고, 이것은 DMA 명령어를 실행한다. Cell BE Handbook에서는 450-456 페이지에 걸쳐 DMA 트랜스퍼를 수행하는데 필요한 채널 연산을 정의한다. (참고자료) 이 함수가 수행하는 첫 번째 일은 레지스터 4의 64-bit 유효 주소를 두 개의 32-bit 컴포넌트(high-와 low-order component)로 변환하는 것이다. (채널은 32-bit이다.) 채널은 레지스터의 선호 단어 사이즈 슬롯을 사용하여 작성되기 때문에, 64-bit 어드레스는 선호 슬롯에 High Order 비트를 이미 갖고 있다. 따라서, 콘텐트를 네 바이트씩 왼쪽으로 새로운 레지스터로 이동하여 선호 슬롯에 Low Order 비트를 얻는다. 그런 다음, 논리적 스토어 주소를 작성하고, 유효 주소의 High Order 비트를, 유효 주소의 Low Order 비트, 전송의 크기, DMA 명령어의 태그, wrch 명령어를 사용하여 해당 채널에 대한 명령어를 작성한다. 명령어가 작성되면 DMA 요청은 MFC에 대기하게 된다. "태그"는 하나 또는 여러 개의 DMA 명령어들로 할당될 수 있는 숫자이다. 같은 태그로 실행된 모든 DMA 명령어들은 하나의 그룹으로 간주되고, 상태 업데이트와 시퀀싱 연산은 전체적으로 그룹에 적용된다. 이 애플리케이션에서 여러분은 한 번에 하나의 DMA 명령어만 실행할 수 있기 때문에, 모든 연산들은 DMA 태그로서 0을 사용한다. DMA 명령어는 MFC_GET_CMD 또는 MFC_PUT_CMD가 되어야 한다. 다른 것들도 있지만, 여기에서는 신경 쓰지 않겠다. MFC 명령어는, 이것이 실제로 명령어를 실행하는지 여부와 상관 없이 SPE의 관점에서 수행된다. 따라서 MFC_GET_CMD는 데이터를 메인 메모리에서 로컬 스토어로 이동하고, MFC_PUT_CMD는 다른 방향으로 간다.

DMA 명령어는 비동기식이기 때문에, 하나가 완료될 때까지 기다리는 것이 좋다. wait_for_dma_completion 함수는 이를 정확히 수행한다. 태그를 유일한 매개변수로서 취하고, 태그 마스크로 변환하고, DMA 상태를 요청한 다음, 상태를 읽는다. 그렇다면 어떻게 DMA 연산이 끝나는 것을 기다리는 것일까? 2의 값을 가진 $MFC_WrTagUpdate 채널에 작성할 때 $MFC_RdTagStat은 연산이 완료될 때까지 값을 가질 수 없다. 따라서, rdch를 사용하여 채널 읽기를 시도하면 상태를 사용할 수 있을 때까지 차단된다.

이제 실제 프로그램으로 가보자. SPE 프로그램이 수행하는 첫 번째 일은 애플리케이션의 매개변수 데이터용 공간을 보유하는 것이다. 이것 역시 quadword 바운더리로 정렬된다. (어셈블리 언어에서 .align 4는 C의 __attribute__((aligned(16)))과 같은 작동을 한다. 2^4 = 16이기 때문이다.) .octa는 quadword 값을 보유한다. (mnemonic은 16 비트 시절의 유산이다.) 전체 구조의 크기에 CONVERSION_STRUCT_SIZE 상수를 정의한다.

다음에는 .bss 섹션으로 가보자. 이것은 실행 파일 자체에 값을 포함하지 않는다는 것을 제외하고는 .data 섹션과 비슷하고, 여기에 얼마나 많은 공간이 보유되어야 하는지를 기록하고 있다. 이 섹션은 초기화 되지 않은 데이터를 위한 곳이다. .lcomm conversion_buffer, 16384는 16K 공간을 보유하고 있고 conversion_buffer 심볼에 정의된 시작 주소가 있다. 이것은 MFC DMA 트랜스퍼의 최대 크기이기 때문에 16K를 보유하도록 정의된다. 따라서, 어떤 스트링이라도 이것 보다 길면, PPE는 프로그램을 여러 번 호출해야 한다. (더 나은 프로그램은 요청을 SPE 측의 청크로 나눈다.)

main 함수는 이 프로그램의 주요한 부분이다. 스택 프로그램을 설정하는 것으로 시작한다. 그리고 나서, 프로그램의 메인 컨트롤에 사용될 세 개의 비 휘발성 레지스터를 저장한다. 그런 다음, DMA 트랜스퍼를 수행하여 PPE의 매개변수 구조에 복사한다. 이 함수에 대한 첫 번째 매개변수는 PPE에서 전달되었던 64-bit 주소이다. 그리고 나서, 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을 로딩하고 함수 에필로그를 수행하여 스택 프레임을 복원하고 리턴한다.




위로


메일박스를 사용한 SPE/PPE 통신

DMA 전송이 SPE와 PPE간 많은 데이터를 이동하는데 뛰어난 방법이지만, 적은 데이터 전송에 사용될 수 있는 것 중에 메일박스(mailbox)가 있다. SPE의 경우, 이것은 32-bit 값을 PPE에 작성하는 채널(읽기 채널과 쓰기 채널) 세트이다.

개념을 설명하기 위해, 메일박스에 서명되지 않은 정수를 기다리고 그 숫자의 제곱을 다시 작성하는 SPE 서버를 작성할 것이다. 다음은 코드이다. (square_server.s):


Listing 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):


Listing 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 방식을 결합할 때 생긴다. 예를 들어, 메일박스에 버퍼 주소를 리스닝 하고 그 주소를 사용하여 DMA를 통해 데이터가 처리되도록 하는 SPE 태스크가 생긴다.




위로


결론

지금까지, 본 시리즈에서는 리눅스® 기반 PLAYSTATION 3의 Cell BE 프로세서에서 어셈블리 언어 프로그래밍의 개념을 설명했다. 기본 아키텍처, SPU 어셈블리 언어의 신택스, SPE와 PPE 간 기본 통신 모드를 설명했다. 다음 글에서는 Cell BE 프로세서 SPE에서 얼마만큼의 성능을 이끌어 낼 수 있는지를 설명할 것이다. C를 이용한 SPE 프로그래밍 방법을 안다면 도움이 될 것이다.



참고자료



필자소개

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 소개 개인정보 보호정책 문의