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

4 ответа

Почему это медленно
Причина использования 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

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

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

Я думаю, что для получения дополнительной информации о том, почему одна версия быстрее другой, требуется более внимательно прочитать исходный материал и / или использовать что-то вроде 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

Полный выходной файл.

Результаты:

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

Резюме: 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 для детали.)


Ни один из партиалов частичного-reg не является частью длинной цепочки зависимостей, поскольку объединенный регистр перезаписывается в следующей итерации. Очевидно, что 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, который ОП разливает в стек. user786653-х triAsm4 делает это, за исключением того, что он использует безумную вариацию логики тролля: он создает кадр стека и устанавливает ebp как обычно, но потом заначки esp в статическом месте и использует его как регистр нуля!! Очевидно, что это ужасно сломается, если в вашей программе есть какие-либо обработчики сигналов, но в остальном все в порядке (за исключением усложнения отладки).

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

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

Сравнение с указателем конца с cmp / jb на самом деле более оптимально на Core2, чем dec / jge тем не мение. Неподписанные условия могут слиться с cmp, До снб, только 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, Это позволило бы сохранить частичную регистрацию в eax, но сгенерировать ее для объединения bh в ebx так что это имеет ограниченную ценность. Возможно, 4B запись и два перекрывающихся 1B магазина действительно хороши здесь, но это начинает быть большим количеством магазинов. Тем не менее, это может быть распространено на достаточно других инструкций, чтобы не стать узким местом в порту магазина.

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

; Использование определяет, вы можете поместить [] вокруг, так что ясно, что они ссылки на память; %define cr  ebp+0x10
% определить cr  esp+ что-то, что зависит от того, сколько мы нажали
%define dcr ebp+0x1c;; измените их на работу с ebp тоже.
% определить dcg ebp+0x20
%define dcb ebp+0x24; esp-относительные смещения могут быть неправильными, просто быстро сделал это в моей голове без тестирования:; мы добавляем еще 3 regs после ebp, который был моментом, когда моментальные снимки ebp esp в версии стекового фрейма. Так что добавьте 0xc (т.е. умственно добавьте 0x10 и вычтите 4); 32-битный код в любом случае глуп.  64bit передает аргументы в рег.

%define dest_arg  esp+14
%define cnt_arg   esp+18
... все остальное

tri_pjc:
    толчок
    толчок
    Push ESI
    толчок отлив; только эти 4 должны быть сохранены в обычном 32-битном соглашении о вызовах

    мов ебп, [кр]
    мов эси, [cg]
    мов эди, [cb]

    шл еси,   8; поместите нужные биты по верхнему краю, чтобы нам не приходилось маскироваться после смещения в нули
    зы [dcg], 8
    шл эди, 8
    ЗЫ [ДЦБ], 8; очевидно, исходный код не заботится о переполнении cr в верхнем байте.

    MOV EDX, [Dest_arg]
    mov     ecx, [cnt_arg]
    Lea Ecx, [edx + ecx*4]; один за концом, который будет использоваться в качестве границы цикла
    mov    [dest_arg], ecx; пролить его обратно в стек, где нам нужно только прочитать его.

ALIGN 16
.loop:; СМОТРИТЕ НИЖЕ, этот внутренний цикл может быть еще более оптимизирован
    добавить esi, [dcg]
    Mov Eax, ESI
    шрэакс, 24; eax bytes = { 0  0  0 cg }

    добавить edi, [dcb]
    ШЛД ЭАКС, ЭДИ, 8; eax bytes = { 0  0 cg cb }

    добавить ebp, [dcr]
    MOV ECX, EBP
    и ecx, 0xffff0000
    или eax, ecx; eax bytes = { x cr cg cb} где x - переполнение от cr. Убей это, изменив маску на 0x00ff0000; другое объединение может быть быстрее на других процессорах, но не на core2; объединение с mov cx, ax также возможно на процессорах, где это дешево (AMD, Intel IvB и более поздние версии)

    mov    DWORD [edx], eax; альтернативно:; mov    DWORD [edx], ebp; MOV WORD [edx], eax; этот insn заменяет mov/ и / или слияние

    добавить edx, 4 cmp     edx, [dest_arg]; core2 может макро-предохранитель cmp/unsigned условие, но не подписано
    jb .loop

    поп прилив
    поп еси
    поп-эди
    поп ебп
    RET

В итоге я получил еще один регистр, чем мне было нужно, выполнив указатель omit-frame и поместив границу цикла в память. Вы можете либо кэшировать что-то дополнительное в регистрах, либо избежать сохранения / восстановления регистра. Может быть, сохраняя границу цикла в 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 mops-per-insn. SnB и позднее перепроектировали декодеры, и добавил кэш UOP, который делает декодирование обычно не узким местом и может обрабатывать только группы 1-1-1-1, 2-1-1, 3-1 и 4.)

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

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

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

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


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

ALIGN 16; mov ebx, 111; IACA начало; дБ 0x64, 0x67, 0x90
.loop:
    добавить ebp, [dcr]
    Мов Эакс, ЭБП
    шрекс, 16; eax bytes = { 0  0  x cr}, где x - переполнение от cr. Убейте этих предварительно смещенных cr и dcr, как и другие, и используйте shr 24 здесь

    добавить esi, [dcg]
    Shld Eax, ESI, 8; eax bytes = { 0  x cr cg}
    добавить edx, 4; это идет между `shld`s ', чтобы помочь с пропускной способностью декодера на pre-SnB, и не нарушать макрослияние.
    добавить edi, [dcb]
    ШЛД ЭАКС, ЭДИ, 8; eax bytes = { x cr cg cb}
    mov    DWORD [edx-4], eax

    cmp     edx, ebx; используйте наш запасной регистр здесь
    jb .loop; core2 может макро-предохранитель cmp/unsigned условие, но не подписано.  Macro-fusion работает в 32-битном режиме только на Core2.;mov ebx, 222; Конец IACA; дБ 0x64, 0x67, 0x90

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

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

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

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

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

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

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

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

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

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

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