지금까지 나온 모든 SIMD 연산은 _레인 단위_로 동작했어요.
결과의 레인 i에 있는 값은 입력의 레인 i를 대상으로 계산돼요.
하지만 값이 레인 사이를 이동하는 것이 필요하거나 바람직한 상황이 많아요. 예를 들어, 수행해야 할 계산에 맞게 레인의 순서가 뒤바뀌어 있을 수 있어요.
데이터를 여러 방식으로 레인 사이에서 옮기는 SIMD 명령어가 많아요.
셔플은 레지스터의 바이트나 레인의 순서를 다시 정렬하며, 각 목적지 레인을 어디에 있든 임의의 소스 레인에서 가져와요. 같은 레인을 서로 다른 목적지로 선택할 수도 있는데, 그래서 값을 브로드캐스트하는 데도 쓸 수 있어요.
셔플 명령어는 크기와 실행 도메인에 따라 다르게 동작해요.
pshufd 명령어는 소스의 32비트 레인 네 개를 목적지에 재배치해요.
선택은 8비트 즉시값으로 표현되는데, 목적지 레인마다 하나씩인 네 개의 2비트 필드로 읽어요. 각 필드는 소스 레인 네 개 중 어느 것을 그 목적지 레인에 복사할지 선택해요:
| 필드 | 레인 인덱스 |
|---|---|
00 |
0 |
01 |
1 |
10 |
2 |
11 |
3 |
즉시값에서 필드의 위치는 오른쪽에서 왼쪽으로 읽으며, 선택한 레인이 어디에 삽입되는지를 나타내요. 소스 레인은 두 번 이상 선택될 수 있는데, 이렇게 해서 하나의 레인을 레지스터 전체에 브로드캐스트해요:
; The bit fields are read right-to-left
pshufd xmm1, xmm0, 0b00_01_10_11 ; reverse: lane 0 takes source 0b11 (3), lane 1 takes source 0b10 (2), and so on
pshufd xmm3, xmm2, 0b00_00_00_00 ; broadcast source lane 0 into all four lanes
shuf + p + 크기 접미사(s 또는 d)라는 같은 일반 문법을 따르는 부동소수점 셔플 명령어가 두 개 있어요.
이들도 즉시값으로 레인을 선택해요.
shufps는 앞의 정수 셔플과 같은 2비트 필드 인코딩을 사용해요.
하지만 128비트 피연산자에는 64비트 레인이 두 개뿐이라서, shufpd는 1비트 필드 인코딩만 사용해요.
이 명령어들은 하나가 아니라 두 피연산자에서 레인을 가져온다는 점에서 정수 명령어와 달라요:
shufps xmm0, xmm1, 0b11_10_01_00 ; xmm0 = {xmm0[0], xmm0[1], xmm1[2], xmm1[3]}
shufps xmm2, xmm3, 0b00_00_00_00 ; xmm2 = {xmm2[0], xmm2[0], xmm3[0], xmm3[0]}
shufpd xmm4, xmm5, 0b0_1 ; xmm4 = {xmm4[1], xmm5[0]}
두 피연산자가 모두 결과에 기여하므로, 한 단계로 두 벡터를 인터리브하는 데 쓸 수 있어요. 소스와 목적지가 같은 레지스터라면, 모든 레인을 그 레지스터에서 가져와요.
pshufb는 가장 일반적인 셔플이에요.
pshufb와 pshufd는 크기 접미사만 다를 뿐 이름이 아주 비슷하지만, 수행하는 연산은 아주 달라요.
먼저, pshufd는 즉시값으로 레인 위치를 선택하는 반면, pshufb는 소스 피연산자의 제어 벡터를 사용해요.
이 제어 벡터는 xmm 레지스터이거나 16바이트 메모리 피연산자예요.
16개의 목적지 레인 각각에 대해, 제어 벡터의 하위 4비트가 0부터 15까지의 소스 바이트 인덱스를 알려줘요.
제어 벡터의 레인 i에 소스 바이트 인덱스 j가 들어 있으면, 소스의 j-th번째 바이트가 목적지의 i번째 레인으로 옮겨져요.
여기서 두 명령어의 두 번째 차이가 드러나요.
pshufd는 셔플할 레인을 다른 소스 피연산자에서 가져오는 반면, pshufb는 목적지에서 제자리로 셔플해요.
section .rodata
align 16
reverse: db 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0
section .text
fn:
pshufb xmm0, [rel reverse]
; xmm0[0] = xmm0[reverse[0]] (xmm0[15])
; xmm0[1] = xmm0[reverse[1]] (xmm0[14])
; ...
; in the end, the bytes in xmm0 are reversed
pshufb는 그보다 훨씬 유연해서 아무 레인이나 지울 수 있어요.
제어 벡터의 레인 i에서 최상위 비트가 설정되어 있으면, 목적지의 레인 i-th는 0으로 지워져요.
덕분에 pshufb는 한 명령어 안에서 임의의 바이트 순열이자 선택적 초기화 역할을 해요.
pshufb는 바이트 단위로 동작하므로, 인접한 레인을 묶어서 크기가 다른 레인을 셔플하는 데도 쓸 수 있어요.
예를 들어 dword를 셔플하는 데 쓸 수 있어요:
section .rodata
align 16
input: dd 13, 25, 37, 49 ; each number takes 4 bytes
reverse_dwords: db 12, 13, 14, 15, 8, 9, 10, 11, 4, 5, 6, 7, 0, 1, 2, 3 ; aligned too, since input spans 16 bytes
section .text
fn:
movdqa xmm0, [rel input] ; xmm0 = {13, 25, 37, 49}
pshufb xmm0, [rel reverse_dwords]
; xmm0 = {49, 37, 25, 13}
언팩 명령어는 두 피연산자에서 레인을 가져와 둘을 번갈아 가며 엮어요. 정수 변형과 부동소수점 변형이 있고, 둘은 대체로 같은 일반 문법 구조를 따르지만 두 가지 차이가 있어요:
l)에서 동작하는지 위쪽 절반(h)에서 동작하는지를 나타내는 l 또는 h 접미사가 있어요.bw나 qdq예요(16바이트의 크기 접미사는 movdqu에서처럼 dq예요).punpcklwd xmm0, xmm1 ; p + unpck + l + wd
; this interleaves the first 4 words of xmm0 and xmm1 into 4 dwords
; for each dword in the result, the first word is taken from xmm0 and the second, from xmm1
; xmm0 = {xmm0[0], xmm1[0], xmm0[1], xmm1[1], xmm0[2], xmm1[2], xmm0[3], xmm1[3]}
unpckhps xmm2, xmm3 ; unpck + h + p + s
; this interleaves the high 32-bit floats of xmm2 and xmm3
; xmm2 = {xmm2[2], xmm3[2], xmm2[3], xmm3[3]}
팩 명령어는 반대 방향으로 동작해서, 두 피연산자의 레인을 결합해 목적지의 절반 너비 레인으로 만들어요.
이 명령어들은 p 접두사를 붙이지 않아요.
그 점을 빼면, 문법은 이미 본 요소들을 조합한 형태예요:
pack.s 또는 u.s, 이전 개념에서 본 포화 연산에서처럼요.unpck와 반대 동작이에요).팩 연산은 좁히는 연산이라서 l이나 h 접미사가 필요 없어요:
packssdw xmm0, xmm1 ; pack + s (for signed) + s (for saturating) + dw (dword to word)
packuswb xmm2, xmm3 ; pack + u (for unsigned) + s (for saturating) + wb (word to byte)
이 명령어들은 포화 방식이에요. 즉, 출력값이 더 좁은 범위로 제한돼요. dword에서 워드로, 워드에서 바이트로 동작해요. qword에서 dword로 가는 변형은 없어요.
입력은 항상 부호 있음으로 해석된다는 점에 주의해요.
부호 있음이든 부호 없음이든, 그 타입은 _출력_에 대한 것이에요.
제한할 범위를 나타내요.
예를 들어 packsswb는 부호 있는 바이트의 범위, 즉 [-128, 127]로 제한해요.
결과의 아래쪽 레인은 목적지 피연산자에서, 위쪽 레인은 소스에서 와요:
packusdw xmm0, xmm1 ; 8 words, each clamped to 0..65535
; xmm0 = {xmm0[0], xmm0[1], xmm0[2], xmm0[3], xmm1[0], xmm1[1], xmm1[2], xmm1[3]}
이 명령어들에 대응하는 부동소수점 버전은 없어요.
지금까지는 movq/movd를 써서 SIMD 레지스터와 범용 레지스터 사이로 데이터를 옮겼어요.
이 명령어들은 SIMD 레지스터의 최하위 레인에만 쓰거나 그 레인에서만 읽을 수 있고, 쓸 때는 나머지 레인을 모두 지워요.
첫 번째 레인뿐 아니라 아무 레인에나 같은 일을 하면서 다른 레인은 건드리지 않는 명령어도 있어요:
둘 다 정수 문법을 따라요: p + insr/extr + 크기 접미사(b, w, d, q).
두 경우 모두 범용 레지스터는 보통 32비트예요.
pinsrq와 pextrq만 64비트 피연산자가 필요해요.
메모리 피연산자는 항상 연산의 크기와 같아요. pinsrb/pextrb는 8비트, pinsrw/pextrw는 16비트, 이런 식이에요.
pinsrb xmm0, eax, 5 ; replace byte 5 of xmm0 with the low byte of eax
pextrb byte [rdx], xmm0, 5 ; copy byte 5 of xmm0 into the memory location indicated by rdx
이 명령어들은 movd와 movq처럼 그냥 원시 바이트를 옮긴다는 점에 주의해요.
따라서 메모리나 범용 레지스터에 저장된 부동소수점 값에도 쓸 수 있어요.
소프트웨어 이미지 파이프라인의 내부 루프를 작성해요. 이 단계는 텍스처를 준비하고 레이어를 합성해 화면에 보내기 전에 처리하는 부분이에요. 파이프라인은 픽셀을 한 번에 한 블록씩 처리하면서, 블록 전체에 같은 연산을 적용해요.
픽셀은 1바이트 채널 네 개로 이루어져 있어요. 순서대로 빨강, 초록, 파랑, 알파이며, 이를 RGBA라고 불러요.
블록 하나는 픽셀 4개이므로 총 16바이트예요.
과제는 다섯 개예요.
연산에 필요한 값은 메모리 주소를 통해 받고, 결과는 결과 주소를 통해 써요. 이 연습 문제의 모든 메모리 주소는 16바이트로 정렬되어 있어요.
이 연습 문제의 계산은 스칼라 연산이 아니라 SIMD 명령어로 수행해야 해요.
텍스처는 RGBA로 저장되지만, 이 파이프라인이 그려 넣는 프레임버퍼는 각 픽셀을 BGRA 순서로 기대해요. 즉 빨강과 파랑 채널은 서로 바뀌고, 초록과 알파 채널은 그대로 있어요.
이미지는 블록의 연속으로 들어오고, 모든 블록이 같은 방식으로 변환돼요.
이미지 전체를 RGBA에서 BGRA로, 한 번에 한 블록씩 변환하는 to_display_order 함수를 구현해요.
채널 순서를 바꾸는 제어 마스크는 메모리에 패킹된 상수로 정의하고, 모든 블록에서 재사용해야 해요.
이 함수는 다음 순서로 인자를 받아요:
result: 변환된 블록이 기록되는 버퍼의 메모리 주소이며, 블록당 16바이트예요.pixels: 원본 블록의 메모리 주소이며, 블록당 픽셀 4개이고 각 픽셀은 RGBA 순서로 4바이트예요.block_count: 블록의 개수이며, 항상 0보다 커요.pixels = {200, 64, 32, 255, 10, 20, 30, 40, 0, 0, 0, 255, 12, 34, 56, 78,
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} // 2 blocks
block_count = 2
result = {32, 64, 200, 255, 30, 20, 10, 40, 0, 0, 0, 255, 56, 34, 12, 78,
3, 2, 1, 4, 7, 6, 5, 8, 11, 10, 9, 12, 15, 14, 13, 16}
이 함수는 반환값이 없어요.
영역을 지우거나 단색 영역을 칠할 때, 파이프라인은 영역의 모든 픽셀에 하나의 색을 써요.
block_count개의 블록으로 이루어진 영역을 같은 색의 복사본으로 채우는 fill_region 함수를 구현해요.
이 함수는 다음 순서로 인자를 받아요:
result: 채워진 블록이 기록되는 버퍼의 메모리 주소이며, 블록당 16바이트예요.color: 픽셀 하나의 메모리 주소이며, RGBA 순서로 4바이트예요.block_count: 채울 블록의 개수이며, 항상 0보다 커요.color = {18, 52, 86, 120}
block_count = 2
result = {18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120,
18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120}
이 함수는 반환값이 없어요.
단일 채널 레이어 두 개를 각 샘플을 서로 끼워 넣으면서 하나의 버퍼로 합쳐야 해요. 결과는 첫 번째 레이어의 샘플 하나, 그다음 두 번째 레이어의 샘플 하나가 번갈아 나타나요.
각각 16개인 샘플 행 두 개를 32개 샘플의 행 하나로 서로 끼워 넣는 weave_scanlines 함수를 구현해요.
이 함수는 다음 순서로 인자를 받아요:
result: 서로 끼워 넣은 32개 샘플이 기록되는 버퍼의 메모리 주소예요.first: 첫 번째 행의 메모리 주소이며, 샘플 16개이고 각각 8비트 값이에요.second: 두 번째 행의 메모리 주소이며, 샘플 16개이고 각각 8비트 값이에요.first = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
second = {100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115}
result = {0, 100, 1, 101, 2, 102, 3, 103, 4, 104, 5, 105, 6, 106, 7, 107,
8, 108, 9, 109, 10, 110, 11, 111, 12, 112, 13, 113, 14, 114, 15, 115}
이 함수는 반환값이 없어요.
밝기 조정 단계는 각 샘플을 16비트 작업 정밀도로 스케일링해요. 그래서 너무 밝은 샘플은 255를 넘을 수 있고, 차이는 0 아래로 내려갈 수 있어요.
마지막 단계는 이 작업 값들을 다시 화면 표시용 8비트 샘플로 좁혀요. 이때 0보다 작은 값은 0으로, 255보다 큰 값은 255로 클램프해요.
8개씩인 작업 값 두 묶음을 순서대로 16개 샘플의 행 하나로 좁히는 pack_samples 함수를 구현해요.
이 함수는 다음 순서로 인자를 받아요:
result: 클램프된 샘플 16개가 기록되는 버퍼의 메모리 주소이며, 각각 8비트 값이에요.first: 처음 8개 작업 값의 메모리 주소이며, 각각 부호 있는 16비트 정수예요.second: 그다음 8개 작업 값의 메모리 주소이며, 각각 부호 있는 16비트 정수예요.first = {300, -5, 128, 255, 0, 400, 64, 200}
second = {255, 256, -1, 100, 50, 1000, 7, 0}
result = {255, 0, 128, 255, 0, 255, 64, 200, 255, 255, 0, 100, 50, 255, 7, 0}
이 함수는 반환값이 없어요.
텍스처와 정점 데이터는 흔히 서로 끼워진 채로 들어와요. 각 점의 x와 y가 함께 묶여 있죠.
그런데 효율적으로 처리하려면 이들을 따로 분리해야 할 때가 많아요. 모든 x 값은 한 벡터에, 모든 y 값은 다른 벡터에 담는 거예요.
서로 끼워진 (x, y) 점 네 개를 x 좌표 벡터와 y 좌표 벡터로 분리하는 split_coordinates 함수를 구현해요.
이 함수는 다음 순서로 인자를 받아요:
xs: x 좌표 4개가 기록되는 버퍼의 메모리 주소이며, 32비트 부동소수점 수 4개예요.ys: y 좌표 4개가 기록되는 버퍼의 메모리 주소이며, 32비트 부동소수점 수 4개예요.first: 처음 두 점의 메모리 주소이며, {x0, y0, x1, y1} 형태의 32비트 부동소수점 수 4개예요.second: 그다음 두 점의 메모리 주소이며, {x2, y2, x3, y3} 형태의 32비트 부동소수점 수 4개예요.first = {0.0, 0.5, 1.0, 1.5} // {x0, y0, x1, y1}
second = {2.0, 2.5, 3.0, 3.5} // {x2, y2, x3, y3}
xs = {0.0, 1.0, 2.0, 3.0} // {x0, x1, x2, x3}
ys = {0.5, 1.5, 2.5, 3.5} // {y0, y1, y2, y3}
이 함수는 반환값이 없어요.
Exercism에 가입하고 x86-64 Assembly 트랙을 개념 22개연습 문제 130개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.