Может ли аппаратное беззнаковое разделение 128-бит / 64-бит в некоторых случаях быстрее, чем 64-битное / 32-битное деление на процессорах Intel / AMD x86-64? - PullRequest
1 голос
/ 18 июня 2019

Может ли масштабированное 64-битное / 32-битное разделение, выполняемое инструкцией аппаратного 128-битного / 64-битного разделения, например:

; Entry arguments: Dividend in EAX, Divisor in EBX
shl rax, 32  ;Scale up the Dividend by 2^32
xor rdx,rdx
and rbx, 0xFFFFFFFF  ;Clear any garbage that might have been in the upper half of RBX
div rbx  ; RAX = RDX:RAX / RBX

... быть быстрее в некоторых особых случаях, чем масштабированное 64-битное / 32-битное разделение, выполняемоеаппаратная 64-битная / 32-битная инструкция деления, например:

; Entry arguments: Dividend in EAX, Divisor in EBX
mov edx,eax  ;Scale up the Dividend by 2^32
xor eax,eax
div ebx  ; EAX = EDX:EAX / EBX

Под «некоторыми особыми случаями» я подразумеваю необычные дивиденды и делители.Я заинтересован в сравнении только инструкции div.

Ответы [ 2 ]

5 голосов
/ 19 июня 2019

Вы спрашиваете об оптимизации деления uint64_t / uint64_t C на деление asm 64b / 32b => 32b x86, когда известно, что делитель 32-битный.Разумеется, компилятор должен избегать возможности исключения #DE при совершенно допустимом (в C) 64-разрядном делении, иначе он не следовал бы правилу «как будто».Так что он может сделать это только в том случае, если доказуемо, что частное будет соответствовать 32 битам.

Да, это выигрыш или, по крайней мере, безубыточность.На некоторых процессорах даже стоит проверить возможность во время выполнения, потому что 64-битное деление намного медленнее. Но, к сожалению, современные компиляторы x86 не имеют возможности оптимизатора для поиска этой оптимизации , даже если вам удается предоставить им достаточно информации, чтобы они могли доказать, что это безопасно.например, if (edx >= ebx) __builtin_unreachable(); не помогает в прошлый раз, когда я пытался.


Для тех же входов 32-битный размер операнда всегда будет по крайней мере таким же быстрым

16 или 8-бит может быть медленнее 32, потому что они могут иметь ложную зависимость, записывая свои выходные данные, но запись 32-битного регистра ноль расширяется до 64, чтобы избежать этого.(Вот почему mov ecx, ebx - это хороший способ расширения нуля от ebx до 64-битного, лучше, чем and значение, которое не кодируется как 32-битное расширение с немедленным расширением знака, как указывал Гарольд).Но кроме махинаций с частичным регистром, 16-битное и 8-битное деление обычно так же быстро, как 32-битное или не хуже.

В процессорах AMD производительность деления не зависит отразмер операнда, только данные .0 / 1 со 128/64-битным должно быть быстрее, чем наихудший случай любого меньшего размера операнда.Команда AMD для целочисленного деления составляет всего 2 мопа (предположительно потому, что она должна записать 2 регистра) со всей логикой, выполняемой в исполнительном блоке.

16-бит / 8-бит => 8-битное делениена Ryzen - один моп (потому что нужно только написать AH: AL = AX).


На процессорах Intel div / idiv микрокодируется столько же мопов .Примерно одинаковое количество мопов для всех размеров операндов до 32-битных (Skylake = 10), но 64-битные намного намного медленнее .(Skylake div r64 - 36 моп, Skylake idiv r64 - 57 моп).См. Таблицы инструкций Agner Fog: https://agner.org/optimize/

div / idiv пропускная способность для размеров операндов до 32-битных фиксирована на 1 на 6 циклов на Skylake.Но пропускная способность div/idiv r64 составляет один на 24-90 циклов.

См. Также Код пробного разделения работает в 2 раза быстрее, чем 32-разрядный в Windows, по сравнению с 64-разрядным в Linux для конкретного эксперимента с производительностью, где изменяется REX.WПрефикс в существующем двоичном файле для изменения div r64 на div r32 увеличил пропускную способность в ~ 3 раза.

И Почему Clang делает этот трюк по оптимизации только начиная с Sandy Bridge? Показывает, что Clang оппортунистически использует 32-битное деление, когда дивиденд небольшой, при настройке на процессоры Intel.Но у вас большой дивиденд и достаточно большой делитель, что является более сложным случаем.Эта оптимизация clang по-прежнему обнуляет верхнюю половину дивиденда в asm, никогда не используя ненулевой или не расширенный EDX.


Мне не удалось сделать популярные компиляторы Cгенерировать последний код при делении 32-разрядного целого без знака (смещенного влево на 32 бита) на другое 32-разрядное целое число.

Я предполагаю, что вы преобразовали это 32-разрядное целое число в uint64_t первый , чтобы избежать UB и получить нормальный uint64_t / uint64_t в абстрактной машине C.

Это имеет смысл: Ваш путь не будет безопасным, он выйдет из строя с #DE когда edx >= ebx. Сбой деления x86, когда частное переполнение AL / AX / EAX / RAX, вместо тихого усечения.Отключить это невозможно.

Поэтому компиляторы обычно используют idiv только после cdq или cqo и div только после обнуления старшей половины, если только вы не используете встроенный или встроенный ассемблер дляРаскройте себя до возможности сбоя вашего кода.В C x / y только ошибки, если y = 0 (или для подписи, INT_MIN / -1 также допускается ошибка 1 ).

GNU C не имеет встроенной функции для широкого деления, , но MSVC имеет _udiv64.(В gcc / clang регистр с делением шире, чем 1, использует вспомогательную функцию, которая пытается оптимизировать работу для небольших входов. Но это не помогает для деления 64/32 на 64-битной машине, где GCC и clang просто используют 128/ 64-битная инструкция деления.)

Даже если бы был какой-то способ пообещать компилятору, что ваш делитель будет достаточно большим, чтобы фактор соответствовал 32 битам, текущие gcc и clang этого не ищутоптимизация по моему опыту.Это была бы полезная оптимизация для вашего случая (если она всегда безопасна), но компиляторы не будут ее искать.


Сноска 1. Чтобы быть более конкретным, ISO C описывает эти случаи как "неопределенные".поведение";некоторые ISA, такие как ARM, имеют безошибочные инструкции по разделению.C UB означает, что может произойти что угодно , включая только усечение до 0 или какой-либо другой целочисленный результат.См. Почему целочисленное деление на -1 (отрицательное) приводит к FPE? для примера AArch64 против кода x86 и результатов. Допускается до отказа, не означает требуется до отказа.

2 голосов
/ 18 июня 2019

Может ли 128-битное / 64-битное аппаратное разделение без знака быть быстрее в некоторых случаях, чем 64-битное / 32-битное деление на процессорах Intel / AMD x86-64?

Теоретически все возможно (например, может бытьчерез 50 лет Nvidia создает процессор 80x86, который ...).

Однако я не могу придумать единственной вероятной причины, по которой 128-битное / 64-битное деление будет быстрее, чем (не просто эквивалентно)64-битное / 32-битное деление на x86-64.

Я подозреваю это, потому что я предполагаю, что авторы компилятора C очень умны, и до сих пор я не смог заставить популярные компиляторы C генерировать последний код, когдаделение 32-разрядного целого без знака (сдвинутое влево на 32 бита) на другое 32-разрядное целое число.Он всегда компилируется в 128-битную / 64-битную инструкцию div.PS Сдвиг влево компилируется нормально до shl.

Разработчики компиляторов умны, но компиляторы сложны, и правила языка C мешают.Например, если вы просто делаете a = b/c;b, являющимся 64-битным, и c, являющимся 32-битным), языковые правила таковы, что c повышается до 64-битного, прежде чем произойдет деление, так что это заканчивается64-разрядный делитель на каком-то промежуточном языке, и это затрудняет для внутреннего перевода (с промежуточного языка на язык ассемблера) сказать, что 64-разрядный делитель может быть 32-разрядным.

...