Chciałbym się tutaj zgodzić z Brianem, Wouterem i pjc50.
Chciałbym również dodać, że w przypadku procesorów ogólnego przeznaczenia, zwłaszcza procesorów CISC, instrukcje nie wszystkie mają taką samą przepustowość - skomplikowana operacja może po prostu zająć więcej cykli niż łatwa.
Rozważ X86: AND
(która jest operacją „i”) jest prawdopodobnie bardzo szybka. To samo dotyczy NOT
. Spójrzmy na trochę demontażu:
Kod wejściowy:
#include <immintrin.h>
#include <stdint.h>
__m512i nand512(__m512i a, __m512i b){return ~(a&b);}
__m256i nand256(__m256i a, __m256i b){return ~(a&b);}
__m128i nand128(__m128i a, __m128i b){return ~(a&b);}
uint64_t nand64(uint64_t a, uint64_t b){return ~(a&b);}
uint32_t nand32(uint32_t a, uint32_t b){return ~(a&b);}
uint16_t nand16(uint16_t a, uint16_t b){return ~(a&b);}
uint8_t nand8(uint8_t a, uint8_t b){return ~(a&b);}
Polecenie wykonania złożenia:
gcc -O3 -c -S -mavx512f test.c
Zespół wyjściowy (skrócony):
.file "test.c"
nand512:
.LFB4591:
.cfi_startproc
vpandq %zmm1, %zmm0, %zmm0
vpternlogd $0xFF, %zmm1, %zmm1, %zmm1
vpxorq %zmm1, %zmm0, %zmm0
ret
.cfi_endproc
nand256:
.LFB4592:
.cfi_startproc
vpand %ymm1, %ymm0, %ymm0
vpcmpeqd %ymm1, %ymm1, %ymm1
vpxor %ymm1, %ymm0, %ymm0
ret
.cfi_endproc
nand128:
.LFB4593:
.cfi_startproc
vpand %xmm1, %xmm0, %xmm0
vpcmpeqd %xmm1, %xmm1, %xmm1
vpxor %xmm1, %xmm0, %xmm0
ret
.cfi_endproc
nand64:
.LFB4594:
.cfi_startproc
movq %rdi, %rax
andq %rsi, %rax
notq %rax
ret
.cfi_endproc
nand32:
.LFB4595:
.cfi_startproc
movl %edi, %eax
andl %esi, %eax
notl %eax
ret
.cfi_endproc
nand16:
.LFB4596:
.cfi_startproc
andl %esi, %edi
movl %edi, %eax
notl %eax
ret
.cfi_endproc
nand8:
.LFB4597:
.cfi_startproc
andl %esi, %edi
movl %edi, %eax
notl %eax
ret
.cfi_endproc
Jak widać, dla typów danych mniejszych niż 64, wszystkie rzeczy są po prostu obsługiwane jako długie (stąd i l, a nie l ), ponieważ, jak się wydaje, jest to „natywna” przepustowość mojego kompilatora.
Fakt, że jest mov
między nimi wynika tylko z faktu, że eax
jest to rejestr zawierający wartość zwracaną przez funkcję. Zwykle wystarczy obliczyć w edi
rejestrze ogólnego przeznaczenia, aby obliczyć wynik.
W przypadku 64 bitów jest tak samo - tylko z „quad” (stąd końcowymi q
) słowami i rax
/ rsi
zamiast eax
/ edi
.
Wygląda na to, że dla 128-bitowych operandów i większych Intel nie dbał o wdrożenie operacji „nie”; zamiast tego kompilator tworzy 1
rejestr ogólny (samo porównanie rejestru z samym sobą, wynik zapisany w rejestrze z vdcmpeqd
instrukcją) i tak xor
jest.
W skrócie: Implementując skomplikowaną operację z wieloma instrukcjami elementarnymi, niekoniecznie spowalniasz operację - po prostu nie ma korzyści z posiadania jednej instrukcji, która wykonuje wiele instrukcji, jeśli nie jest szybsza.