스칼라 코드는 어떤 조건에 따라 분기하기 위해 여러 명령어가 설정하는 플래그에 의존해요. 반면 패킹된 값은 하나가 아니라 여러 값을 병렬로 나타내요. 하나의 조건이 어떤 레인에서는 거짓이 되고 다른 레인에서는 참이 될 수 있어요.
그래서 SIMD 코드는 기본적으로 분기가 없어요.
플래그에 의존하는 대신, 패킹된 비교는 보통 목적지 피연산자에 마스크를 만들어요.
각 레인마다, 비교 결과가 참이면 레인 전체를 1로 채우고 거짓이면 0으로 채워요.
부호 있는 정수로 읽으면 참인 레인은 -1이고 거짓인 레인은 0이에요.
이 마스크는 그다음 비트 연산과 조합해서 특정 레인만 걸러내는 데 쓸 수 있어요.
스칼라 cmp는 여러 플래그를 한꺼번에 설정하는 데 쓰인다는 점에서 범용적이에요.
그러면 다른 명령어가 그 플래그를 받아서 분기하거나 계산을 수행할 수 있어요.
하지만 패킹된 비교는 조건을 확인하는 동시에 마스크까지 계산하기 때문에 범용적이지 않아요. 비교 명령어에는 검사할 조건을 정확히 알려줘야 해요.
방법은 두 가지예요:
eq는 같은지, gt는 큰지를 나타내요.
다른 변형은 이 둘 중 하나의 결과를 조합해서 만들어요.정수 비교에서 특정 조건 접미사를 쓴다는 점을 빼면, 문법은 지금까지 봐 온 것과 같은 구조를 따라요:
p + cmp + 조건 + 크기(b, w, d 또는 q)예요.cmp + p + 크기(s 또는 d)예요.
조건은 추가 피연산자로 즉시값에 담겨 전달돼요.앞서 말했듯이, 정수 비교에는 같은지와 큰지만 있어요:
| 명령어 | 설명 |
|---|---|
pcmpeqb, pcmpeqw, pcmpeqd, pcmpeqq
|
레인별 같은지 여부 |
pcmpgtb, pcmpgtw, pcmpgtd, pcmpgtq
|
레인별 부호 있는 큰지 여부 |
movdqa xmm0, [rel scores]
pcmpgtd xmm0, [rel threshold] ; lane i = 0xFFFFFFFF (-1) if scores[i] > threshold[i], else 0
보다 작은지 비교하려면 gt를 쓰되 피연산자를 바꿔요. a < b는 b > a와 같아요.
비교가 부호 있는 연산이라는 점에 주의해요. 부호 없는 비교를 하려면 두 피연산자의 최상위 비트를 뒤집어요. 최상위 비트만 1로 설정된 마스크와 XOR하면 돼요.
알아 두면 유용한 관용구가 두 가지 있어요:
예를 들어:
pxor xmm4, xmm4 ; xmm4 = all zeros
pcmpeqd xmm7, xmm7 ; xmm7 = all ones
모두 0과 모두 1은 각각 "모두 거짓"과 "모두 참"을 나타내는 흔한 마스크예요.
패킹된 0이나 패킹된 -1을 나타내는 데도 쓸 수 있는데, 둘 다 흔한 센티널 값이에요.
예를 들어, 문자열의 끝을 표시하는 NUL은 0이에요.
부동소수점 레인은 모양이 달라요. 명령어 하나 cmpps(64비트 레인에는 cmppd)를 쓰고, 조건을 즉시값으로 넘겨요:
movaps xmm0, [rel readings]
cmpps xmm0, [rel limits], 1 ; condition 1 is "less than": lane i = all ones if readings[i] < limits[i]
NASM에는 올바른 즉시값으로 연결되고 기억하기 더 쉬운 의사 명령도 있어요.
다음에서 px의 x는 s(32비트 부동소수점) 또는 d(64비트 부동소수점)가 될 수 있어요:
| 의사 명령 | 즉시값 | 비교 |
|---|---|---|
cmpeqpx |
0 | a == b |
cmpltpx |
1 | a < b |
cmplepx |
2 | a <= b |
cmpunordpx |
3 | a 또는 b가 NaN |
cmpneqpx |
4 | a != b |
cmpnltpx |
5 | a >= b |
cmpnlepx |
6 | a > b |
cmpordpx |
7 | a도 b도 NaN이 아님 |
마스크는 조건의 결과를 담고 있어요. 그러면 이 마스크로 그 조건에 따라 두 값 집합 중 하나를 레인별로 골라낼 수 있어요. 마스크가 참인 레인은 한 값에서, 거짓인 레인은 다른 값에서 가져와요:
; result = (a AND mask) OR (b AND NOT mask)
movdqa xmm2, xmm0 ; xmm0 holds the mask, keep a copy
pand xmm2, xmm3 ; xmm2 = a AND mask: lanes of a where mask is true
pandn xmm0, xmm4 ; xmm0 = NOT mask AND b: lanes of b where mask is false
por xmm2, xmm0 ; combine the two halves
여기서 pandn의 비대칭성이 빛을 발해요. 마스크가 목적지에 놓여 부정되고, 명령어 하나로 b에서 값을 골라내니까요.
이 패턴은 분기 없는 선택의 패킹된 형태예요.
모든 레인이 계산되고, 어떤 값이 살아남을지는 마스크만이 결정해요. 어디에도 jcc가 없어요.
같은 선택을 마스크 레지스터에서 원소마다 한 비트씩 읽어 곧바로 수행하는 명령어도 있어요. 이런 명령어를 블렌드 명령어라고 해요:
| 명령어 | 원소 | 마스크 출처 |
|---|---|---|
pblendvb |
바이트 | 암시적 xmm0
|
blendvps |
32비트 레인 | 암시적 xmm0
|
blendvpd |
64비트 레인 | 암시적 xmm0
|
첫 번째 명령어는 정수 문법을 따르고, 나머지 둘은 부동소수점 문법을 따른다는 점에 주의해요. 하지만 이 명령어들은 그냥 원시 바이트를 선택할 뿐이라, 어느 것이든 정수와 부동소수점 모두에 쓸 수 있어요.
각 원소마다, 블렌드는 해당 마스크 원소의 최상위 비트가 0이면 목적지를 유지하고, 1이면 출처에서 가져와요.
참조하는 것은 오직 그 최상위 비트뿐인데, 비교 마스크는 각 레인이 모두 1이거나 모두 0이라 이 조건을 만족해요.
마스크 레지스터는 언제나 xmm0이고, 이는 암시적이에요:
movaps xmm0, [rel mask] ; the selecting mask must be in xmm0
movaps xmm1, [rel b] ; destination: kept where the mask bit is clear
blendvps xmm1, [rel a] ; source: taken where the mask bit is set
pblendvb로 다른 크기의 비교 마스크에서도 레인을 선택할 수 있어요.
참인 레인의 모든 바이트가 1이므로, pblendvb는 그 바이트를 전부 선택해요.
이 명령어들은 모두 수행하는 연산(blend) 뒤에 v를 붙여요.
이 v는 _variable_을 뜻하는데, 선택이 고정적이지 않고 레지스터에 따라 달라지기 때문이에요.
즉시값에 따라 선택하는, v가 없는 변형도 있어요.
이들도 같은 방식으로 동작해서, 즉시값의 비트 i가 1이면 레인 i를 선택해요.
SIMD 코드는 강력하지만, 스칼라 코드가 가진 유연성의 상당 부분을 갖추지 못했어요. 많은 상황에서 패킹된 레지스터에서 다시 스칼라 명령어의 세계로 돌아가야 해요.
movmsk 명령어군은 두 세계를 잇는 다리 역할을 해요.
이 명령어들은 모든 레인의 최상위 비트를 범용 레지스터로 추출해요:
| 명령어 | 모으는 것 | 결과 너비 |
|---|---|---|
pmovmskb |
16바이트 각각의 최상위 비트 | 16비트 |
movmskps |
4개 dword 각각의 최상위 비트 | 4비트 |
movmskpd |
2개 qword 각각의 최상위 비트 | 2비트 |
비교 뒤에 사용하면, 1로 설정된 각 비트는 "참"인 레인을, 0으로 지워진 각 비트는 "거짓"인 레인을 나타내요.
이 결과는 그러면 스칼라 명령어로 평소처럼 다룰 수 있어요.
예를 들어 popcnt는 일치하는 개수를 세고, tzcnt는 첫 번째를 찾아요.
범용 레지스터는 32비트 또는 64비트일 수 있어요.
스칼라 test 명령어에도 패킹된 변형이 있는데, 바로 ptest예요.
ptest는 두 피연산자를 수정하지 않고 AND 연산을 수행한다는 점에서 스칼라 짝과 비슷해요.
test와 달리, ptest는 첫 번째 피연산자를 부정하는 ANDN 연산도 수행해요.
따라서 ptest는 결과에 따라 플래그를 설정하는, pand와 pandn의 비파괴 버전이라고 생각할 수 있어요.
이 두 명령어와 거의 같은 방식으로, ptest는 SIMD 레지스터 전체를 하나의 레인으로 취급하기 때문에 크기 접두사를 붙이지 않아요.
AND 연산의 결과가 0이면 ZF가 설정되고, ANDN 연산의 결과가 0이면 CF가 설정돼요.
즉 ptest로 모두 1인 마스크와 모두 0인 마스크를 모두 확인할 수 있어요:
ptest하면, 레지스터가 모두 0일 때만 ZF가 설정돼요.
이는 test를 자기 자신과 써서 0인지 확인하는 흔한 스칼라 관용구를 그대로 따른 거예요.ptest하면, 레지스터가 모두 1일 때만 CF가 설정돼요.
또한 레지스터가 모두 0일 때만 ZF가 설정되므로, 두 마스크를 한 번에 확인할 수 있어요.pxor xmm0, xmm0 ; all zeros
pcmpeqb xmm1, xmm1 ; all ones
pcmpeqb xmm2, xmm2
ptest xmm0, xmm0 ; ZF set: a register against itself detects all zeros
ptest xmm0, xmm1 ; ZF set, CF clear: xmm0 is all zeros, not all ones
ptest xmm2, xmm1 ; CF is set only if xmm2 is all ones
ptest의 결과는 평소처럼 분기하거나 setcc, cmovcc 같은 분기 없는 명령어에 활용할 수 있어요.
여러분은 학교의 채점 스테이션을 운영하며, 한 번에 한 블록씩 학급 결과를 채점해요.
각 블록에는 4개의 결과가 들어 있고, 스테이션은 블록 안의 모든 결과에 같은 연산을 적용해요. 점수는 32비트 부동소수점 수예요. 여러 단계에서 마스크를 사용해요. 마스크는 4개의 레인으로 이루어진 블록으로, 각 레인은 모두 1(해당 결과에 대한 예)이거나 모두 0(아니요)이에요.
다섯 개의 과제가 있어요. 피연산자는 메모리 주소를 통해 전달받아요. 어떤 과제는 답을 결과 주소에 기록하고, 어떤 과제는 답을 직접 반환해요.
이 연습 문제의 모든 메모리 주소는 16바이트로 정렬되어 있어요.
이 연습 문제의 계산은 SIMD 명령어를 사용해 수행해야 해요.
첫 번째 단계에서는 각 결과를 임계값과 비교해 채점해요. 점수가 임계값을 초과할 때만 기준을 통과해요. 임계값보다 작거나 같은 점수는 통과하지 못해요.
flag_above_threshold 함수를 구현해요. 이 함수는 임계값을 초과하는 점수마다 모두 1인 레인을, 그렇지 않은 경우 모두 0인 레인을 만들어 마스크를 구성해요.
이 함수는 다음 순서로 인자를 받아요:
result: 4개의 마스크 레인이 기록될 버퍼의 메모리 주소예요.scores: 점수들의 메모리 주소로, 4개의 32비트 정규 부동소수점 수가 들어 있어요(NaN은 없어요).thresholds: 각 레인의 임계값에 대한 메모리 주소로, 4개의 32비트 정규 부동소수점 수가 들어 있어요(NaN은 없어요).scores = {72.0, 55.0, 90.0, 40.0}
thresholds = {60.0, 60.0, 60.0, 60.0}
result = {0xFFFFFFFF, 0x00000000, 0xFFFFFFFF, 0x00000000}
이 함수는 반환값이 없어요.
별도의 보고서에서는 만점을 받은 결과, 즉 가능한 최고 점수에 도달한 결과를 강조해요.
flag_perfect 함수를 구현해요. 이 함수는 최고 점수와 같은 점수마다 모두 1인 레인을, 그렇지 않은 경우 모두 0인 레인을 만들어 마스크를 구성해요.
이 함수는 다음 순서로 인자를 받아요:
result: 4개의 마스크 레인이 기록될 버퍼의 메모리 주소예요.scores: 점수들의 메모리 주소로, 4개의 32비트 정규 부동소수점 수가 들어 있어요(NaN은 없어요).maxima: 각 레인의 최고 점수에 대한 메모리 주소로, 4개의 32비트 정규 부동소수점 수가 들어 있어요(NaN은 없어요).scores = {100.0, 88.0, 100.0, 73.0}
maxima = {100.0, 100.0, 100.0, 100.0}
result = {0xFFFFFFFF, 0x00000000, 0xFFFFFFFF, 0x00000000}
이 함수는 반환값이 없어요.
각 점수는 1에서 3까지의 등급을 받아요:
50.0 이하인 점수는 등급 1이에요.assign_ranks 함수를 구현해요. 이 함수는 각 점수의 등급을 기록해요.
합격 기준과 등급 값은 메모리에 패킹된 상수로 정의하는 게 좋아요. 앞의 두 과제에서 만든 함수를 재사용할 수 있어요. 점수가 임계값을 넘으면 적어도 등급 2이고, 최고 점수와 같으면 등급 3이에요.
이 함수는 다음 순서로 인자를 받아요:
result: 4개의 등급이 기록될 버퍼의 메모리 주소로, 각 등급은 32비트 부호 없는 정수예요.scores: 점수들의 메모리 주소로, 4개의 32비트 정규 부동소수점 수가 들어 있어요(NaN은 없어요).maxima: 각 레인의 최고 점수에 대한 메모리 주소로, 4개의 32비트 정규 부동소수점 수가 들어 있어요(NaN은 없어요).scores = {40.0, 75.0, 100.0, 60.0}
maxima = {100.0, 100.0, 100.0, 100.0}
result = {1, 2, 3, 2}
이 함수는 반환값이 없어요.
한 해 동안 각 학생은 누적 등급을 쌓아요. 스테이션은 보충 수업을 몇 개 열어야 할지 계획하기 위해, 전체 학생 가운데 합격 기준 아래로 떨어지는 등급이 몇 개인지 집계해요.
count_failures 함수를 구현해요. 이 함수는 모든 블록에서 합격 기준보다 엄격하게 낮은 등급이 몇 개인지 반환해요.
기준은 4개의 동일한 레인으로 이루어진 블록으로 주어지므로, 한 번만 불러와 모든 블록에 재사용할 수 있어요.
이 함수는 다음 순서로 인자를 받아요:
ranks: 등급들의 메모리 주소로, 4개 레인 블록이 정수 개만큼 있고, 각 등급은 32비트 부호 없는 정수예요.block_count: 4개 레인 블록의 개수로, 항상 0보다 커요.pass_threshold: 합격 기준의 메모리 주소로, 4개의 동일한 32비트 정수가 들어 있어요.ranks = {1, 2, 3, 1, 2, 2, 1, 3} // 2 blocks
block_count = 2
pass_threshold = {2, 2, 2, 2}
// => 3
이 함수는 개수를 부호 있는 32비트 정수로 반환해요.
기록을 보관하기 전에, 스테이션은 전체 학생이 문제없는지 확인해요. 어떤 블록에서도 실패한 결과가 하나도 없으면 통과예요.
all_passed 함수를 구현해요. 이 함수는 모든 학생이 합격했으면 1을, 그렇지 않으면 0을 반환해요.
학생은 failing 배열에서 대응하는 레인이 모두 0이면 합격이에요.
이 함수는 다음 순서로 인자를 받아요:
failing: 실패 마스크들의 메모리 주소로, 4개 레인 블록이 정수 개만큼 있고, 각 레인은 모두 1이거나 모두 0이에요.block_count: 4개 레인 블록의 개수로, 항상 0보다 커요.failing = {0x00000000, 0x00000000, 0x00000000, 0x00000000,
0x00000000, 0x00000000, 0x00000000, 0x00000000} // 2 blocks
block_count = 2
// => 1
이 함수는 답을 부호 있는 32비트 정수, 즉 1 또는 0으로 반환해요.
Exercism에 가입하고 x86-64 Assembly 트랙을 개념 22개연습 문제 130개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.