Почему mov ah, bh и mov al, bl вместе намного быстрее, чем одиночная инструкция mov ax, bx? - PullRequest
18 голосов
/ 11 августа 2011

Я обнаружил, что

mov al, bl
mov ah, bh

намного быстрее, чем

mov ax, bx

Может кто-нибудь объяснить мне, почему? Я работаю на Core 2 Duo 3 Ghz, в 32-битном режиме под Windows XP. Компиляция с использованием NASM, а затем связь с VS2010. Команда компиляции Nasm:

nasm -f coff -o triangle.o triangle.asm

Вот основной цикл, который я использую для визуализации треугольника:

; some variables on stack
%define cr  DWORD [ebp-20]
%define dcr DWORD [ebp-24]
%define dcg DWORD [ebp-32]
%define dcb DWORD [ebp-40]

loop:

add esi, dcg
mov eax, esi
shr eax, 8

add edi, dcb
mov ebx, edi
shr ebx, 16
mov bh, ah

mov eax, cr
add eax, dcr
mov cr, eax

mov ah, bh  ; faster
mov al, bl
;mov ax, bx

mov DWORD [edx], eax

add edx, 4

dec ecx
jge loop

Я могу предоставить весь проект VS с источниками для тестирования.

Ответы [ 4 ]

10 голосов
/ 03 октября 2011

Почему это медленно
Причина, по которой использование 16-разрядного регистра является дорогой, в отличие от использования 8-разрядного регистра, заключается в том, что инструкции 16-разрядного регистра декодируются в микрокоде.Это означает дополнительный цикл во время декодирования и невозможность сопряжения во время декодирования.
Кроме того, поскольку ax является частичным регистром, для его выполнения потребуется дополнительный цикл, потому что верхняя часть регистра должна быть объединена с записью в нижний регистр.part.
8-битные записи имеют специальное аппаратное обеспечение, чтобы ускорить это, но 16-битные записи - нет.Опять же на многих процессорах 16-битные инструкции занимают 2 цикла вместо одного, и они не допускают спаривания.

Это означает, что вместо того, чтобы обрабатывать 12 команд (по 3 на цикл) в 4 циклах, теперь вы можете выполнять только 1, потому что у вас есть остановка при декодировании инструкции в микрокод и остановка при обработкемикрокод.

Как я могу сделать это быстрее?

mov al, bl
mov ah, bh

(Этот код занимает минимум 2 цикла ЦП и может привести к остановкевторая инструкция, потому что на некоторых (более старых) процессорах x86 вы получаете блокировку EAX)
Вот что происходит:

  • EAX читается. (цикл 1)
    • Меняется младший байт EAX (цикл по-прежнему 1)
    • и полное значение записывается обратно в EAX. (цикл 1)
  • EAX заблокирован для записи, пока первая запись не будет полностью разрешена. (потенциальное ожидание нескольких циклов)
  • Процесс повторяется для старшего байта в EAX. (цикл 2)

На последних процессорах Core2 это не такая большая проблема, потому что было установлено дополнительное оборудование, которое знает, что bl и bh на самом деле никогда не мешать друг другу.

mov eax, ebx

При перемещении 4 байтов за раз эта отдельная команда будет выполняться в 1 цикле процессора (и может быть в паре с другими инструкциями параллельно).

  • Если вам нужен быстрый код, всегда используйте 32-битные (EAX, EBX и т. Д.) регистры.
  • Старайтесь избегать использования 8-битных подрегистров, если вам не нужно.
  • Никогда не используйте 16-битные регистры.Даже если вам нужно использовать 5 инструкций в 32-битном режиме, это все равно будет быстрее.
  • Используйте инструкции movzx reg, ... (или movsx reg, ...)

Ускорение кода
Я вижу несколько возможностей ускорить код.

; some variables on stack
%define cr  DWORD [ebp-20]
%define dcr DWORD [ebp-24]
%define dcg DWORD [ebp-32]
%define dcb DWORD [ebp-40]

mov edx,cr

loop:

add esi, dcg
mov eax, esi
shr eax, 8

add edi, dcb
mov ebx, edi
shr ebx, 16   ;higher 16 bits in ebx will be empty.
mov bh, ah

;mov eax, cr   
;add eax, dcr
;mov cr, eax

add edx,dcr
mov eax,edx

and eax,0xFFFF0000  ; clear lower 16 bits in EAX
or eax,ebx          ; merge the two. 
;mov ah, bh  ; faster
;mov al, bl


mov DWORD [epb+offset+ecx*4], eax ; requires storing the data in reverse order. 
;add edx, 4

sub ecx,1  ;dec ecx does not change the carry flag, which can cause
           ;a false dependency on previous instructions which do change CF    
jge loop
8 голосов
/ 12 августа 2011

Это также быстрее на моем процессоре Core 2 Duo L9300 1,60 ГГц. Как я уже писал в комментарии, я думаю, что это связано с использованием частичных регистров (ah, al, ax). Смотрите больше, например здесь , здесь и здесь (стр. 88).

Я написал небольшой набор тестов, чтобы попытаться улучшить код, и хотя версия ax, представленная в OP, не является самой умной, попытка устранить частичное использование регистров действительно улучшает скорость (даже больше). так, чем моя быстрая попытка освободить еще один регистр).

Чтобы получить больше информации о том, почему одна версия быстрее другой, я думаю, что нужно более внимательно прочитать исходный материал и / или использовать что-то вроде Intel VTune или AMD CodeAnalyst. (Может оказаться, что я ошибаюсь)

ОБНОВЛЕНИЕ, хотя приведенный ниже вывод oprofile ничего не доказывает, он показывает, что в обеих версиях происходит много частичных остановок регистров, но примерно в два раза больше в самой медленной версии (triAsm2), чем в «быстрой». версия (triAsm1).

$ opreport -l test                            
CPU: Core 2, speed 1600 MHz (estimated)
Counted CPU_CLK_UNHALTED events (Clock cycles when not halted) with a unit mask of 0x00 (Unhalted core cycles) count 800500
Counted RAT_STALLS events (Partial register stall cycles) with a unit mask of 0x0f (All RAT) count 1000000
samples  %        samples  %        symbol name
21039    27.3767  10627    52.3885  triAsm2.loop
16125    20.9824  4815     23.7368  triC
14439    18.7885  4828     23.8008  triAsm1.loop
12557    16.3396  0              0  triAsm3.loop
12161    15.8243  8         0.0394  triAsm4.loop

Полный вывод oprofile .

Результаты:

triC: 7410.000000 мс, a5afb9 (реализация кода asm C)

triAsm1: 6690.000000 мс, a5afb9 (код из OP, с использованием al и ah)

triAsm2: 9290,000000 мс, a5afb9 (код из OP, с использованием ax)

triAsm3: 5760,000000 мс, a5afb9 (прямой перевод кода OP в код без частичного использования регистра)

triAsm4: 5640,000000 мс, a5afb9 (быстрая попытка сделать это быстрее)

Вот мой набор тестов, скомпилированный с -std=c99 -ggdb -m32 -O3 -march=native -mtune=native:

test.c:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <time.h>

extern void triC(uint32_t* dest, uint32_t cnt, uint32_t cr, uint32_t cg, uint32_t cb, uint32_t dcr, uint32_t dcg, uint32_t dcb);
extern void triAsm1(uint32_t* dest, uint32_t cnt, uint32_t cr, uint32_t cg, uint32_t cb, uint32_t dcr, uint32_t dcg, uint32_t dcb);
extern void triAsm2(uint32_t* dest, uint32_t cnt, uint32_t cr, uint32_t cg, uint32_t cb, uint32_t dcr, uint32_t dcg, uint32_t dcb);
extern void triAsm3(uint32_t* dest, uint32_t cnt, uint32_t cr, uint32_t cg, uint32_t cb, uint32_t dcr, uint32_t dcg, uint32_t dcb);
extern void triAsm4(uint32_t* dest, uint32_t cnt, uint32_t cr, uint32_t cg, uint32_t cb, uint32_t dcr, uint32_t dcg, uint32_t dcb);

uint32_t scanline[640];

#define test(tri) \
    {\
        clock_t start = clock();\
        srand(60);\
        for (int i = 0; i < 5000000; i++) {\
            tri(scanline, rand() % 640, 10<<16, 20<<16, 30<<16, 1<<14, 1<<14, 1<<14);\
        }\
        printf(#tri ": %f ms, %x\n",(clock()-start)*1000.0/CLOCKS_PER_SEC,scanline[620]);\
    }

int main() {
    test(triC);
    test(triAsm1);
    test(triAsm2);
    test(triAsm3);
    test(triAsm4);
    return 0;
}

tri.c:

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

void triC(uint32_t* dest, uint32_t cnt, uint32_t cr, uint32_t cg, uint32_t cb, uint32_t dcr, uint32_t dcg, uint32_t dcb) {
    while (cnt--) {
        cr += dcr;
        cg += dcg;
        cb += dcb;
        *dest++ = (cr & 0xffff0000) | ((cg >> 8) & 0xff00) | ((cb >> 16) & 0xff);
    }
}

atri.asm:

    bits 32
    section .text
    global triAsm1
    global triAsm2
    global triAsm3
    global triAsm4

%define cr DWORD [ebp+0x10]
%define dcr DWORD [ebp+0x1c]
%define dcg DWORD [ebp+0x20]
%define dcb DWORD [ebp+0x24]

triAsm1:
    push ebp
    mov ebp, esp

    pusha

    mov edx, [ebp+0x08] ; dest
    mov ecx, [ebp+0x0c] ; cnt
    mov esi, [ebp+0x14] ; cg
    mov edi, [ebp+0x18] ; cb

.loop:

    add esi, dcg
    mov eax, esi
    shr eax, 8

    add edi, dcb
    mov ebx, edi
    shr ebx, 16
    mov bh, ah

    mov eax, cr
    add eax, dcr
    mov cr, eax

    mov ah, bh  ; faster
    mov al, bl

    mov DWORD [edx], eax

    add edx, 4

    dec ecx
    jge .loop

    popa

    pop ebp
    ret


triAsm2:
    push ebp
    mov ebp, esp

    pusha

    mov edx, [ebp+0x08] ; dest
    mov ecx, [ebp+0x0c] ; cnt
    mov esi, [ebp+0x14] ; cg
    mov edi, [ebp+0x18] ; cb

.loop:

    add esi, dcg
    mov eax, esi
    shr eax, 8

    add edi, dcb
    mov ebx, edi
    shr ebx, 16
    mov bh, ah

    mov eax, cr
    add eax, dcr
    mov cr, eax

    mov ax, bx ; slower

    mov DWORD [edx], eax

    add edx, 4

    dec ecx
    jge .loop

    popa

    pop ebp
    ret

triAsm3:
    push ebp
    mov ebp, esp

    pusha

    mov edx, [ebp+0x08] ; dest
    mov ecx, [ebp+0x0c] ; cnt
    mov esi, [ebp+0x14] ; cg
    mov edi, [ebp+0x18] ; cb

.loop:
    mov eax, cr
    add eax, dcr
    mov cr, eax

    and eax, 0xffff0000

    add esi, dcg
    mov ebx, esi
    shr ebx, 8
    and ebx, 0x0000ff00
    or eax, ebx

    add edi, dcb
    mov ebx, edi
    shr ebx, 16
    and ebx, 0x000000ff
    or eax, ebx

    mov DWORD [edx], eax

    add edx, 4

    dec ecx
    jge .loop

    popa

    pop ebp
    ret

triAsm4:
    push ebp
    mov ebp, esp

    pusha

    mov [stackptr], esp

    mov edi, [ebp+0x08] ; dest
    mov ecx, [ebp+0x0c] ; cnt
    mov edx, [ebp+0x10] ; cr
    mov esi, [ebp+0x14] ; cg
    mov esp, [ebp+0x18] ; cb

.loop:
    add edx, dcr
    add esi, dcg
    add esp, dcb

    ;*dest++ = (cr & 0xffff0000) | ((cg >> 8) & 0xff00) | ((cb >> 16) & 0xff);
    mov eax, edx ; eax=cr
    and eax, 0xffff0000

    mov ebx, esi ; ebx=cg
    shr ebx, 8
    and ebx, 0xff00
    or eax, ebx
    ;mov ah, bh

    mov ebx, esp
    shr ebx, 16
    and ebx, 0xff
    or eax, ebx
    ;mov al, bl

    mov DWORD [edi], eax
    add edi, 4

    dec ecx
    jge .loop

    mov esp, [stackptr]

    popa

    pop ebp
    ret

    section .data
stackptr: dd 0
6 голосов
/ 03 февраля 2016

сводка : 16-битные инструкции не являются проблемой напрямую. Проблема заключается в чтении более широких регистров после записи частичных регистров, что приводит к остановке частичных регистров в Core2. Это намного меньше проблем на Sandybridge и позже, так как они объединяются гораздо дешевле. mov ax, bx вызывает дополнительное слияние, но даже в «быстрой» версии ОП есть некоторые ларьки.

См. Конец этого ответа для альтернативного скалярного внутреннего цикла, который должен быть быстрее, чем два других ответа, используя shld для перестановки байтов между регистрами. Предварительное смещение, оставленное 8b вне цикла, помещает нужный нам байт в верхнюю часть каждого регистра, что делает его действительно дешевым. Он должен работать немного лучше, чем одна итерация за 4 такта на 32-битном ядре 2, и насыщать все три исполнительных порта без остановок. Он должен выполняться на одной итерации на 2.5c в Haswell.

Чтобы на самом деле сделать это быстро, посмотрите на вывод векторизованного компилятором с авто-векторизацией, и, возможно, сократите это или повторно внедрите с помощью векторных встроенных функций.


Вопреки утверждениям о медленных инструкциях размером в 16 бит, Core2 теоретически может выдерживать 3 insns за такт, чередуя mov ax, bx и mov ecx, edx. Здесь нет никакого «переключателя режима». (Как уже отмечалось, «переключение контекста» - ужасный выбор вымышленного имени, поскольку оно уже имеет конкретное техническое значение.)

Проблема заключается в частичной остановке регистра, когда вы читаете регистр, для которого ранее вы писали только часть. Вместо принудительной записи в ax ожидание готовности старого содержимого eax (ложная зависимость), процессоры семейства Intel P6 отслеживают зависимости для частичных регистров отдельно. Чтение более широкого регистра приводит к слиянию, которое останавливается на 2-3 цикла в соответствии с Agner Fog . Другая большая проблема с использованием размера 16-битных операндов связана с непосредственными операндами, где вы можете получить возможность LCP зависать в декодерах на процессорах Intel для немедленных, которые не вписываются в imm8.

Семейство SnB гораздо эффективнее, просто вставив дополнительный моп, чтобы выполнить слияние без остановки, пока оно так делает. AMD и Intel Silvermont (и P4) вообще не переименовывают частичные регистры, поэтому они имеют «ложные» зависимости от предыдущего содержимого. В этом случае мы позже читаем полный регистр, так что это настоящая зависимость, потому что мы хотим слияния, чтобы у этих процессоров было преимущество. (Intel Haswell / Skylake (и, возможно, IvB) не переименовывают AL отдельно от RAX; они переименовывают только AH / BH / CH / DH по отдельности. А чтение регистров high8 имеет дополнительную задержку. Подробнее см. в этом разделе вопросов и ответов о частичных регистрах в HSW / SKL .)


Ни одна из остановок частичных регистров не является частью длинной цепочки зависимостей, поскольку объединенный регистр перезаписывается на следующей итерации. Очевидно, что Core2 просто останавливает внешний интерфейс или даже неработающее ядро? Я хотел задать вопрос о том, насколько дорогостоящим является частичное замедление регистра в Core2, и как измерить стоимость в SnB. Ответ oprofile @ user786653 проливает свет на это. (А также имеет несколько действительно полезных C, переработанных из ассемблера OP, чтобы прояснить, чего на самом деле пытается эта функция).

Компиляция этого C с современным gcc может создать векторизованный asm, который выполняет цикл по 4 слова одновременно, в регистре xmm. Тем не менее, он работает намного лучше, когда может использовать SSE4.1. (И clang вообще не автоматически векторизует это с -march=core2, но он разворачивает много, вероятно, чередуя несколько итераций, чтобы избежать частичной регистрации.) Если вы не скажете gcc, что dest выровнено он генерирует огромное количество скалярного пролога / эпилога вокруг векторизованной петли, чтобы достичь точки, где он выровнен.

Превращает целочисленные аргументы в векторные константы (в стеке, поскольку 32-битный код имеет только 8 векторных регистров). Внутренний цикл

.L4:
        movdqa  xmm0, XMMWORD PTR [esp+64]
        mov     ecx, edx
        add     edx, 1
        sal     ecx, 4
        paddd   xmm0, xmm3
        paddd   xmm3, XMMWORD PTR [esp+16]
        psrld   xmm0, 8
        movdqa  xmm1, xmm0
        movdqa  xmm0, XMMWORD PTR [esp+80]
        pand    xmm1, xmm7
        paddd   xmm0, xmm2
        paddd   xmm2, XMMWORD PTR [esp+32]
        psrld   xmm0, 16
        pand    xmm0, xmm6
        por     xmm0, xmm1
        movdqa  xmm1, XMMWORD PTR [esp+48]
        paddd   xmm1, xmm4
        paddd   xmm4, XMMWORD PTR [esp]
        pand    xmm1, xmm5
        por     xmm0, xmm1
        movaps  XMMWORD PTR [eax+ecx], xmm0
        cmp     ebp, edx
        ja      .L4

Обратите внимание, что во всем цикле есть один магазин.Все нагрузки являются только векторами, которые он рассчитал ранее и хранятся в стеке как локальные.


Существует несколько способов ускорить код ОП .Наиболее очевидным является то, что нам не нужно создавать кадр стека, освобождая ebp.Наиболее очевидное использование для этого - удерживать cr, который OP выливает в стек.user786653 triAsm4 делает это, за исключением того, что он использует безумную вариацию логики тролля: он создает кадр стека и устанавливает ebp как обычно, но затем сохраняет esp в статическом месте и использует его как регистр нуля!!Это, очевидно, ужасно сломается, если ваша программа имеет какие-либо обработчики сигналов, но в остальном все в порядке (за исключением усложнения отладки).

Если вы собираетесь сойти с ума настолько, что захотите использовать esp в качествепоцарапайте, скопируйте аргументы функции в статические местоположения, так что вам не нужен регистр для хранения указателей на стек памяти.(Сохранение старого esp в регистре MMX также является опцией, так что вы можете сделать это в реентерабельных функциях, используемых одновременно из нескольких потоков. Но не в том случае, если вы копируете аргументы куда-то статически, если только это не в локальное хранилищес переопределением сегмента или чем-то. Вам не нужно беспокоиться о повторном входе из одного и того же потока, потому что указатель стека находится в непригодном для использования состоянии. Что-то вроде обработчика сигнала, который может повторно ввести вашу функцию в том же потокевместо этого произойдет сбой.>. <) </p>

Разбивка cr на самом деле не самый оптимальный выбор: вместо использования двух регистров для цикла (счетчик и указатель) мы можем просто сохранить указатель dst в регистре.Сделайте границу цикла, вычислив указатель конца (один после конца: dst+4*cnt), и используйте cmp с операндом памяти в качестве условия цикла.

Сравнение с указателем конца с cmp / jb на самом деле более оптимален на Core2, чем dec / jge в любом случае.Беззнаковые условия могут слиться с макрокомандой cmp.До SnB только cmp и test могут слиться воедино.(Это также верно для AMD Bulldozer, но cmp и test могут совмещаться с любым jcc на AMD).Процессоры семейства SnB могут макро-предохранить dec / jge.Интересно, что Core2 может сравнивать только со знаком с макросом (например, jge) с test, а не cmp.(Сравнение без знака является правильным выбором для адреса, так как 0x8000000 не является особенным, но 0 есть. Я не использовал jb просто как рискованную оптимизацию.)


Мы не можем предварительно сдвинуть cb и dcb до младшего байта, потому что они должны поддерживать большую точность внутри.Тем не менее, мы можем влево сдвинуть другие два, чтобы они оказались напротив левого края их регистров.Если переместить их вправо вниз до их места назначения, это не оставит мусорных старших битов от возможного переполнения.

Вместо слияния с eax мы могли бы делать перекрывающиеся магазины.Сохраните 4B от eax, затем сохраните низкий 2B от bx.Это сохранит частичную задержку reg в eax, но сгенерирует единицу для объединения bh в ebx, так что это имеет ограниченную ценность.Возможно, 4B запись и два перекрывающихся 1B магазина действительно хороши здесь, но это начинает быть большим количеством магазинов.Тем не менее, он может быть распределен по достаточному количеству других инструкций, чтобы не создавать узких мест на порте хранилища. * Tri92m * user786653 пользователя использует маску и инструкции or для слияния, что выглядит как разумный подход для Core2.Для AMD, Silvermont или P4 использование команд mov 8b и 16b для объединения частичных регистров, вероятно, на самом деле хорошо.Вы также можете воспользоваться этим на Ivybridge / Haswell / Skylake, если вы пишете только low8 или low16, чтобы избежать штрафов за слияние.Тем не менее, я придумал несколько улучшений, которые требуют меньшего количества маскировки.

; use defines you can put [] around so it's clear they're memory refs
; %define cr  ebp+0x10
%define cr  esp+something that depends on how much we pushed
%define dcr ebp+0x1c  ;; change these to work from ebp, too.
%define dcg ebp+0x20
%define dcb ebp+0x24

; esp-relative offsets may be wrong, just quickly did it in my head without testing:
; we push 3 more regs after ebp, which was the point at which ebp snapshots esp in the stack-frame version.  So add 0xc (i.e. mentally add 0x10 and subract 4)
; 32bit code is dumb anyway.  64bit passes args in regs.

%define dest_arg  esp+14
%define cnt_arg   esp+18
... everything else

tri_pjc:
    push    ebp
    push    edi
    push    esi
    push    ebx  ; only these 4 need to be preserved in the normal 32bit calling convention

    mov     ebp, [cr]
    mov     esi, [cg]
    mov     edi, [cb]

    shl     esi,   8          ; put the bits we want at the high edge, so we don't have to mask after shifting in zeros
    shl     [dcg], 8
    shl     edi,   8
    shl     [dcb], 8
       ; apparently the original code doesn't care if cr overflows into the top byte.

    mov     edx, [dest_arg]
    mov     ecx, [cnt_arg]
    lea     ecx, [edx + ecx*4] ; one-past the end, to be used as a loop boundary
    mov    [dest_arg], ecx    ; spill it back to the stack, where we only need to read it.

ALIGN 16
.loop: ; SEE BELOW, this inner loop can be even more optimized
    add     esi, [dcg]
    mov     eax, esi
    shr     eax, 24           ; eax bytes = { 0  0  0 cg }

    add     edi, [dcb]
    shld    eax, edi, 8       ; eax bytes = { 0  0 cg cb }

    add     ebp, [dcr]
    mov     ecx, ebp
    and     ecx, 0xffff0000
    or      eax, ecx          ; eax bytes = { x cr cg cb}  where x is overflow from cr.  Kill that by changing the mask to 0x00ff0000
    ; another shld to merge might be faster on other CPUs, but not core2
    ; merging with mov cx, ax   would also be possible on CPUs where that's cheap (AMD, and Intel IvB and later)

    mov    DWORD [edx], eax
    ; alternatively:
    ; mov    DWORD [edx], ebp
    ; mov     WORD [edx], eax   ; this insn replaces the mov/and/or merging

    add     edx, 4
    cmp     edx, [dest_arg]   ; core2 can macro-fuse cmp/unsigned condition, but not signed
    jb .loop

    pop     ebx
    pop     esi
    pop     edi
    pop     ebp
    ret

Я получил еще один регистр, чем мне было нужно, после того, как я сделал omit-frame-pointer и поместил границу цикла в память. Вы можете либо кэшировать что-то дополнительное в регистрах, либо избежать сохранения / восстановления регистра. Возможно, лучше всего держать границу цикла в ebx. Это в основном сохраняет одну инструкцию пролога. Хранение dcb или dcg в реестре потребует дополнительного insn в прологе для его загрузки. (Изменения с назначением памяти уродливы и медленны, даже на Skylake, но небольшого размера кода. Они не находятся в цикле, и у core2 нет кэша UOP. Загрузка / Shift / Store отдельно по-прежнему 3 моп, так что вы не сможете победить его, если не будете хранить его в реестре вместо хранения.)

shld - это 2-х контактный insn на P6 (Core2). К счастью, легко заказать цикл, так что это пятая инструкция, которой предшествуют четыре инструкции по одной операции. Он должен поразить декодеры как первый моп во 2-й группе из 4, чтобы не вызывать задержки во внешнем интерфейсе. ( Core2 может декодировать шаблоны 1-1-1-1, 2-1-1-1, 3-1-1-1 или 4-1-1-1 uops-per-insn. SnB и позже перепроектировал декодеры, и добавил кэш UOP, который делает декодирование обычно не узким местом, и может обрабатывать только группы 1-1-1-1, 2-1-1, 3-1 и 4.)

shld - это ужасно для AMD K8, K10, семейства Bulldozer и Jaguar . 6 m-ops, задержка 3c и одна на пропускную способность 3c. Это здорово на Atom / Silvermont с 32-битным операндом, но ужасно с 16 или 64b регистрами.

Этот порядок insn может декодироваться с cmp как последним insn группы, а затем jb сам по себе, что делает его не макро-плавким предохранителем. Это может дать дополнительное преимущество для метода слияния перекрывающихся хранилищ, больше, чем просто сохранение uop, если внешние эффекты являются фактором для этого цикла. (И я подозреваю, что так и будет, учитывая высокую степень параллелизма и то, что переносимые по циклам цепочки dep короткие, поэтому работа с несколькими итерациями может выполняться одновременно.)

Итак: uops fused-domain за одну итерацию: 13 на Core2 (при условии, что слияние макросов может и не произойти), 12 на семейство SnB. Таким образом, IvB должен выполнять это за одну итерацию на 3c (при условии, что ни один из 3 портов ALU не является узким местом. mov r,r не нужны порты ALU, равно как и хранилище. add. 1119 * и shld - единственные, которые не могут работать с широким выбором портов, и есть только две смены за три цикла.) Core2 будет требовать 4c за итерацию, чтобы выдать его, даже если ему удастся избежать узких мест внешнего интерфейса и даже дольше его запускать.

Возможно, мы по-прежнему работаем достаточно быстро на Core2, чтобы разливать / перезагружать cr в стек каждую итерацию было бы узким местом, если бы мы все еще это делали. Он добавляет циклическое повторение памяти (5c) в цепочку зависимостей, переносимых циклами, в результате чего общая длина цепи депозита составляет 6 циклов (включая сложение).


Хм, на самом деле даже Core2 может выиграть от использования двух shld insns для слияния. Это также сохраняет другой регистр!

ALIGN 16
;mov ebx, 111           ; IACA start
;db 0x64, 0x67, 0x90
.loop:
    add     ebp, [dcr]
    mov     eax, ebp
    shr     eax, 16           ; eax bytes = { 0  0  x cr}  where x is overflow from cr.  Kill that pre-shifting cr and dcr like the others, and use shr 24 here

    add     esi, [dcg]
    shld    eax, esi, 8       ; eax bytes = { 0  x cr cg}
    add     edx, 4     ; this goes between the `shld`s to help with decoder throughput on pre-SnB, and to not break macro-fusion.
    add     edi, [dcb]
    shld    eax, edi, 8       ; eax bytes = { x cr cg cb}
    mov    DWORD [edx-4], eax

    cmp     edx, ebx      ; use our spare register here
    jb .loop     ; core2 can macro-fuse cmp/unsigned condition, but not signed.  Macro-fusion works in 32-bit mode only on Core2.

;mov ebx, 222           ; IACA end
;db 0x64, 0x67, 0x90

Повторение: SnB: 10 мопов слитых доменов. Core2: 12 мопов в слитых доменах, так что на короче, чем предыдущая версия для процессоров Intel (но ужасно для AMD). Использование shld сохраняет инструкции mov, поскольку мы можем использовать их для неразрушающего извлечения старшего байта источника.

Core2 может выдавать цикл за одну итерацию за 3 такта. (Это был первый процессор Intel с конвейером шириной 4 мегапикселя).

Из Таблица Агнера Фога для Мером / Конро (первый поколения Core2) (обратите внимание, что на блок-диаграмме Дэвида Кантера перевернуты p2 и p5):

  • shr: работает на p0 / p5
  • shld: 2 моп для р0 / р1 / р5? В таблице Агнера для пре-Хасвелла не сказано, куда и куда могут пойти мопы.
  • mov r,r, add, and: p0 / p1 / p5
  • слитый cmp-and-branch: p5
  • store: p3 и p4 (эти микроплавкие предохранители в 1 хранилище слитых доменов)
  • каждая нагрузка: р2. (все нагрузки микроплавлены с ALU ops в переплавленном домене).

Согласно IACA, в котором есть режим для Nehalem, но не для Core2, большинство shld мопов переходят на p1, и в среднем только менее 0,6 от каждого insn работает на других портах. Nehalem имеет по существу те же исполнительные блоки, что и Core2. Все приведенные здесь инструкции имеют одинаковую стоимость UOP и портовые требования для NHM и Core2. Анализ IACA выглядит хорошо для меня, и я не хочу проверять все самостоятельно для этого ответа на 5-летний вопрос. Хотя было весело отвечать. :)

В любом случае, согласно IACA, мопы должны хорошо распределяться между портами. Он полагает, что Nehalem может выполнять цикл в одну итерацию за 3,7 цикла, насыщая все три исполнительных порта. Это анализ выглядит хорошо для меня. (Обратите внимание, что мне пришлось удалить операнд памяти из cmp, чтобы IACA не давал глупых результатов.) В любом случае, это явно необходимо, поскольку pre-SnB может выполнять только одну загрузку за цикл: мы имеем узкое место на порте 2 с четырьмя загрузками. в цикле.

IACA не согласен с тестированием Agner Fog для IvB и SnB (он считает, что shld - это еще 2 мопа, хотя на самом деле он один, согласно моему тестированию на SnB). Так что его цифры глупы.

IACA выглядит правильно для Haswell, где говорится, что узким местом является внешний интерфейс. Он считает, что HSW может запустить его по одному на 2.5c. (Буфер циклов в Haswell, по крайней мере, может выдавать циклы с нецелым числом циклов на итерацию. Sandybridge может быть ограничен целым числом циклов, где взятая ветвь цикла заканчивает группу выпуска . )

Я также обнаружил, что мне нужно использовать iaca.sh -no_interiteration, иначе он мог бы подумать, что существует зависимость, переносимая циклом интерполяции, и подумать, что цикл будет принимать 12c для NHM.

3 голосов
/ 11 августа 2011

В 32-битном коде mov ax, bx требуется префикс размера операнда, тогда как перемещения байтового размера - нет.По-видимому, современные разработчики процессоров не тратят много сил на то, чтобы префикс размера операнда быстро декодировался, хотя меня удивляет, что штрафа будет достаточно, чтобы вместо этого сделать два перемещения размером в байт.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...