Подтвердить что ты не робот

Почему mov ah, bh и mov al, bl вместе намного быстрее, чем одна команда mov ax, bx?

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

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 с источниками для тестирования.

4b9b3361

Ответ 1

Почему это медленное
Причина использования 16-битного регистра дорогая, в отличие от использования 8-битного регистра, заключается в том, что 16-битные инструкции регистра декодируются в микрокоде. Это означает дополнительный цикл во время декодирования и невозможность быть сопряженным во время декодирования.
Кроме того, поскольку ax является частичным регистром, потребуется выполнить дополнительный цикл, потому что верхняя часть регистра должна быть объединена с записью в нижнюю часть.
В 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

Ответ 2

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

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

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

UPDATE, в то время как приведенный ниже вывод 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 ms, a5afb9 (C реализация кода asm)

triAsm1: 6690.000000 ms, a5afb9 (код из OP, используя al и ah)

triAsm2: 9290.000000 ms, a5afb9 (код из OP, используя ax)

triAsm3: 5760.000000 ms, a5afb9 (Прямой перевод кода OPs в один без использования частичного регистра)

triAsm4: 5640.000000 ms, 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

Ответ 3

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

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

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


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

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

SnB-family намного эффективнее, просто вставляя лишний uop для слияния без остановки, пока он это делает. AMD и Intel Silvermont (и P4) не переименовывают частичные регистры отдельно, поэтому у них есть "ложные" зависимости от предыдущего содержимого. В этом случае мы позже читаем полный регистр, так что это истинная зависимость, потому что мы хотим слияния, поэтому у этих процессоров есть преимущество. (Intel Haswell/Skylake (и, возможно, IvB) не переименовывают AL отдельно от RAX, они только переименовывают AH/BH/CH/DH отдельно. И чтение регистров high8 имеет дополнительную задержку.  См. этот Q & A о частичных регистрах на HSW/SKL для деталей.)


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

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

Он превращает целые args в векторные константы (в стек, так как 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

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


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

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

Spilling 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 is. Я не использовал jb так же, как рискованную оптимизацию.)


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

Вместо слияния с eax мы могли бы делать перекрывающиеся хранилища. Сохраните 4B от eax, затем сохраните низкое значение 2B от bx. Это сэкономит партию с неполным регистром в eax, но сгенерирует одно для слияния bh в ebx, так что это ограниченное значение. Возможно, 4B-записи и два перекрывающихся магазина 1B на самом деле хороши здесь, но это начинает быть большим количеством магазинов. Тем не менее, это может быть распространено на достаточно других инструкциях, чтобы не быть узким местом на порту магазина.

user786653 triAsm3 использует маскировку и or инструкции для слияния, которые выглядят как разумный подход для Core2. Для AMD, Silvermont или P4 использование команд 8b и 16b mov для слияния неполных регистров, вероятно, действительно хорошо. Вы также можете воспользоваться им на Ivybridge/Haswell/Skylake, если вы пишете только low8 или low16, чтобы избежать слияния штрафов. Тем не менее, я придумал несколько улучшений по сравнению с тем, чтобы требовать меньше маскировки.

; use defines you can put [] around so it 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 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 и поместив границу цикла в память. Вы можете либо кэшировать что-то лишнее в регистрах, либо не сохранять/восстанавливать регистр. Возможно, наилучшим вариантом является сохранение границы цикла в ebx. Это в основном сохраняет одну прологовую инструкцию. Сохранение dcb или dcg в регистре потребует дополнительного insn в прологе для его загрузки. (Сдвиги с местом назначения памяти уродливы и медленны даже на Skylake, но небольшой размер кода. Они не находятся в цикле, а core2 не имеет кэша uop. Загрузка/смена/сохранение по-разному - поэтому вы не можете победить его, если вы не сохраните его в регистре, а не в хранилище.)

shld является 2-uop insn на P6 (Core2). К счастью, легко упорядочить петлю так, чтобы она была пятой инструкцией, которой предшествовали четыре команды с одним-юпом. Он должен поразить декодеры как первый uop во 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-family и Jaguar. 6 m-ops, 3c latency и 1 на 3c пропускную способность. Это замечательно на Atom/Silvermont с 32-разрядным размером операнда, но ужасно с 16 или 64b-регистрами.

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

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

Мы, возможно, все еще достаточно быстро работаем на 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-футовым конвейером).

Из Таблица Agner Fog для Merom/Conroe (первое поколение Core2) (обратите внимание, что блок-диаграмма Дэвида Кантера имеет p2 и p5 в обратном порядке):

  • shr: выполняется на p0/p5
  • shld: 2 uops для p0/p1/p5? Таблица Agner для pre-Haswell не говорит, к чему может идти uops.
  • mov r,r, add, and: p0/p1/p5
  • плавленый cmp-и-ветвь: p5
  • store: p3 и p4 (эти микро-предохранители в 1 хранилище с объединенными доменами)
  • каждая нагрузка: p2. (все нагрузки микроконфигурируются с операциями ALU в плавленном домене).

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

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

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

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

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

Ответ 4

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