сводка : 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.