2016-09-14 1 views
0

다시 한번 기본 어셈블리 언어를 다시 가르치기 위해 다시 뛰어 들고 있으므로 모든 것을 완전히 잊지는 않습니다.SIMD 연산 결과가 배열로 돌아가는 방법 : 캐시 비호 환적입니까?

나는이 연습 코드를 만들었습니다. 그리고 그 안에, 벡터 연산의 결과를 배열에 거꾸로 넣어야한다는 것이 밝혀졌습니다. 그렇지 않으면 틀린 대답을했습니다. 덧붙여 말하자면, 이것은 GCC가 SIMD 연산 결과를 메모리 위치로 되돌려 보내는 것과 마찬가지로 어셈블리 코드를 출력하는 방법이기도하므로 "올바른"방법이라고 생각합니다.

그러나 오랜 시간 동안 게임 개발자로서 의식 해 왔던 뭔가, 캐시의 친숙 함에서 뭔가가 생겼습니다. 내 이해는 인접한 메모리 블록에서 앞으로 나아가는 것이 항상 이상적이며, 그렇지 않으면 캐시 미스가 발생할 위험이 있습니다.

내 질문은 : 아래의 예제가 두 개의 4 요소 벡터를 계산하고 종료하기 전에 네 개의 숫자를 내뱉는 것 이상이라도 기술적으로 무엇인가 배열에 다시 입력해야하는지 궁금합니다. 역순으로 - 실제 세계에서 캐시 미스에 영향을 미칩니다. 초당 수십만 건의 SIMD 벡터 계산 (더 구체적으로 메모리로 반환)을 수행하는 일반적인 프로덕션 수준의 프로그램에서 그렇습니다. 여기

는 stackexchange하는 나의이 호기심을 가지고 나를하라는 메시지가 원래 포함 주석이있는 전체 코드 (리눅스 64 비트 NASM)입니다 :

extern printf 
extern fflush 

global _start 
section .data 
outputText:  db '[%f, %f, %f, %f]',10,0 

align 16 
vec1: dd 1.0, 2.0, 3.0, 4.0 
vec2: dd 10.0,10.0,10.0,50.0 

section .bss 
result: resd 4  ; four 32-bit single-precision floats 

section .text 
_start: 
    sub rsp,16 

    movaps xmm0,[vec1] 
    movaps xmm1,[vec2] 

    mulps xmm0,xmm1   ; xmm0 = (vec1 * vec2) 

    movaps [result],xmm0  ; copy 4 floats back to result[] 

    ; printf only accepts 64-bit floats for some dumb reason, 
    ; so convert these 32-bit floats packed within the 128-bit xmm0 
    ; register into four 64-bit floats, each in a separate xmm* reg 
    movss xmm0,[result+12] ; result[3] 
    unpcklps xmm0,xmm0  ; 32-->64 bit 
    cvtps2pd xmm3,xmm0  ; put double in 4th XMM 

    movss xmm0,[result+8] ; result[2] 
    unpcklps xmm0,xmm0  ; 32-->64 bit 
    cvtps2pd xmm2,xmm0  ; put double in 3rd XMM 

    movss xmm0,[result+4] ; result[1] 
    unpcklps xmm0,xmm0  ; 32-->64 bit 
    cvtps2pd xmm1,xmm0  ; put double in 2nd XMM 

    movss xmm0,[result]  ; result[0] 
    unpcklps xmm0,xmm0  ; 32-->64 bit 
    cvtps2pd xmm0,xmm0  ; put double in 1st XMM 

    ; FOOD FOR THOUGHT! 
    ; ***************** 
    ; That was done backwards, going from highest element 
    ; of what is technically an array down to the lowest. 
    ; 
    ; This is because when it was done from lowest to 
    ; highest, this garbled bird poop was the answer: 
    ; [13510801139695616.000000, 20.000000, 30.000000, 200.000000] 
    ; 
    ; HOWEVER, if the correct way is this way, in which 
    ; it traipses through an array backwards... 
    ; is that not cache-unfriendly? Or is it too tiny and 
    ; miniscule to have any impact with cache misses? 

    mov rdi, outputText  ; tells printf where is format string 

    mov rax,4    ; tells printf to print 4 XMM regs 
    call printf 

    mov rdi,0 
    call fflush    ; ensure we see printf output b4 exit 

    add rsp,16 

_exit: 
    mov eax,1   ; syscall id for sys_exit 
    mov ebx,0   ; exit with ret of 0 (no error) 
    int 80h 

답변

0
으로 주소를 내림차순으로 스트림을 인식 할 수

HW 프리 페 처가 잘 오름차순. Intel의 최적화 매뉴얼은 HW 프리 페처를 공정하게 문서화합니다. AMD의 프리 페처는 내림차순 패턴을 인식 할 수 있다는 점에서 대체로 유사하다고 생각합니다.

단일 캐시 라인 내에서 AFAIK에 액세스하는 순서는 중요하지 않습니다.

더 많은 링크를 보려면 태그 위키를 참조하십시오. 특히 Agner Fog's Optimizing Assembly guide은 컴파일러가 수행 할 수있는 것보다 느리지는 않은 asm을 작성하는 방법을 학습합니다. 태그 위키에는 Intel의 매뉴얼에 대한 링크가 있습니다.


또한, 그것은 약간 못 생겼습니다.

Printf는 variadic 함수에 arg 승격을위한 C 규칙 때문에 double만을 허용합니다. 예, 이것은 다소 바보 같지만 FP-> base-10 텍스트 변환은 추가 float-> double 변환에서 오버 헤드를 감소시킵니다. 고성능 FP-> 문자열이 필요하다면 매 호출마다 형식 문자열을 구문 분석해야하는 함수를 사용하지 않아야합니다.

디버거 사용에 비해 ASM의 디버그 인쇄가 일반적으로 가치가있는 것보다 문제가 많습니다. 또한

:

  • 이 64 비트 코드, 그래서 종료 32 비트 int 0x80 ABI를 사용하지 않는 것입니다.
  • UNPCKLPS 명령어는 아무런 의미가 없습니다. CVTPS2PD는 두 가지 결과를 산출하지만 두 개를 변환하는 대신 동일한 번호를 병렬로 두 번 변환하고 다음에의 압축을 풀어야합니다. 스칼라 인수를 취하는 함수를 호출 할 때 XMM의 하위 요소 인 double만이 중요하므로 높은 가비지를 남겨 둘 수 있습니다.
  • 저장/재로드는 무의미

DEFAULT REL   ; use RIP-relative addressing for [vec1] 

extern printf 
;extern fflush   ; just call exit(3) instead of manual fflush 
extern exit 

section .rodata  ; read-only data can be part of the text segment 
outputText:  db '[%f, %f, %f, %f]',10,0 

align 16 
vec1: dd 1.0, 2.0, 3.0, 4.0 
vec2: dd 10.0,10.0,10.0,50.0 

section .bss 
;; static scratch space is unwise. Use the stack to reduce cache misses, and for thread safety 
; result: resd 4  ; four 32-bit single-precision floats 

section .text 
global _start 
_start: 
    ;; sub rsp,16   ; What was this for? We have a red-zone in x86-64 SysV, and we don't use 

    movaps xmm2, [vec1] 
    ; fold the load into the mulps 
    mulps  xmm2, [vec2] ; (vec1 * vec2) 

    ; printf only accepts 64-bit doubles, because it's a C variadic function. 
    ; so convert these 32-bit floats packed within the 128-bit xmm0 
    ; register into four 64-bit floats, each in a separate xmm* reg 

    ; xmm2 = [f0,f1,f2,f3] 
    cvtps2pd xmm0, xmm2  ; xmm0=[d0,d1] 
    movaps xmm1, xmm0 
    unpckhpd xmm1, xmm1  ; xmm1=[d1,d1] 

    unpckhpd xmm2, xmm2  ; xmm2=[f2,f3, f2,f3] 

    cvtps2pd xmm2, xmm2  ; xmm2=[d2,d3] 
    movaps xmm3, xmm3 
    unpckhpd xmm3, xmm3  ; xmm3=[d3,d3] 

    mov  edi, outputText  ; static data is in the low 2G, so we can use 32-bit absolute addresses 
    ;lea  rdi, [outputText] ; or this is the PIC way to do it 

    mov  eax,4    ; tells printf to print 4 XMM regs 
    call  printf 

    xor  edi, edi 
    ;call  fflush    ; flush before _exit() 
    jmp  exit     ; tailcall exit(3) which does flush, like if you returned from main() 

    ; add rsp,16 

;; this is how you would exit if you didn't use the libc function. 
_exit: 
    xor  edi, edi 
    mov  eax, 231    ; exit_group(0) 
    syscall      ; 64-bit code should use the 64-bit ABI 
또한 다른 등록의 낮은 64 비트에 하나 개의 레지스터에서 높은 64 비트를 이동 MOVHLPS를 사용할 수 있지만 그 거짓을 가지고

이전 내용에 대한 의존성.

cvtps2pd xmm0, xmm2  ; xmm0=[d0,d1] 

    ;movaps xmm1, xmm0 
    ;unpckhpd xmm1, xmm1  ; xmm1=[d1,d1] 

    ;xorps  xmm1, xmm1 ; break the false dependency 
    movhlps xmm1, xmm0  ; xmm1=[d1,??] ; false dependency on old value of xmm1 

샌디 브리지에서 xorps 및 movhlps는보다 효율적인 있기 때문에 handle xor-zeroing without using an execution unit을 할 수있을 것이다. IvyBridge 이상 및 AMD CPU는 동일한 방식으로 MOVAPS를 제거 할 수 있습니다. 대기 시간이 없습니다. 그러나 여전히 uop 및 일부 프론트 엔드 처리량 리소스가 필요합니다. 별도로 각각의 플로트를 저장하고 다시로드 및 변환하려고한다면


, 당신은 부하 (cvtss2sd xmm2, [result + 12]) 또는 movss 이후에, CVTSS2SD을 사용하십시오.

MOVSS를 처음 사용하면 CVTSS2SD가 잘못 설계하여 이전의 값으로 병합하는 대신 전체 레지스터에 대한 잘못된 종속성이 깨집니다. int-> float 또는 double 변환과 동일합니다. 병합 사례는 스칼라 수학보다 훨씬 드물며 reg-reg MOVSS를 사용하여 수행 할 수 있습니다.