Как разрешить компилятору GCC переменное деление на mul (если быстрее)

int a, b;
scanf("%d %d", &a, &b);
printf("%d\n", (unsigned int)a/(unsigned char)b);

При компиляции я получил   ...

    ::00401C1E::  C70424 24304000          MOV DWORD PTR [ESP],403024  %d %d
    ::00401C25::  E8 36FFFFFF              CALL 00401B60               scanf
    ::00401C2A::  0FB64C24 1C              MOVZX ECX,BYTE PTR [ESP+1C]
    ::00401C2F::  8B4424 18                MOV EAX,[ESP+18]                        
    ::00401C33::  31D2                     XOR EDX,EDX                             
    ::00401C35::  F7F1                     DIV ECX                                 
    ::00401C37::  894424 04                MOV [ESP+4],EAX                         
    ::00401C3B::  C70424 2A304000          MOV DWORD PTR [ESP],40302A  %d\x0A
    ::00401C42::  E8 21FFFFFF              CALL 00401B68               printf

Будет ли быстрее, если DIV превратится в MUL и будет использовать массив для хранения mulvalue? Если да, то как разрешить компилятору сделать оптимизацию?

int main() {
    uint a, s=0, i, t;
    scanf("%d", &a);
    diviuint aa = a;
    t = clock();
    for (i=0; i<1000000000; i++)
        s += i/a;
    printf("Result:%10u\n", s);
    printf("Time:%12u\n", clock()-t);
    return 0;
}

где diviuint (a) делает память 1/a и использует несколько вместо Используя s + = i/aa, скорость 2 раза равна s + = i/a

Ответ 1

Замена DIV на MUL может иметь смысл (но не обязательно во всех случаях), когда одно из значений известно во время компиляции. Когда оба являются входами пользователя, вы не знаете, что такое диапазон, поэтому все обычные трюки не будут работать.

В основном вам нужно обрабатывать как a, так и b между INT_MAX и INT_MIN. Там нет места для масштабирования их вверх/вниз. Даже если вы хотите расширить их до более крупных типов, вероятно, потребуется больше времени для инверсии b и убедитесь, что результат будет согласованным.

Ответ 2

Единственный способ ЗНАТЬ, если div или mul быстрее, - это тестирование как в эталонном тесте [очевидно, если вы используете свой код выше, вы в основном измеряете время чтения/записи входов и результаты, а не фактическая инструкция деления, поэтому вам нужно что-то, где вы можете изолировать инструкцию деления от ввода и вывода].

Я предполагаю, что на немного более старых процессорах mul будет немного быстрее, на современных процессорах div будет работать так быстро, как если бы не быстрее, чем поиск 256 int значений.

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

Обратите также внимание на то, что вы вводите зависимость, которая сама по себе может замедлить последовательность операций. Современный процессор довольно хорош в "скрывающей" задержке, если есть другие инструкции для выполнения [так что вы должны использовать это в "как можно более реалистичный сценарий".

Ответ 3

Вы правы, что найти мультипликативный обратный может стоить того, если целочисленное деление внутри цикла неизбежно. gcc и clang не будут делать этого для вас с постоянными во времени; только константы времени компиляции. Это слишком дорого (в кодовом размере) для компилятора, чтобы не быть уверенным, что это необходимо, и прирост производительности не так велик с константами без компиляции. (Я не уверен, что ускорение всегда будет возможно, в зависимости от того, насколько хорошее целочисленное деление находится на целевой микроархитектуре.)


Используя мультипликативный обратный

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

Обратите внимание, что разные константы нуждаются в разных сдвигах верхней половины полного умножения, а некоторые константы нуждаются в более разных сдвигах, чем другие. (Другой способ сказать, что некоторые значения сдвига равны нулю для некоторых констант). Таким образом, код, не связанный с компиляцией по времени, по-разному, требует всех сдвигов, а счетчики сдвига должны быть переменными. (На x86 это дороже, чем немедленное смещение).

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

(Intel SSE/AVX не выполняет целочисленное деление на аппаратное обеспечение, но предоставляет множество множителей и довольно эффективные инструкции сдвига счетчика переменных. Для 16-битных элементов есть инструкция, которая производит только большую половину умножения. Для 32-битных элементов расширение расширяется, поэтому вам понадобится перетасовка.)

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


Другие способы получения div из цикла

for (i=0; i<1000000000; i++)
    s += i/a;

В вашем примере вы можете получить лучшие результаты от использования аккумулятора uint128_t s и деления на a вне цикла. 64-битная пара add/adc довольно дешевая. (Это не даст одинаковых результатов, потому что целочисленное деление усекает вместо округления до ближайшего.)

Я думаю, вы можете это объяснить, перебирая с помощью i += a; tmp++ и делая s += tmp*a, чтобы объединить все добавления из итераций, где i/a - то же самое. Таким образом, s += 1 * a учитывает все итерации от i = [a .. a*2-1]. Очевидно, что это был просто тривиальный пример, и более эффективная петля обычно не возможна. Это не по теме для этого вопроса, но стоит сказать в любом случае: ищите большие оптимизации, реструктурируя код или используя некоторую математику, прежде чем пытаться ускорить выполнение той же самой вещи быстрее. Говоря о математике, вы можете использовать формулу sum(0..n) = n * (n+1) / 2 здесь, потому что мы можем определить a из a*1 + a*2 + a*3 ... a*max. У меня может быть один за другим, но я уверен, что простой расчет постоянного времени в замкнутой форме даст тот же ответ, что и цикл для любого a:

uint32_t n = 1000000000 / a;
uint32_t s = a * n*(n+1)/2 + 1000000000 % a;

Если вам нужен только i/a в цикле, возможно, стоит сделать что-то вроде:

// another optimization for an unlikely case
for (uint32_t i=0, remainder=0, i_over_a=0 ; i < n ; i++) {
    // use i_over_a

    ++remainder;
    if (remainder == a) {        // if you don't need the remainder in the loop, it could save an insn or two to count down from a to 0 instead of up from 0 to a, e.g. on x86.  But then you need a clever variable name other than remainder.
        remainder = 0;
        ++i_over_a;
    }
}

Опять же, это маловероятно: оно работает только в том случае, если вы делите счетчик циклов на константу. Однако он должен хорошо работать. Либо a велико, поэтому неверные предсказания ветвления будут нечастыми, или a (надеюсь) достаточно мал для хорошего предсказателя ветвей, чтобы распознать повторяющийся паттерн a-1 в одну сторону, затем 1 разветкить другой путь. Наихудшее значение a может быть 33 или 65 или что-то в зависимости от микроархитектуры. Безветровое asm возможно возможно, но не стоит. например handle ++i_over_a с добавлением-переносом и условным перемещением для обнуления. (например, псевдокод x86 cmp a-1, remainder/cmovc remainder, 0/adc i_over_a, 0. Условие b (ниже) - это просто CF==1, то же самое, что и условие c (переносить). Безветровое asm было бы упрощено декремент от a до 0. (не нужно обнулять reg для cmov и может иметь a в регистре вместо a-1))

Ответ 4

В вопросе есть неправильное предположение. Мультипликативный инвертированный целое число больше 1 - это доля меньше единицы. Они не существуют в мире целых чисел. Таблица поиска не работает, потому что вы не можете найти то, что не существует. Даже если вы "масштабируете" дивиденд, результаты будут неверными в том смысле, что они совпадают с целым делением. Возьмите этот пример:

printf("%x %x\n", 0x10/0x9, 0x30/0x9);
// prints: 1 5

Предполагая, что существует мультипликативный обратный, оба члена делятся на один и тот же делитель (9), поэтому должны иметь одно и то же значение таблицы поиска (мультипликативный обратный). Любое фиксированное значение поиска, соответствующее делителю (9), умноженное на целое число, будет во втором члене относительно первого слагаемого в 3 раза больше. Как видно из примера, результатом фактического целочисленного деления является 5, а не 3.

Вы можете приблизить все, используя масштабированную таблицу поиска. Например, таблица поиска, которая является мультипликативной обратной, когда результат делится на 2 ^ 16. Затем вы умножаетесь на значение таблицы поиска и сдвигаете результат на 16 бит вправо. Занимает много времени и требует таблицы поиска по 1024 байта. Тем не менее, это не привело бы к тем же результатам, что и целочисленное разделение. Оптимизация компилятора не приведет к "приближенным" результатам целочисленного деления.