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

У меня есть проблема, когда мне нужно вычислить функцию 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 ]

0 голосов
/ 18 января 2019

Если вы заботитесь о производительности, избегайте инструкции 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://stackoverflow.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 продуктов.

В противном случае вам, вероятно, придется распаковать в dwords и 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*, и для этого не требуется меньше ненулевых векторных констант, так что это ни в коем случае не лучше. Но это одинаково хорошо на Скайлэйке.
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 + multiply. Вероятно, здесь нет узкого места в пропускной способности.

Но pcmp и padd работают на ограниченных портах (на Intel до Skylake), тогда как pxor может работать на любом векторном порте ALU. Сочетание чисто padd / pcmp / pmul / psrl` оставляет один векторный порт 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 идея хуже, чем маска копирования и слияния.

Кроме того, IACA считает, что 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, даже если она говорит, что оба узких места на внешний интерфейс ...)

0 голосов
/ 17 января 2019

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

...