Вычислите f(x)=2*(x^2)+5 с насыщением, используя набор инструкций MMX для 128 чисел размером 2 байта, загруженных из двоичного файла

У меня есть эта проблема, где мне нужно вычислить функцию f(x)=2*(x^2)+5 с набором инструкций MMX. У меня две проблемы. Это мой код сейчас:

section .data
    print_fmt db '%d', 10, 0
    my_loaded_data times 128 dw 0
    fives times 4 dw 5
    twos times 4 dw 2
    my_loaded_data_file_name db 'test_numbers.bin', 0
    mod_f db 'r', 0
section .text
extern printf, fopen, fread
global main
main:
    PUSH rbp
    mov rbp, rsp

    mov rax, 0
    mov rdi, my_loaded_data_file_name
    mov rsi, mod_f
    call fopen
    cmp rax, 0
    je .end

    PUSH rax
    PUSH rdi
    PUSH rsi
    PUSH rdx
    PUSH rcx

    mov rdi, my_loaded_data
    mov rsi, 2
    mov rdx, 128
    mov rcx, rax
    mov rax, 0
    call fread

    POP rcx
    POP rdx
    POP rsi
    POP rdi
    POP rax

    mov rsi, my_loaded_data
    mov rcx, 32
    jmp .square_loop

.square_loop:
    movq mm0, [rsi]
    movq mm1, [rsi]
    pmulhw mm0, mm1
    movq [rsi], mm0
    add rsi, 8
    loop .square_loop

    mov rcx, 32
    mov rsi, my_loaded_data
    movq mm1, [twos]
    jmp .mult_with2_loop

.mult_with2_loop:
    movq mm0, [rsi]
    pmulhw mm0, mm1
    movq [rsi], mm0
    add rsi, 8
    loop .mult_with2_loop

    mov rcx, 32
    mov rsi, my_loaded_data
    movq mm1, [fives]
    jmp .add_five_loop

.add_five_loop:
    movq mm0, [rsi]
    paddusw mm0, mm1
    movq [rsi], mm0
    add rsi, 8
    loop .add_five_loop
    jmp .write_it

.write_it:
    mov r8, my_loaded_data
    mov rcx, 128
.write_loop:
    mov rax, 0
    mov ax, [r8]

    PUSH r8
    PUSH rcx
    PUSH rdi
    PUSH rsi
    PUSH rax
    mov rdi, print_fmt
    mov rsi, rax
    mov rax, 0
    call printf
    POP rax
    POP rsi
    POP rdi
    POP rcx
    POP r8

    add r8, 2
    loop .write_loop
.end:
    mov rax, 0
    POP rbp
    ret

Моя первая проблема - инструкция умножения. Какую инструкцию я использую для насыщения. Сначала я подумал, что будет такая инструкция pmulsw но, похоже, нет. pmulhw сохраняет верхние 16-битные 32-битные результаты. Я не могу найти инструкцию, которая дала бы 16-битный результат. Это единственный способ сохранить 32-битные результаты?

Вторая проблема связана с printf. Это продолжает давать ошибку сегментации, и я не знаю почему. Это из моего терминала:

Program received signal SIGSEGV, Segmentation fault.
__GI___tcgetattr (fd=1, termios_p=termios_p@entry=0x7ffffffed9a8) at ../sysdeps/unix/sysv/linux/tcgetattr.c:42
42      ../sysdeps/unix/sysv/linux/tcgetattr.c: No such file or directory.

Вот make-файл:

zad7.o: zad7.asm
    nasm -f elf64 -g -F dwarf zad7.asm -o zad7.o
zad7: zad7.o
    gcc -o zad7 zad7.o -no-pie -ggdb

А для вашего удобства есть небольшая программа на C, которая может генерировать двоичный файл для чтения:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

void make_data_file(unsigned int number_of_data, unsigned int size_in_bytes, char* file_name) 
{
    FILE *write_ptr = fopen(file_name, "wb");
    if(write_ptr==NULL) 
    {
        printf("Error creating file '%s'.\n", file_name);
        return;
    }

    double n_bits = size_in_bytes * 8;
    unsigned int max_num = pow(2, n_bits);
    unsigned short random_number;
    for(int i=0; i< number_of_data; i++) 
    {
        random_number = i;
        fwrite(&random_number, size_in_bytes, 1, write_ptr);
    }
    fclose(write_ptr);
}

int main() 
{
    make_data_file(128, 2, "test_numbers.bin");
    return 0;
}

2 ответа

Решение

Если вы заботитесь о производительности, избегайте loop инструкция по современным процессорам. Почему инструкция цикла медленная? Разве Intel не могла реализовать это эффективно?, Также используйте SSE2 вместо MMX; ваш размер массива кратен 16 и 8, и вы используете x86-64, который гарантированно будет иметь SSE2. MMX абсолютно бессмысленен, если вы не делаете 32-разрядную версию для Pentium III / Athlon-XP и более ранних версий.

(Весь код в моем ответе будет работать с 8-байтовыми регистрами MMX вместо 16-байтовых регистров XMM, потому что есть MMX-версии всех инструкций, которые я использовал. Согласно приложению B к руководству NASM, pmullw, pxor, pcmpgtw, а также paddusw были все доступны в оригинальном P5 Pentium MMX. Некоторые инструкции, которые в руководстве Intel указаны как "MMX" (например, pmulhuw а также pshufw) были добавлены только позже, как, например, с Pentium II или вместе с SSE в Pentium III, но это не относится к инструкциям, которые были здесь полезны.)

См. https://stackru.com/tags/x86/info для получения руководств по производительности / оптимизации, а также ссылок ABI / соглашений о вызовах, которые объясняют 16-байтовое выравнивание стека, необходимое для вызова функций.

mov rax, 0 / mov ax, [r8] это действительно глупо. использование movzx eax, word [r8] как нормальный человек. Вам также не нужно jmp на следующую строку источника, как jmp .square_loop / .square_loop: Выполнение всегда переходит к следующей строке само по себе, если нет инструкции перехода.


x86 SIMD не имеет умножающего насыщения, только насыщающее добавление со знаком / без знака и насыщающее уплотнение для более узких элементов. (MMX / SSE2 paddsw / paddusw). Поскольку вы печатаете с %d Может быть, вы хотите подписанную насыщенность? Но это только после распаковки в 32-битную версию, и ваша формула всегда будет давать положительный результат, поэтому вы можете использовать беззнаковое насыщение. Я вижу, это то, что ваш код использует paddusw,

Кроме того, использование 3 отдельных циклов, которые сохраняют / перезагружают ваши данные между каждым шагом формулы, действительно ужасно. Вы (почти) всегда хотите увеличить вычислительную интенсивность (объем ALU-работы, выполняемой с вашими данными на каждую полосу пропускания памяти / кэша). Вам также не нужна инструкция умножения, чтобы удвоить число: просто добавьте его к себе. padd* работает на большем количестве портов, чем pmul* и имеет лучшую задержку и (в данном случае, соответствующую) пропускную способность.

default rel
  ;;; Efficient but *doesn't* saturate the multiply

    lea      rcx, [rsi + length]      ; rcx = end_pointer
    movdqa   xmm5, [fives]

.loop:                      ; do{
    movdqu   xmm0, [rsi]               ; use movdqa if your data is aligned, faster on very old CPUs.
    pmullw   xmm0, xmm0      ; x*x      ; does NOT saturate.  will wrap.
    paddusw  xmm0, xmm0      ; *= 2    ; with unsigned saturation
    paddusw  xmm0, xmm5      ; += 5    ; xmm5 = _mm_set1_epi16(5) outside the loop

    movdqu   [rsi], xmm0
    add      rsi, 16
    cmp     rsi, rcx        ; }while(p<endp);
    jb     .loop
    ...

section .rodata
align 16
  fives: times 8 dw 5

Для насыщения вы можете использовать SSSE3 https://www.felixcloutier.com/x86/pmaddubsw, но для этого нужны только байтовые входы. Насыщает горизонтальную сумму пар i8 x u8 => i16 произведений.

В противном случае вам, вероятно, придется распаковать мечи и packssdw (подписано) или packusdw (без знака насыщения) вернуться к словам. Но умножение мечей происходит медленно с SSE4.1 pmulld (2 мопа на Haswell и позже). Однако на некоторых старых процессорах это всего 1 моп. И, конечно же, распаковка создает в два раза больше работы благодаря наличию более широких элементов.


В этом случае ваша формула монотонна с величиной входа, поэтому вы можете просто сравнить на входе и насытить вручную.

Если мы предположим, что ваши входные данные также не подписаны, нам не нужно вводить абсолютное значение перед сравнением. Но (до AVX512) у нас нет сравнения целых чисел без знака, только подписи больше-больше, поэтому большие входы без знака будут сравниваться как отрицательные.

  • 2 * 0x00b5^2 + 5 = 0xfff7 который умещается в 16 бит
  • 2 * 0x00b6^2 + 5 = 0x102cd который не делает, и мы хотим, чтобы он был насыщенным 0xffff

Точка отсечения переполнения является четным числом, поэтому мы могли бы решить проблему сравнения со знаком, сдвигая вправо. Это было бы беззнаковым делением на 2, что позволило бы безопасно использовать результат как неотрицательное целое число со знаком. 0xb6 >> 1 = 0x5b, Но pcmpgtw это сравнение для > не >=,

Если мы обратим операнды к pcmpgtw сравнивать для (x>>1) < (0xb6>>1) тогда нам придется movdqa константа, чтобы избежать его разрушения, но нам все еще нужно сдвинуть вправо x с movdqa+psrlw. И более эффективно иметь вектор, который 0xffff когда необходимо насыщение (иначе 0), потому что мы можем применить это непосредственно с POR или PADDUSW.

Таким образом, наш лучший выбор - просто сдвинуть диапазон как x, так и 0xb5 подписать и сделать (x-0x8000) > (0xb5 - 0x8000) с использованием pcmpgtw SIMD подписано сравнить.

Другие худшие варианты включают в себя:

  • Проверка переполнения при умножении с pmulhuw рассчитать верхнюю половину (и проверить, если она не равна нулю). Мы были бы в опасности узкого места в умножении пропускной способности и проверки на ненулевое значение с pcmpeqw является обратным условию, которое мы хотим.
  • psubusw x, 0xb5 и проверьте, что это == 0. pcmpeqw даст нам перевернутую маску, но мы не можем использовать pcmpgtw проверить на usat16(x-0xb5) > 0 потому что большие входные данные (с установленным старшим битом) останутся "отрицательными" после вычитания только небольшого числа, например 0xb5,
  • paddusw и проверить на == 0xffff: только небольшие входные данные оставляют конечный результат ненасыщенным. Некоторые процессоры могут работать pxor на большем количестве портов, чем padd* и это не требует меньшего числа ненулевых векторных констант, так что это никак не лучше. Но это одинаково хорошо на Skylake.
default rel
;;; With a check on the input to saturate the output

    lea      rcx, [rsi + length]      ; rcx = end_pointer
    movdqa   xmm4, [input_saturation_cutoff]
    movdqa   xmm5, [fives]

    pcmpeqd  xmm3, xmm3
    psllw    xmm3, 15        ; xmm3 = set1_epi16(0x8000)  for range-shifting to signed

.loop:
    movdqu   xmm0, [rsi]
    movdqa   xmm1, xmm0

        ; if  x>0xb5 (unsigned), saturate output to 0xffff
    pxor     xmm1, xmm3      ; x - 0x8000 = range shift to signed for compare
    pcmpgtw  xmm1, xmm4      ; xmm1 = (x > 0xb5)  ?  -1  :  0

    pmullw   xmm0, xmm0      ; x*x
    paddusw  xmm0, xmm0      ; *= 2    ; saturating add or not doesn't matter here

    por      xmm1, xmm5      ; 0xffff (saturation needed) else 5.   Off the critical path to give OoO exec an easier time.
    paddusw  xmm0, xmm1      ; += 5  or  += 0xffff  with unsigned saturation.

    movdqu   [rsi], xmm0
    add      rsi, 16
    cmp      rsi, rcx
    jb      .loop

   ...

section .rodata
align 16
   input_saturation_cutoff:  times 8 dw (0x00b5 - 0x8000)  ; range-shifted to signed for pcmpgtw
   fives: times 8 dw 5

 ; 5 = 0xb6 >> 5 or 0xb6 >> 5  but we'd need to knock off the high bit from input_saturation_cutoff
 ; Or we could materialize constants like this:
 ; movdqa  xmm4, [set1w_0xb5]
 ; pcmpeqd xmm3, xmm3
 ; psllw   xmm3, 15              ; rangeshift_mask = set1(0x8000)

 ; movdqa  xmm5, xmm4
 ; psrlw   xmm5, 5               ; fives = set1(5)

 ; pxor    xmm4, xmm3            ; input_sat_cutoff = set1(0x80b5)

 ;; probably not worth it since we already need to load 1 from memory.

Я проверил это, и paddusw делает 0x2 + 0xffff = 0xffff например.

Мы могли бы просто POR окончательный результат с 0 или 0xffff, чтобы оставить его неизменным или установить 0xffff, но изменив ввод до последнего paddusw создает больше параллелизма на уровне команд за одну итерацию. Таким образом, выполнение вне порядка не должно перекрывать столько независимых итераций, чтобы скрыть задержку тела цикла. (Если бы мы действительно планировали это для Atom или P5 Pentium-MMX по порядку, мы бы чередовали больше из двух цепочек dep.)


На самом деле, смещение вправо на 1 сработало бы: нам нужно только сравнение, чтобы поймать входные данные, настолько большие, что умножение оборачивается до небольшого результата. 0xb6 * 0xb6 не оборачивается, поэтому он сам по себе просто насыщается paddubsw,

Хорошо, если мы проверим (x>>1) > (0xb6>>1) с pcmpgtw (вместо >=) чтобы поймать входы как 0xffff (где pmullw с 0xffff дает нам 0x0001). Таким образом, мы могли бы сохранить 1 векторную константу, но в остальном это не лучше.

pxor + pcmpgtw по крайней мере так же дешево, как psrlw xmm, 1 + pcmpgtw, если только мы не настраиваемся на семейство Intel P6 (Core2/Nehalem) и работаем в стойках регистрации-чтения-порта. Но это маловероятно: xmm0, xmm1 и rsi всегда должны быть горячими (недавно записанными и, таким образом, прочитанными из ROB, а не из файла постоянного реестра). Мы читаем только 2 векторных константы в первой группе из 4 инструкций в цикле, а затем 1 позже.

Как я скажу ниже, на многих процессорах Intel, psrlw может работать только на том же порту, что и pmullw, на vec-int shift+ умножить модуль выполнения. Вероятно, здесь не узкое место в пропускной способности.

Но pcmp а также padd работать на ограниченных портах (на Intel до Skylake), пока pxor может работать на любом векторном порте АЛУ. Смесь чисто padd / pcmp / pmul / psrl` uops оставил бы один векторный порт ALU неиспользованным.


Альтернативная идея проверки насыщенности

(Я написал эту часть, забывая о *2 в формуле, когда искал самый высокий ввод, который не переполняется.)

Если бы формула была (0x00ff)^2 + 5 проверка насыщенности будет проще.

Мы могли бы просто проверить битовые позиции.

  • (0x00ff)^2 + 5 = 0xfe06 который умещается в 16 бит
  • (0x0100)^2 + 5 = 0x10005 который не делает, и мы хотим, чтобы он был насыщенным 0xffff

Таким образом, мы должны проверить, что все старшие 16 бит равны нулю, или что x&0xFF == x, или это x>>8 == 0,

Это требует меньше констант, но на самом деле это хуже, чем смещение диапазона для подписи с PXOR, потому что на некоторых процессорах исполнительные блоки с векторным смещением и умножением вектора находятся на одном и том же порту. (И поэтому psrlw а также pmullw конкурировать друг с другом за пропускную способность. Всего этого достаточно, чтобы не было узкого места в порту 0 на Nehalem / Sandybridge / Haswell, но это не повредит.)

    lea      rcx, [rsi + length]      ; rcx = end_pointer
    movq     xmm5, [fives]
    punpcklqdq  xmm5, xmm5            ; or with SSE3, movddup xmm5, [fives] to broadcast-load
    pxor     xmm4, xmm4               ; xmm4 = 0
.loop:
    movdqu   xmm0, [rsi]
    movdqa   xmm1, xmm0
           ; if  x>0xffU, i.e. if the high byte >0, saturate output to 0xffff
    psrlw    xmm1, 8         ; x>>8  (logical right shift)
    pcmpgtw  xmm1, xmm4      ; xmm1 = ((x>>8) > 0)  ?  -1  :  0

    pmullw   xmm0, xmm0      ; x*x      ; does NOT saturate.  will wrap.
    paddusw  xmm0, xmm0      ; *= 2    ; with unsigned saturation

    por      xmm1, xmm5      ; 0xffff (saturation needed) or 5 (normal).  Off the critical path to give OoO exec an easier time.
    paddusw  xmm0, xmm1      ; += 5  or  += 0xffff  with unsigned saturation.
    movdqu   [rsi], xmm0
    add      rsi, 16
    cmp      rsi, rcx
    jb      .loop

С AVX512BW (Skylake-X) для сравнения без знака с использованием регистров маски

Наконец, мы можем сравнивать целое число без знака с AVX512F, а по размеру элемента word - с AVX512BW. Но результат находится в регистре маски вместо вектора, поэтому мы не можем просто vpor это с вектором set1(5) создать вход для насыщения добавить.

Вместо этого мы можем смешать между вектором 5 а также 0xffff по маске сравнения.

;; AVX512BW version

;; On a Skylake Xeon you probably only want to use YMM regs unless you spend a lot of time in this
;;  to avoid reducing max turbo much.
;; Even with xmm or ymm regs (AVX512VL + BW), this demonstrates
;; that we gain even more efficiency than just widening the vectors

;; Just having non-destructive AVX encodings saves the `movdqa xmm1,xmm0` in the SSE2 version.
;; With YMM or XMM regs, most of these instructions can still use shorter VEX encoding (AVX), not the longer EVEX (AVX512)
;;  (Use vmovdqa/u instead of vmovdqu64.  The 64 is element-size, irrelevant when not masking.)

;;;;;;;;;;; UNTESTED ;;;;;;;;;;;;;;;;;

    mov       eax, 0xb5          ;; materialize vector constants from an immediate
    vpbroadcastd  zmm4, eax       ; largest input that won't overflow/saturate
    vpsrlw        zmm5, zmm4, 5   ; fives = 0xb5 >> 5  = 5

    ;vpcmpeqd     xmm3, xmm3            ; all-ones: result on saturation
    vpternlogd    zmm3,zmm3,zmm3, 0xff  ; alternative for ZMM regs, where there's no compare-into-vector

.loop:
    ; alignment recommended for 512-bit vectors, but `u` lets it still work (slower) on unaligned.
    vmovdqu64  zmm0, [rsi]

        ;; if  x>0xb5 (unsigned), saturate output to 0xffff
    vpcmpuw    k1, zmm0, zmm4, 2   ; k1 = x <= 0xb5.   2 = LE predicate
    ; k1 set for elements that WON'T overflow

    vpmullw    zmm0, zmm0      ; x*x
    vpaddusw   zmm0, zmm0      ; *= 2    ; saturating add or not doesn't matter here

    vmovdqa64  zmm1, zmm3               ; 0xffff
    vpaddusw   zmm1{k1}, zmm0, zmm5     ; 0xffff   or  2*x^2 + 5  merge masking

    vmovdqu64  [rsi], zmm1
    add      rsi, 64
    cmp      rsi, rcx
    jb      .loop

(NASM позволяет vpmullw a, b как ярлык для vpaddusw a, a, b когда вы не хотите использовать неразрушающее целевое 3-операндное кодирование, как это делается для imul eax, 123.)


Более ранняя идея применения насыщенности заключалась в использовании vpblendmw выбирать между векторами 5 а также 0xffff по маске.

vpcmpuw   k1, xmm4, xmm0, 1   ; k1 = 0xb5<x = x>0xb5.   1 = LT predicate numerically because NASM doesn't seem to accept vpcmpltuw the way it accepts vpcmpeqb
; k1 = 1 for elements that WILL overflow.

multiply and add as usual
...

vpblendmw xmm1{k1}, xmm5, xmm3    ; select (k1==0) ? 5 : 0xffff
vpaddusw  xmm0, xmm1              ; += 5  or  += 0xffff  with unsigned saturation.

Копирование регистра по-прежнему занимает входную операцию, но не внутреннюю операцию ALU. Так (особенно для 512-битных регистров, где порт 1 отключается для векторных мопов на SKX), это vpblendmb Идея хуже, чем копировать и слить маску.

Кроме того, МАКА считает, vpblendmw xmm1{k1}, xmm3, xmm5 имеет выходную зависимость от XMM1, хотя на самом деле это только запись. (Я проверил, поместив 8 из этого в цикле, с / без разрыва vpxor). Инструкции смешивания маски представляют собой особый случай: для неустановленных битов маски означает, что он копирует из src1 (или ноль для маскирования нуля), а для установленных битов маски он копирует из src2.

Но машинное кодирование использует маскирование слиянием, поэтому возможно, что HW будет рассматривать его как любую другую операцию ALU с маскированием слиянием. (Где выходной вектор является третьей входной зависимостью, vpaddw xmm1{k1}, xmm2, xmm3: если маска имеет какие-либо нули, результатом в XMM1 будет входное значение этого элемента.)

Вероятно, это не проблема: в соответствии с IACA, SKX может выполнять это за одну итерацию в течение 2,24 циклов (узкое место на внешнем интерфейсе), так что перенос цепи через XMM1 не является проблемой, если только 1 задержка цикла (При развертывании с целью уменьшения узких мест в верхней / внешней части цикла вы должны использовать отдельный вектор для мест назначения смешивания, чтобы разделить итерации, даже если вы не можете получить его где-либо рядом с 1 циклом за итерацию.)

(И версия с использованием копирования + слияния-маскирования в вектор 0xffff также работает с такой пропускной способностью, даже для векторов ZMM. Но IACA считает, что версия vpblendmb будет медленнее с ZMM, даже несмотря на то, что на переднем конце написано, что оба узких места...)

Итак, я нашел решение. Инструкция по использованию pmullw, инструкция pmullw mm0, mm1 рассчитает произведение 4 слов в регистрах по порядку и сохранит их в mm0, И для printf проблема, я просто нажал другой регистр, rdx раньше звонил и теперь работает. Я предполагаю, что это как-то связано с упомянутым смещением стека. Если кто-то может объяснить мне более подробно, как это работает, было бы здорово.

Другие вопросы по тегам