Как я могу сказать компилятору MSVC использовать 64-битное / 32-битное деление вместо более медленного 128-битного / 64-битного деления? - PullRequest
4 голосов
/ 19 июня 2019

Как я могу сказать компилятору MSVC использовать операцию 64-битного / 32-битного деления для вычисления результата следующей функции для цели x86-64:

UINT32 ScaledDiv(UINT32 a, UIN32 b)  // Always a > b
{
  return ((UINT64)b<<32) / a;   //Yes, this must be casted because the result of b<<32 is undefined
}

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

; Assume arguments on entry are: Dividend in EDX, Divisor in ECX
mov edx, edx  ;A dummy instruction to indicate that the dividend is already where it is supposed to be
xor eax,eax
div ecx   ; EAX = EDX:EAX / ECX

... однако компилятор x64 MSVC настаивает на использовании 128-битной / 64-битной div инструкции, такой как:

mov     eax, edx
xor     edx, edx
shl     rax, 32                             ; Scale up the dividend
mov     ecx, ecx
div rcx   ;RAX = RDX:RAX / RCX

См .: https://www.godbolt.ms/z/I2qFSk

Согласно ответу на этот вопрос , 128-битная / 64-битная div инструкция не быстрее , чем 64-битная / 32-битная div инструкция.

Это проблема, потому что она излишне замедляет мой алгоритм DSP, который делает миллионы таких масштабированных делений.

Я проверил эту оптимизацию, исправив исполняемый файл для использования 64-битной / 32-битной инструкции div: Производительность увеличилась на 28% в соответствии с двумя временными метками, полученными в инструкциях rdtsc.

(Примечание редактора: предположительно на некоторых последних процессорах Intel. Процессорам AMD не нужна эта микрооптимизация, как объяснено в связанных вопросах и ответах.)

Ответы [ 2 ]

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

Никакие текущие компиляторы (gcc / clang / ICC / MSVC) не выполнят эту оптимизацию из портативного источника ISO C, даже если вы дадите им доказать, что b < a, так что частное будет соответствовать 32-битным.(Например, с GNU C if(b>=a) __builtin_unreachable(); на Godbolt ).Это пропущенная оптимизация;до тех пор, пока это не будет исправлено, вы должны обойти это с помощью встроенной или встроенной ассм.вычислите мультипликативное обратное значение один раз и примените его несколько раз.)


_udiv64 доступно начиная с окончательной первоначальной версии Visual Studio 2019.

В режиме C (-TC) это, по-видимому, всегда определяется.В режиме C ++ вам нужно #include <immintrin.h>, в соответствии с документацией Microsoft.или intrin.h.

https://godbolt.org/z/vVZ25L (или на Godbolt.ms , поскольку недавний MSVC на главном сайте Godbolt не работает 1 .)

#include <stdint.h>
#include <immintrin.h>       // defines the prototype

// pre-condition: a > b else 64/32-bit division overflows
uint32_t ScaledDiv(uint32_t a, uint32_t b) 
{
    uint32_t remainder;
    uint64_t d = ((uint64_t) b) << 32;
    return _udiv64(d, a, &remainder);
}

int main() {
    uint32_t c = ScaledDiv(5, 4);
    return c;
}

_udiv64 произведет 64/32 дел.Два сдвига влево и вправо являются пропущенной оптимизацией.

;; MSVC 19.20 -O2 -TC
a$ = 8
b$ = 16
ScaledDiv PROC                                      ; COMDAT
        mov     edx, edx
        shl     rdx, 32                             ; 00000020H
        mov     rax, rdx
        shr     rdx, 32                             ; 00000020H
        div     ecx
        ret     0
ScaledDiv ENDP

main    PROC                                            ; COMDAT
        xor     eax, eax
        mov     edx, 4
        mov     ecx, 5
        div     ecx
        ret     0
main    ENDP

Итак, мы видим, что MSVC не выполняет постоянное распространение через _udiv64, хотя в этом случае он не переполняется имог бы скомпилировать main до mov eax, 0ccccccccH / ret.


ОБНОВЛЕНИЕ # 2 https://godbolt.org/z/n3Dyp- Добавлено решение с компилятором Intel C ++, но этоменее эффективен и будет препятствовать постоянному распространению, потому что это встроенный ассм.Microsoft запускает https://www.godbolt.ms/ для размещения последних компиляторов MSVC на реальных Windows, и обычно основной сайт Godbolt.org ретранслируется на MSVC.)

Кажется, что godbolt.ms будет генерировать короткие ссылки, ноне расширять их снова!Полные ссылки в любом случае лучше за их устойчивость к гниению ссылок.

4 голосов
/ 21 июня 2019

@ Ответ Алекса Лопатина показывает, как использовать _udiv64 для получения не страшного скалярного кода (несмотря на глупую пропущенную оптимизацию MSVC, смещающуюся влево / вправо).

Для компиляторов, которые поддерживают встроенный asm GNU C (включая ICC)), вы можете использовать это вместо неэффективного встроенного синтаксиса MSVC asm, который имеет много накладных расходов для переноса одной инструкции.См. В чем разница между «asm», «__asm» и «__asm ​​__»? для примера переноса 64-бит / 32-бит => 32-бит idiv.(Используйте его для div, просто изменив мнемонику и типы на unsigned.) GNU C не имеет встроенной функции для деления 64/32 или 128/64;он должен оптимизировать чистый C. Но, к сожалению, GCC / Clang / ICC пропустили оптимизации для этого случая, даже используя if(a<=b) __builtin_unreachable();, чтобы пообещать, что a>b.


Но это все еще скалярное деление с довольно плохим

Может быть, вы можете использовать графический процессор для вашей задачи DSP?Если у вас достаточно большой пакет работ (а остальная часть вашего алгоритма совместима с графическим процессором), то, вероятно, стоит потратить время на обмен данными с графическим процессором.

Если вы используете процессор,тогда все, что мы можем предложить, выиграет от распараллеливания по нескольким ядрам, так что сделайте это для большей пропускной способности.


x86 SIMD (SSE4 / AVX2 / AVX512 *) не имеет целочисленного деления SIMD ваппаратное обеспечение .Функции Intel SVML _mm_div_epu64 и _mm256_div_epu64 являются , а не встроенными для реальной инструкции, это медленные функции, которые могут быть распакованы в скалярные или вычислить мультипликативные инверсии.Или любой другой трюк, который они используют;возможно, 32-битные функции деления преобразуются в векторы SIMD double, особенно если доступен AVX512.(Intel по-прежнему называет их «встроенными», возможно, потому, что они похожи на встроенную функцию, которую она понимает и может выполнять постоянное распространение. Они, вероятно, настолько эффективны, насколько это возможно, но это «не очень», и им нужнодля обработки общего случая, а не только вашего особого случая с младшей половиной одного делителя, равной нулю, и частным соответствием в 32 битах.)

Если у вас один и тот же делитель для многих элементов см. https://libdivide.com/ для SIMD, чтобы вычислить мультипликативное обратное значение один раз и применить его повторно.(Вы должны адаптировать эту технику, чтобы выпекать при сдвиге дивиденда, фактически не делая этого, оставляя малую половину с нулем неявной.)

Если ваш делитель всегда меняется, и это не средний шагв более крупном SIMD-дружественном алгоритме скалярное деление может быть лучшим выбором, если вам нужны точные результаты.


Вы можете получить большие ускорения от использования SIMD float, если достаточно точности 24-битной мантиссы

uint32_t ScaledDiv(uint32_t a, uint32_t b) 
{
    return ((1ULL<<32) * (float)b) / a;
}

(float)(1ULL<<32) - это константа времени компиляции 4294967296.0f.

Это автоматически векторизирует массив с gcc и clang даже без -ffast-math (но не MSVC). Посмотри на Годболт .Вы можете перенести ассемблер gcc или clang обратно на встроенные функции для MSVC;они используют некоторые трюки FP для упакованного преобразования целых чисел без знака в / из числа с плавающей запятой без AVX512.Невекторная скалярная FP, вероятно, будет медленнее, чем обычное целое число в MSVC, а также будет менее точной.

Например, пропускная способность Skylake div r32 составляет 1 на 6 циклов.Но его пропускная способность AVX vdivps ymm составляет одну инструкцию (из 8 float с) на 5 циклов.Или для 128-битного SSE2 divps xmm имеет пропускную способность один на 3 цикла. Таким образом, вы получаете примерно 10-кратную пропускную способность от AVX на Skylake.(8 * 6/5 = 9,6) Старые микроархитектуры имеют гораздо более медленное деление SIMD FP, но также и несколько более медленное целочисленное деление.Как правило, это соотношение меньше, поскольку старые процессоры не имеют таких широких разделителей SIMD, поэтому 256-битный vdivps должен пропускать 128-битные половины по отдельности.Но все еще есть много выгоды, как лучше, чем в 4 раза на Haswell.И у Ryzen vdivps ymm пропускная способность 6c, но div 32 пропускная способность 14-30 циклов.Так что это еще большее ускорение, чем Skylake.

Если остальная часть вашей задачи DSP может извлечь выгоду из SIMD, общее ускорение должно быть очень хорошим.float операции имеют более высокую задержку, поэтому выполнение вне очереди должно работать усерднее, чтобы скрыть эту задержку и выполнить перекрытие независимых итераций цикла.Итак, IDK, было бы лучше, если бы вы просто конвертировали в float и обратно для этой одной операции, или изменили свой алгоритм для работы с float везде .Это зависит от того, что еще нужно сделать с вашими номерами.


Если ваши числа без знака действительно соответствуют знаковым 32-разрядным целым числам, вы можете использовать прямую аппаратную поддержку дляупакованный SIMD int32 -> преобразование с плавающей точкой .В противном случае вам понадобится AVX512F для упакованного uint32 -> float с одной инструкцией, но это можно эмулировать с некоторой потерей эффективности.Это то, что делает gcc / clang при автоматической векторизации с AVX2, и почему MSVC не автоматически векторизирует.

MSVC выполняет автоматическую векторизацию с int32_t вместо uint32_t (и gcc / clang может сделать более эффективный код), поэтому предпочитайте, чтобы старший бит ваших целочисленных входов и / или выходов не мог быть установлен.(т. е. интерпретация дополнения 2 их битовых комбинаций будет неотрицательной.)

Особенно с AVX, vdivps достаточно медленный, чтобы скрыть затраты на конвертацию из целого числа и обратно, если только нет другихполезная работа, которая вместо этого могла бы перекрываться.


Точность с плавающей точкой:

A float сохраняет числа как significand * 2^exp, где значение и находится в диапазоне [1.0, 2.0).(Или [0, 1.0) для субнормалей).Одинарная точность float имеет 24 бита значимости и точности, включая неявный бит 1.

https://en.wikipedia.org/wiki/Single-precision_floating-point_format

Таким образом, можно представить 24 наиболее значимые цифры целого числа,остальное потеряно из-за ошибки округления.Целое число типа (uint64_t)b << 32 не является проблемой для float;это просто означает больший показатель.Все младшие биты равны нулю.

Например, b = 123105810 дает нам 528735427897589760 для b64 << 32.Преобразование этого значения в float непосредственно из 64-разрядного целого числа дает нам 528735419307655168, ошибка округления 0,0000016% или около 2 ^ -25,8.Это неудивительно: максимальная ошибка округления составляет 0,5 целых (единицы на последнем месте), или 2 ^ -25, и это число было даже таким, что оно все равно имело 1 конечный ноль.Это та же относительная ошибка, которую мы получили бы при конвертации 123105810;результирующий float также тот же, за исключением его поля экспоненты (которое выше на 32).

(я использовал https://www.h -schmidt.net / FloatConverter / IEEE754.html чтобы проверить это.) Максимальный показатель

float достаточно велик, чтобы содержать целые числа вне диапазона от INT64_MIN до INT64_MAX.Младшие биты больших целых чисел, которые может представлять float, равны нулю, но это именно то, что вы имеете с b<<32.Таким образом, вы теряете младшие 9 бит b только в худшем случае, когда он является полным диапазоном и нечетным.

Если важной частью вашего результата являются наиболее значимые биты, иналичие младших ~ 9 целых битов = ошибка округления в порядке после преобразования обратно в целое число, тогда float идеально подходит для вас.

Если float не работает, double может бытьопция.

divpd примерно вдвое медленнее, чем divps на многих процессорах, и выполняет вдвое меньше работы (2 double элементов вместо 4 float).Таким образом, вы теряете пропускную способность в 4 раза таким образом.

Но каждое 32-разрядное целое число может быть представлено в точности как double. И, возвращая обратно с усечением к нулю, я думаювы получите точное целочисленное деление для всех пар входных данных, если только не является двойным округлением (сначала до ближайшего double, затем усечение) .Вы можете проверить это с

// exactly correct for most inputs at least, maybe all.
uint32_t quotient = ((1ULL<<32) * (double)b) / a;

Длинная постоянная без знака (1ULL<<32) преобразуется в double, поэтому у вас есть 2x u32 -> двойные преобразования (a и b), двойное умножение, двойное деление и двойное -> Конвертация у32.x86-64 может делать все это эффективно с помощью скалярных преобразований (с нулевым расширением uint32_t в int64_t или игнорированием старших битов преобразования double-> int64_t), но, вероятно, оно все равно будет медленнее, чем div r32.

Преобразование u32 -> double и обратно (без AVX512) может быть даже дороже, чем преобразование u32 -> float, но clang делает его векторизацию автоматически.(Просто измените float на double в ссылке на крестик выше).Опять же, это очень помогло бы, если бы все ваши входные данные были <= INT32_MAX, чтобы их можно было рассматривать как целые числа со знаком для преобразования FP.

Если двойное округление является проблемой, вы можете установить режим округления FP на усечение.вместо стандартного округления до ближайшего, если вы не используете FP для чего-либо еще в потоке, где выполняется ваш код DSP.

...