Компилятор c/С++ оптимизирует постоянные деления по значению силы двух в сдвигах?

Вопрос говорит все. Кто-нибудь знает, если следующее...

size_t div(size_t value) {
    const size_t x = 64;
    return value / x;
}

... оптимизирован на?

size_t div(size_t value) {
    return value >> 6;
}

Составляют ли компиляторы? (Мой интерес к GCC). Существуют ли ситуации, когда это происходит, и другие, где это не так?

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

Ответ 1

Даже с g++ -O0 (да, -O0!) это происходит. Ваша функция сводится к следующему:

_Z3divm:
.LFB952:
        pushq   %rbp
.LCFI0:
        movq    %rsp, %rbp
.LCFI1:
        movq    %rdi, -24(%rbp)
        movq    $64, -8(%rbp)
        movq    -24(%rbp), %rax
        shrq    $6, %rax
        leave
        ret

Обратите внимание на shrq $6, который является сдвигом вправо на 6 мест.

С -O1 ненужный мусор удаляется:

_Z3divm:
.LFB1023:
        movq    %rdi, %rax
        shrq    $6, %rax
        ret

Результаты на g++ 4.3.3, x64.

Ответ 2

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

Например, MSVC преобразует деление на 71 на следующее:

// volatile int y = x / 71;

8b 0c 24        mov ecx, DWORD PTR _x$[esp+8] ; load x into ecx

b8 49 b4 c2 e6  mov eax, -423447479 ; magic happens starting here...
f7 e9           imul ecx            ; edx:eax = x * 0xe6c2b449

03 d1           add edx, ecx        ; edx = x + edx

c1 fa 06        sar edx, 6          ; edx >>= 6 (with sign fill)

8b c2           mov eax, edx        ; eax = edx
c1 e8 1f        shr eax, 31         ; eax >>= 31 (no sign fill)
03 c2           add eax, edx        ; eax += edx

89 04 24        mov DWORD PTR _y$[esp+8], eax

Итак, вы получаете разницу на 71 с умножением, смена пары и добавление пары.

Подробнее о том, что происходит, проконсультируйтесь с Генри Уорреном "Хакерский восторг" или сопутствующей веб-страницей:

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

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

Еще одна статья, которую я обнаружил сейчас, когда обсуждает эту оптимизацию: http://blogs.msdn.com/devdev/archive/2005/12/12/502980.aspx

Ответ 3

Только тогда, когда он может определить, что аргумент положительный. Это случай для вашего примера, но с тех пор, как C99 определил семантику округления к нулю для целочисленного деления, стало сложнее оптимизировать деление по степеням двух в сдвиги, поскольку они дают разные результаты для отрицательных аргументов.

В ответ на комментарий Майкла ниже, вот один из способов: деление r=x/p; of x на известную степень двух p действительно может быть переведено компилятором:

if (x<0)
  x += p-1;
r = x >> (log2 p);

Поскольку ОП спрашивал, должен ли он думать об этих вещах, один из возможных ответов будет "только если вы знаете знак дивиденда лучше, чем компилятор, или знаете, что не имеет значения, округляется ли результат до 0 или - ∞".

Ответ 4

Да, компиляторы генерируют наиболее оптимальный код для таких упрощенных вычислений. Однако, почему вы настаиваете конкретно на "сменах", мне не ясно. Оптимальный код для данной платформы может легко оказаться чем-то отличным от "сдвига".

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

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