X86 Инструкция MUL от VS 2008/2010

Будут ли современные (2008/2010) заклинания Visual Studio или Visual С++ Express выдавать инструкции x86 MUL (беззнаковое умножение) в скомпилированном коде? Я не могу найти или придумать пример, где они появляются в скомпилированном коде, даже при использовании неподписанных типов.

Если VS не скомпилируется с использованием MUL, есть ли обоснование, почему?

Ответ 1

imul (со знаком) и mul (без знака) имеют форму с одним операндом, которая имеет edx:eax = eax * src. т.е. 32x32b => 64b с полным умножением (или 64x64b => 128b).

186 добавили форму imul dest(reg), src(reg/mem), immediate, а 386 добавили форму imul r32, r/m32, каждая из которых вычисляет только нижнюю половину результата. (Согласно приложению B NASM см. также вики-тег x86)

При умножении двух 32-битных значений младшие значащие 32 бита результата одинаковы, независимо от того, считаете ли вы значения знаковыми или беззнаковыми. Другими словами, разница между умножением со знаком и без знака становится очевидной, только если вы посмотрите на "верхнюю" половину результата, которую один операнд вставляет в imul/mul в edx и два или три операнда imul никуда не денется. Таким образом, многооперандные формы imul могут использоваться для значений со знаком и без знака, и Intel не нужно было также добавлять новые формы mul. (Они могли бы сделать мульти-операнд mul синонимом для imul, но это сделало бы выходные данные дизассемблирования не соответствующими источнику.)

В C результаты арифметических операций имеют тот же тип, что и операнды (после целочисленного преобразования для узких целочисленных типов). Если вы умножите два int вместе, вы получите int, а не long long: "верхняя половина" не сохраняется. Следовательно, компилятору C требуется только то, что обеспечивает imul, и, поскольку imul проще в использовании, чем mul, компилятор C использует imul, чтобы избежать необходимости инструкций mov для ввода данных в/из eax ].

В качестве второго шага, поскольку компиляторы C часто используют многооперандную форму imul, Intel и AMD прилагают усилия для того, чтобы сделать это как можно быстрее. Он записывает только один выходной регистр, а не e/rdx:e/rax, поэтому процессоры могли оптимизировать его проще, чем форма с одним операндом. Это делает imul еще более привлекательным.

Форма с одним операндом mul/imul полезна при реализации арифметики большого числа. В C в 32-битном режиме вы должны получить несколько вызовов mul путем умножения значений unsigned long long вместе. Но, в зависимости от компилятора и ОС, эти коды операций mul могут быть скрыты в какой-то отдельной функции, поэтому вы не обязательно их увидите. В 64-битном режиме long long имеет только 64 бита, а не 128, и компилятор просто использует imul.

Ответ 2

Существует три разных типа умножения инструкций на x86. Первый - MUL reg, который без знака умножает EAX на reg и помещает (64-разрядный) результат в EDX:EAX. Второй - IMUL reg, что делает то же самое с подписанным умножением. Третий тип - это IMUL reg1, reg2 (умножает reg1 на reg2 и сохраняет 32-битный результат в reg1) или IMUL reg1, reg2, imm (умножает reg2 на imm и сохраняет 32-битный результат в reg1).

Так как в C умножение двух 32-битных значений приводит к 32-битным результатам, компиляторы обычно используют третий тип (подпись не имеет значения, младшие 32 бита согласуются между множителями, подписанными и unsigned 32x32). VС++ будет генерировать "длинные многократные" версии MUL/IMUL, если вы действительно используете полные 64-битные результаты, например. здесь:

unsigned long long prod(unsigned int a, unsigned int b)
{
  return (unsigned long long) a * b;
}

2-операндовые (и 3-операндные) версии IMUL быстрее, чем версии с одним операндом, просто потому, что они не дают полного 64-битного результата. Широкие мультипликаторы большие и медленные; гораздо проще построить меньший множитель и синтезировать длинные множители, используя, при необходимости, Microcode. Кроме того, MUL/IMUL записывает два регистра, которые, как правило, обычно разрешаются путем разбиения на несколько инструкций внутри системы - гораздо проще для переопределения аппаратного обеспечения для отслеживания двух зависимых инструкций, каждый из которых записывает один регистр (большинство команд x86 выглядят так, как внутри), чем отслеживать одну инструкцию, которая записывает два.

Ответ 3

Согласно http://gmplib.org/~tege/x86-timing.pdf, команда IMUL имеет более низкую задержку и более высокую пропускную способность (если я правильно читаю таблицу), Возможно, VS просто использует более быструю инструкцию (предполагая, что IMUL и MUL всегда производят один и тот же вывод).

У меня нет Visual Studio, поэтому я попытался получить что-то еще с GCC. Я также всегда получаю некоторые изменения IMUL.

Это:

unsigned int func(unsigned int a, unsigned int b)
{ 
    return a * b;
}

Соответствует этому (с -O2):

_func:
LFB2:
        pushq   %rbp
LCFI0:
        movq    %rsp, %rbp
LCFI1:
        movl    %esi, %eax
        imull   %edi, %eax
        movzbl  %al, %eax
        leave
        ret

Ответ 4

Моя интуиция подсказывает мне, что компилятор выбрал IMUL произвольно (или в зависимости от того, что было быстрее из двух), так как биты будут одинаковыми, если он использует unsigned MUL или подписанный IMUL. Любое 32-битное целочисленное умножение будет 64-битным, охватывающим два регистра, EDX:EAX. Переполнение происходит в EDX, который по существу игнорируется, поскольку мы заботимся только о 32-битном результате в EAX. Используя IMUL, при необходимости добавьте в EDX, но опять же, нам все равно, поскольку нас интересует только 32-разрядный результат.

Ответ 5

Сразу после того, как я посмотрел на этот вопрос, я обнаружил MULQ в моем сгенерированном коде при делении.

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

Код С++:

for_each(TempVec.rbegin(), TempVec.rend(), [&](Short & Num){
    Remainder <<= 32;
    Remainder += Num;
    Num = Remainder / 1000000000;
    Remainder %= 1000000000;//equivalent to Remainder %= DecimalConvert
});

Оптимизированная сгенерированная сборка

00007FF7715B18E8  lea         r9,[rsi-4]  
00007FF7715B18EC  mov         r13,12E0BE826D694B2Fh  
00007FF7715B18F6  nop         word ptr [rax+rax] 
00007FF7715B1900  shl         r8,20h  
00007FF7715B1904  mov         eax,dword ptr [r9]  
00007FF7715B1907  add         r8,rax  
00007FF7715B190A  mov         rax,r13  
00007FF7715B190D  mul         rax,r8  
00007FF7715B1910  mov         rcx,r8  
00007FF7715B1913  sub         rcx,rdx  
00007FF7715B1916  shr         rcx,1  
00007FF7715B1919  add         rcx,rdx  
00007FF7715B191C  shr         rcx,1Dh  
00007FF7715B1920  imul        rax,rcx,3B9ACA00h  
00007FF7715B1927  sub         r8,rax  
00007FF7715B192A  mov         dword ptr [r9],ecx  
00007FF7715B192D  lea         r9,[r9-4]  
00007FF7715B1931  lea         rax,[r9+4]  
00007FF7715B1935  cmp         rax,r14  
00007FF7715B1938  jne         NumToString+0D0h (07FF7715B1900h)  

Обратите внимание на инструкцию MUL 5 строк. Этот сгенерированный код чрезвычайно неинтуитивный, я знаю, на самом деле он не похож на скомпилированный код, но DIV чрезвычайно медленный ~ 25 циклов для 32-битного div и ~ 75 в соответствии с этим диаграмма на современных ПК по сравнению с MUL или IMUL (около 3 или 4 циклов), и поэтому имеет смысл попытаться избавиться от DIV, даже если вам нужно добавить всевозможные дополнительные инструкции.

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

Это пример того, как компилятор использует производительность и возможности полного 64-битного неиспользуемого умножения, не показывая С++-кодер никаких признаков этого.

Ответ 6

Как уже объяснялось, C/С++ не выполняет операции word*word to double-word, для которых лучше всего подходит команда mul. Но есть случаи, когда вы хотите word*word to double-word, поэтому вам нужно расширение для C/С++.

GCC, Clang и ICC предоставляют встроенный тип __int128, который вы можете использовать для косвенного получения инструкции mul.

В MSVC он обеспечивает _ umul128 собственный (по крайней мере, VS 2010), который генерирует инструкцию mul. С этим встроенным наряду с _ addcarry_u64 можно было создать собственный эффективный тип __int128 с MSVC.