Вы спрашиваете об оптимизации деления 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 и результатов. Допускается до отказа, не означает требуется до отказа.