Post Lists

2019년 12월 14일 토요일

SSE from Songho Ahn

http://www.songho.ca/misc/sse/sse.html

SIMD (Single Instruction, Multiple Data, "seem-dee"로 발음되는) 연산은 단일의 명령어로 병렬로 여러개의 데이터를 처리한다. 이것은 한 번에 4개의 연산이 중요한 성능 개선을 만든다.

SSE는 single-precision floating-point 연산을 위해 8개의 새로운 128-bit 레지스터들 (xmm0 ~ xmm7)를 정의한다. 각 레지스터는 128비트 길이이기 때문에, 우리는 32-bit floating-point numbers를 총 4개 저장할 수 있다 (1-bit는 부호, 8-bit는 exponent, 23-bit는 mantissa).

Scalar and Packed Instructions
SSE는 연산에 대해 두 가지 유형을 정의한다; scalar와 packed. Scalar operation은 least-significant data element (0 ~ 31비트)에 대해서만 작동하고, packed operation은 병렬로 모든 4개의 원소들을 연산한다. SSE 명령어들은 scalar operations에 대해 -ss (Single Scalar)의 접미사를 갖는다. 그리고 packed operations에 대해 -ps(Parallel Scalar)를를 갖는다.

스칼라 연산에 대해 xmm0에 있는 상위 3개의 원소들은 바뀌지 않는다는 것을 주목해라.

Data Movements
너가 알아야 하는 첫 번째 것들은 메모리에서 xmm registers로 어떻게 데이터를 복사하는 가이고, SIMD 연산 후에 xmm registers로부터 너의 어플리케이션으로 그 결과를 다시 얻는 가이다. 그 데이터 이동 명령어들은 memory와 xmm registers들 사이에서 scalar와 packed data를 움직인다.

movss : single floating-point data를 복사한다.
movlps : 2개의 floating-point data (low packed)를 복사한다.
movhps : 2개의 floating-point data (high packed)를 복사한다.
movaps : 정렬된 4개의 floating-point data를 복사한다 (fast)
movhlps : 2개의 high elements를 low position으로 복사한다.
movlhps : 2개의 low elements를 high position으로 복사한다.

movaps는 메모리에 있는 데이터가 더 좋은 성능을 위해서 정렬된 16byte boundray에 있어야만 하는 것을 요구한다. Data Alignment에서 데이터를 어떻게 정렬하는지에 대해 더 많이 읽어라. movhlpsmovlhps에 대한 source와 destination에 대한 operands는 xmm registers 여야만 한다.

Arithmetic Instructions
산술 명령어들은 산술 연산을 수행하기 위해 2개의 operands (registers 또는 메모리)를 요구하고, 첫 번째 register에 그 결과를 write한다. 그 source operand는 xmm register 또는 memory일 수 있찌만, 그 destination operand는 xmm register여야만 한다.

addss/ps
subss/ps
mulss/ps
divss/ps
rcpss/ps -> y = 1/x
sqrtss/sqrtps
rsqrtss/rsqrtps -> y = 1/sqrt(x)
maxss/maxps
minss/minps

Shuffle Instruction
shufps는 2개의 operands와 1개의 mask를 요구한다. shufps는 그 마스크를 기반으로 각 operand (register)로부터 2개의 원소들을 선택한다. 그 첫 번째 operand에 대한 2개의 원소들은 destination register에 있는 lower 2 elements로 복사되어지고, second operand에 있는 2개의 원소들은 그 destination register에 있는 더 높은 2개의 elements로 복사된다.

shufps 명령어를 사용하여, 너는 어떤 순서든 어떤 4개의 데이터 원소들을 shuffle할 수 있다. shufps의 빈번한 사용성은  broadcast, swap 그리고 rotate이다.

Broadcast
이것은 single data원소의 모든 4개의 fields를 복사한다. 가능한 masks는 다음이다
00h : least significant data element를 Broadcast한다.
55h : 두 번째 데이터 원소를 Broadcast한다.
AAh : 세 번째 data element를 Broadcast한다.
FFh : most significant data element를 Broadcast한다.

shufps xmm0, xmm0, 0h
shufps xmm0, xmm0, 55h

Swap
이 명령어는 1Bh mask로 데이터 원소들의 순서로 반대로 바꾼다
shufps xmm0, xmm0, 1Bh

Rotate
그것은 data elements의 왼쪽 또는 오른쪽 회전을 수행한다. 데이터를 왼쪽 회전으로 shift하기 위해서 93h를 사용해라. 그러면 most significant data element는 least significant position으로 움직여진다. 데이터를 오른쪽으로 shift하기 위해 39h를 사용해라. 그러면 least significant data element는 most significant position으로 움직여진다.

shufps xmm0, xmm0, 93h
shufps xmm0, xmm0, 39h

Unpack
unpcklps는 2개의 operands의 각각으로부터 2개의 lower elements를 복사하고 interleave한다. unpckhps는 2개의 operands의 각각으로부터 2개의 higher elements를 복사하고 destination register에 interleaves한다.

unpcklps xmm0, xmm1
unpckhps xmm0, xmm1

Comparision Instructions
비교 명령어들은 2개의 operands를 비교하고, destination register에 true (모두 1를) 또는 false (모두 0을) 설정한다. source operand는 xmm register 또는 메모리가 될 수 있찌만, 그 destination은 xmm register가 되어야만 한다.

x = y | cmpeqss / cmpeqps
x != y | cmpneqss / cmpneqps
x < y | cmpltss / cmpltps
x !< y | cmpnltss / cmpnltps
x <= y | cmpless / cmpleps
x !<= y | cmpnless / cmpnleps

Bitwise Logical Instructions
논리 명령어들은 bitwise 논리 연산을 packed floating-point elements에 대해 수행한다. 일반적은 사용은 숫자를 음수화하거나 절대값으로 바꾸는 것이다.

AND - andps
OR - orps
XOR - xorps
AND NOT - andnps

Absolute Value
절대값 연산을 수행하기 위해, source register에서 most significant bit (sign bit)에 0을 저장하고, 나머지 비트들에 1를 저장해라. 그러고나서, AND operation을 수행해라 : number & 7FFFFFFFh.

andps xmm0, xmm1

Negate
음수화를 수행하기 위해, most significant bit에 1를 저장하고, 나머지에는 0을 저장해라. 그러고나서 XOR 연산을 수행해라 : number ^ 8000000h.

Conversion
변환 명령어들은 floating-point 숫자를 정수로 또는 역으로 변환한다.

Float to integer with rounding - cvtss2si / cvtps2pi
Float to integer with truncation - cvttss2si / cvttps2pi
Integer to float - cvtsi2ss / cvtpi2ps

그 packed operations들인 cvtps2pi, cvttps2pi 그리고 cvtpi2ps는 병렬로 4개가 아닌 2개의 숫자들을 바꾼다. 왜냐하면 MMX 레지스터들 (mm0 ~ mm7)은 64bit 길이 (2 * 32bit)이기 떄문이다. 그러므로, XMM registers에서 두 개의 상위 elements를 변환시에 사용되지 않는다.

cvtss2si eax, xmm0
cvtsi2ss xmm0, eax
cvtps2pi mm0, xmm0
cvtpi2ps xmm0, mm0

Streaming memory Instructions
SSE는 prefetching의 사용을 통해서 read-miss latency가 실행을 중복하도록 한다. 그리고 그것은 write-miss latency가 streaming stores를 통해 실행을 overlapping함으로써 줄어들도록 한다.

Prefetch Instructions
그 prefetch instructions은 그 프로그램이 그 데이터를 실제로 필요하기전에 L1 and/or L2 cache에 데이터를 가져오는 cache hints를 제공한다. 이것은 data access latency를 최소화한다. 이러한 명령어들은 비동기적으로 실행된다. 그러므로, 프로그램 실행은 prefetching하는 동안 멈추지 않는다.

prefetcht0 : t0 hint를 사용해서 데이터를 메모리에서 L1과 L2 caches로 옮긴다.
prefetcht1 :  t1 hint를 사용해서 데이터를 L2 cache로 옮긴다.
prefetchnta :  non-temporal aligned data를 메모리에서 L1 cache로 직접 옮긴다 (L2를 지나치고).

AMD athlonXP, Intel Pentium4 또는 더 높은 CPUs들은 automatic cache prefetching을 포함하고 있다는 것을 주목해라. 그러므로, 너의 코드에서 이러한 명령어들을 직접 호출하는 것은 반드시해야 하는 것은 아니다.

Streaming Store Instuctions
store move 명령어들은 streaming하는 것은 캐시를 업데이트하지 않고 메모리에 직접적으로 non-temporal data를 저장한다. 이것은 cache pollution을 최소화하고, 캐시와 XMM registers사이의 불필요한 bus bandwidth를 최소화한다. 왜냐하면 그것은 write miss에 대해 write-allocate를 하지 않기 때문이다.

Non-temporal은 data가 긴 간격으로 (한 번 참조되고, 즉각적인 미래에 재사용되지 않는) 불규칙적으로 접근된다는 것을 의미한다. 예를들어 3D graphics에서 vertex data는 매 프레임마다 재 생성된다. Write-allocate는 cache miss가 발생할 때 data가 cache에 쓰여지는 것을 말한다.

movntps : XMM register에서 memory로 직접적으로 4개의 non-temporal floating-point elements를 옮기고, 캐시를 지나친다. 그 메모리 주소는 정렬된 16-byte boundaries여야만 한다.

movntq : XMM register로부터 메모리에 non-temporal quadword (2개의 정수, 4개의 shorts, 8개의 chars)를 옮기고, 캐시를 지나친다.

movntps [edi], xmm0
movntq [edi], mm0

Store Fence
sfencesfence 명령어 이전에 어떤 store 명령어에 대한 데이터가 이후의 store instruction전에 메모리에 쓰여질 거라는 것을 보장한다.

다음의 inline assembly 예제는 source에서 destination array로 4개의 float data (16-byte block)를 복사하는 것을 보여준다.

// move 4 floats (16-bytes) at once
__asm
{
mov ecx, count        // # of float data
chr ecx, 2               // # of 16-byte blocks (4 floats)
mov edi, dst           // dst pointer
mov esi, src           // src pointer

loop1:
movaps xmm0, [esi]       // get from src
movaps [edi], xmm0      // put to dst

add esi, 16
add edi, 16

dec ecx                      // next
jnz loop1
}

Detecting SSE support
cupid 명령어는 그 processor가 SSE를 지원하는지 안하는지를 탐지하는데 사용될 수 있다. 대부분의 x86 processors들은 요즘에 cpuid 명령어를 지원한다. 그리고 이것은 CPU 정보와 지원되는 기능들을 반환한다. 너의 CPU가 cpuid 명령어를 지원하는지 결정하기 위해, EFLAGS에서 bit 21를 수정하려고 해보아라. 만약 bit 21이 바뀔 수 있다면, cpuid는 호출될 수 있다.

cupideax=01h로 호출하는 것은 edx register에 표준 feature flags를 반환한다. SSE는 만약 edx register의 bit 25 (least significant bit에서 26번째 비트)가 1이라면 지원된다. 게다가, bit-26은 SSE2 support를 위한 것이고, bit-23은 MMX support를 위한 것이다.

이것은 SSE support와 다른 features를 탐지하는 매우 간단한 프로그램이다 : cupid_msvc.zip.
(이 프로그램은 그것에 MSVC에 특정한 inline assembly codes를 사용한다는 것에 주목해라)

Example of Inline Assembly
다음의 프로그램은 MSVC inline assembly에서 SSE 사용의 한 예시이다. 그것은 모든 위의 SSE 명령어들에 대한 예제 코드를 포함한다.

소스와 바이너리를 다운로드해라 :  sse_msvc.zip

댓글 없음:

댓글 쓰기