Действительно ли умножение и деление с использованием операторов сдвига в C быстрее?

249

Умножение и деление могут быть достигнуты с использованием битовых операторов, например

i*2 = i<<1
i*3 = (i<<1) + i;
i*10 = (i<<3) + (i<<1)

и т.д.

Действительно ли быстрее использовать say (i<<3)+(i<<1) для умножения с 10, чем напрямую с помощью i*10? Есть ли какие-либо входные данные, которые нельзя размножать или разделять таким образом?

  • 8
    На самом деле, возможно дешевое деление на константу, отличную от степени двойки, но сложная задача, в которой вы не отдает должное «/ Деление… / Разделение» в вашем вопросе. Посмотрите, например, hackersdelight.org/divcMore.pdf (или получите книгу « Восхищение хакера», если можете).
  • 45
    Это похоже на то, что можно легко проверить.
Показать ещё 10 комментариев
Теги:
bit-shift
division
multiplication

16 ответов

411
Лучший ответ

Короткий ответ: Не похоже.

Длинный ответ: У вашего компилятора есть оптимизатор, который знает, как умножаться так быстро, как ваша целевая архитектура процессора способна. Лучше всего сказать компилятору ваше намерение ясно (то есть я * 2, а не я < 1), и дать ему возможность решить, какая самая быстрая последовательность сборки/машинного кода. Возможно даже, что сам процессор реализовал инструкцию умножения как последовательность сдвигов и добавляет в микрокод.

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

  • 189
    +1 за то, что «ваши коллеги поблагодарят вас позже».
  • 24
    Да, как уже говорилось, возможная выгода почти для каждого приложения полностью перевесит введенную неизвестность. Не беспокойтесь об этом виде оптимизации преждевременно. Создайте то, что семантически ясно, определите узкие места и оптимизируйте оттуда ...
Показать ещё 5 комментариев
90

Просто конкретная точка измерения: много лет назад я сравнивал два версии моего алгоритма хеширования:

unsigned
hash( char const* s )
{
    unsigned h = 0;
    while ( *s != '\0' ) {
        h = 127 * h + (unsigned char)*s;
        ++ s;
    }
    return h;
}

и

unsigned
hash( char const* s )
{
    unsigned h = 0;
    while ( *s != '\0' ) {
        h = (h << 7) - h + (unsigned char)*s;
        ++ s;
    }
    return h;
}

На каждой машине я оценил ее, первый был, по крайней мере, так же быстро, как второй. Несколько удивительно, что это было иногда быстрее (например, на Sun Sparc). Когда аппаратное обеспечение не поддерживает быстрое умножение (и большинство не было тогда), компилятор преобразует умножение в соответствующие комбинации сдвигов и добавить/под. И поскольку это знал конечную цель, иногда это делалось с меньшими инструкциями, чем когда вы явно написали сдвиги и add/subs.

Обратите внимание, что это было что-то вроде 15 лет назад. Надеюсь, компиляторы с тех пор только улучшились, так что вы можете в значительной степени рассчитывать на компилятор делает правильные вещи, возможно, лучше, чем вы могли. (Также, причина, по которой код выглядит так C'ish, потому что это было более 15 лет назад. Я бы, очевидно, использовал std::string и итераторы сегодня.)

  • 5
    Возможно, вас заинтересует следующий пост в блоге, в котором автор отмечает, что современные оптимизирующие компиляторы, похоже, воссоздают общие шаблоны, которые программисты могут использовать, думая о них более эффективно в своих математических формах, чтобы действительно генерировать для них наиболее эффективную последовательность команд. , shape-of-code.coding-guidelines.com/2009/06/30/...
  • 0
    @PascalCuoq Ничего особенного в этом нет. Я обнаружил почти то же самое для Sun CC около 20 лет назад.
60

В дополнение ко всем другим хорошим ответам здесь, позвольте мне указать еще одну причину не использовать сдвиг, когда вы имеете в виду разделить или умножить. Я никогда не видел, чтобы кто-то вводил ошибку, забывая о относительном преимуществе умножения и добавления. Я видел ошибки, введенные, когда программисты-программисты забывали, что "умножение" через сдвиг логически является умножением, но не синтаксически с тем же приоритетом, что и умножение. x * 2 + z и x << 1 + z очень разные!

Если вы работаете с числами, используйте арифметические операторы типа + - * / %. Если вы работаете с массивами бит, используйте операторы бит twiddling, такие как & ^ | >>. Не смешивайте их; выражение, которое имеет как бит, так и арифметику, является ошибкой, ожидающей появления.

  • 5
    Избегать с простыми скобками?
  • 21
    @ Джоэл: Конечно. Если вы помните, что они вам нужны. Я хочу сказать, что легко забыть, что ты делаешь. Люди, которые приобретают умственную привычку читать «x << 1», как если бы это было «x * 2», приобретают умственную привычку думать, что << - это то же, что и умножение, чего не происходит.
Показать ещё 8 комментариев
50

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

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

  • 3
    Просто для приблизительной оценки: на типичном 16-битном процессоре (80C166) добавление двух целых чисел происходит за 1-2 такта, умножение за 10 тактов и деление за 20 тактов. Плюс некоторые операции перемещения, если вы оптимизируете i * 10 на несколько операций (каждая перемещается еще на +1 цикл). Наиболее распространенные компиляторы (Keil / Tasking) не оптимизируют, за исключением случаев умножения / деления на степень 2.
  • 53
    И вообще, компилятор оптимизирует код лучше, чем вы.
Показать ещё 1 комментарий
35

Действительно ли быстрее использовать say (i < 3) + (i < 1) умножить на 10, чем непосредственно на я * 10?

Это может быть или не быть на вашей машине - если вам интересно, измерьте в своем реальном использовании.

Пример исследования - от 486 до ядра i7

Бенчмаркинг очень сложно сделать значимо, но мы можем посмотреть на несколько фактов. Из http://www.penguin.cz/~literakl/intel/s.html#SAL и http://www.penguin.cz/~literakl/intel/i.html#IMUL мы получаем представление о тактовых циклах x86, необходимых для арифметического сдвига и умножения, Скажем, мы придерживаемся "486" (самый новый из перечисленных), 32-битных регистров и сразу же, IMUL занимает 13-42 цикла и IDIV 44. Каждая SAL занимает 2 и добавляет 1, поэтому даже с несколькими из них, как победитель.

В наши дни с ядром i7:

(из http://software.intel.com/en-us/forums/showthread.php?t=61481)

Задержка 1 цикл для целочисленного сложения и 3 цикла для целочисленного умножения. Вы можете найти задержки и скорость в Приложении C "Справочное руководство по оптимизации архитектуры Intel 64 и IA-32", которое расположено на http://www.intel.com/products/processor/manuals/.

(из некоторых выпусков Intel)

Используя SSE, Core i7 может выдавать одновременные команды добавления и умножения, приводящие к пиковой скорости 8 операций с плавающей запятой (FLOP) за такт.

Это дает вам представление о том, как далеко все прошло. Оптимизация мелочей, подобная бит, по сравнению с *, которая была воспринята серьезно даже в 90-х годах, сейчас устарела. Бит-сдвиг по-прежнему быстрее, но для не-power-of-two mul/div к тому времени, когда вы делаете все свои смены и добавляете результаты, он медленнее снова. Затем больше инструкций означает больше ошибок кэша, больше потенциальных проблем при конвейерной обработке, больше использования временных регистров может означать больше сохранения и восстановления содержимого регистра из стека... он быстро становится слишком сложным, чтобы количественно оценить все воздействия окончательно, но они преимущественно отрицательный.

функциональность в исходном коде и реализации

В общем, на ваш вопрос помечены C и С++. Как языки третьего поколения, они специально разработаны, чтобы скрыть детали базового набора инструкций процессора. Чтобы удовлетворить свои языковые стандарты, они должны поддерживать операции умножения и сдвига (и многие другие), даже если базовое оборудование не работает. В таких случаях они должны синтезировать требуемый результат, используя множество других инструкций. Точно так же они должны обеспечивать поддержку программного обеспечения для операций с плавающей запятой, если у CPU нет этого, и нет FPU. Современные процессоры поддерживают * и <<, поэтому это может показаться абсурдно теоретическим и историческим, но важно то, что свобода выбора реализации идет в обоих направлениях: даже если у процессора есть инструкция, которая реализует операцию, запрошенную в исходный код в общем случае, компилятор может выбрать что-то другое, что он предпочитает, потому что он лучше подходит для конкретного случая, с которым сталкивался компилятор.

Примеры (с гипотетическим языком ассемблера)

source           literal approach         optimised approach
#define N 0
int x;           .word x                xor registerA, registerA
x *= N;          move x -> registerA
                 move x -> registerB
                 A = B * immediate(0)
                 store registerA -> x
  ...............do something more with x...............

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

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

Таким образом, даже если перемещение и добавление все еще быстрее на каком-то конкретном оборудовании, тогда писатель-компилятор, вероятно, разработал именно тогда, когда он будет безопасным и полезным.

ремонтопригодность

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

К счастью, хорошие компиляторы, такие как GCC, обычно могут заменять ряд бит-сдвигов и арифметику прямым умножением, когда включена любая оптимизация (т.е. ...main(...) { return (argc << 4) + (argc << 2) + argc; }imull $21, 8(%ebp), %eax), поэтому перекомпиляция может помочь даже без исправления кода, но что не гарантируется.

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

Общие решения и частичные решения

Если у вас есть дополнительные знания, например, что ваш int действительно будет хранить только значения x, y и z, тогда вы сможете разработать некоторые инструкции, которые работают для этих значений и получить ваш результат быстрее, чем когда компилятор не обладает этим пониманием и нуждается в реализации, которая работает для всех значений int. Например, рассмотрите ваш вопрос:

Умножение и деление могут быть достигнуты с помощью бит-операторов...

Вы проиллюстрируете умножение, но как насчет деления?

int x;
x >> 1;   // divide by 2?

В соответствии со стандартом С++ 5.8:

-3- Значение E1 → E2 равно E1 поправочным позициям E2. Если E1 имеет неподписанный тип, или если E1 имеет подписанный тип и неотрицательное значение, значение результата является неотъемлемой частью частного E1, деленной на величину 2, поднятую до мощности E2. Если E1 имеет подписанный тип и отрицательное значение, результирующее значение определяется реализацией.

Итак, ваш сдвиг бит имеет определенный результат реализации, когда x отрицательный: он может работать не так на разных компьютерах. Но / работает гораздо более предсказуемо. (Это может быть и не совсем последовательным, так как разные машины могут иметь разные представления отрицательных чисел и, следовательно, разные диапазоны, даже если есть такое же количество бит, составляющих представление.)

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

Есть ли какой-либо вход, который нельзя размножать или разделять таким образом?

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

  • 2
    Очень хороший ответ. Сравнение Core i7 и 486 поучительно!
32

Просто попробовал на моей машине компиляцию:

int a = ...;
int b = a * 10;

При разборке он производит вывод:

MOV EAX,DWORD PTR SS:[ESP+1C] ; Move a into EAX
LEA EAX,DWORD PTR DS:[EAX+EAX*4] ; Multiply by 5 without shift !
SHL EAX, 1 ; Multiply by 2 using shift

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

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

  • 1
    Вы бы получили большое одобрение за это, если бы пропустили часть о векторе. Если компилятор может исправить умножение, он также может видеть, что вектор не изменяется.
  • 0
    Как компилятор может знать, что размер вектора не изменится без каких-либо действительно опасных предположений? Или вы никогда не слышали о параллелизме ...
Показать ещё 2 комментария
20

Сдвиг, как правило, намного быстрее, чем умножение на уровне команд, но вы вполне можете тратить свое время на преждевременные оптимизации. Компилятор вполне может выполнить эти оптимизации в compiletime. Само по себе это повлияет на читаемость и, возможно, не повлияет на производительность. Вероятно, стоит только делать такие вещи, если вы профилировали и обнаружили, что это узкое место.

На самом деле трюк разделения, известный как "магическое деление", действительно может дать огромные выигрыши. Снова вы должны сначала просмотреть, чтобы узнать, если это необходимо. Но если вы его используете, есть полезные программы, чтобы помочь вам понять, какие инструкции необходимы для одной семантики разделения. Вот пример: http://www.masm32.com/board/index.php?topic=12421.0

Пример, который я снял с потока OP на MASM32:

include ConstDiv.inc
...
mov eax,9999999
; divide eax by 100000
cdiv 100000
; edx = quotient

Генерирует:

mov eax,9999999
mov edx,0A7C5AC47h
add eax,1
.if !CARRY?
    mul edx
.endif
shr edx,16
  • 15
    Ссылка кажется случайной веткой форума о симпатиях по математике.
  • 3
    Дерьмо. Написал неправильно. Исправленный.
Показать ещё 4 комментария
11

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

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

#include <stdio.h>

int main(void)
{
    int i;

    for (i = 5; i >= -5; --i)
    {
        printf("%d / 2 = %d, %d >> 1 = %d\n", i, i / 2, i, i >> 1);
    }
    return 0;
}

Вывод:

5 / 2 = 2, 5 >> 1 = 2
4 / 2 = 2, 4 >> 1 = 2
3 / 2 = 1, 3 >> 1 = 1
2 / 2 = 1, 2 >> 1 = 1
1 / 2 = 0, 1 >> 1 = 0
0 / 2 = 0, 0 >> 1 = 0
-1 / 2 = 0, -1 >> 1 = -1
-2 / 2 = -1, -2 >> 1 = -1
-3 / 2 = -1, -3 >> 1 = -2
-4 / 2 = -2, -4 >> 1 = -2
-5 / 2 = -2, -5 >> 1 = -3

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

  • 4
    Целочисленные умножения микрокодируются, например, в PPU PlayStation 3, и останавливают весь конвейер. На некоторых платформах рекомендуется избегать целочисленных умножений :)
  • 2
    Многие беззнаковые деления - при условии, что компилятор знает, как - реализованы с использованием умножения без знака. Одно или два умножения на несколько тактовых циклов каждый может выполнять ту же работу, что и деление на 40 тактов и более.
Показать ещё 1 комментарий
3

Это полностью зависит от целевого устройства, языка, цели и т.д.

Пиксельный хруст в драйвере видеокарты? Очень вероятно, да!

.NET бизнес-приложение для вашего отдела? Абсолютно нет причин даже смотреть на это.

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

2

Не делайте, если вам абсолютно не нужно, и ваш код требует изменения, а не умножения/деления.

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

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

1

Я согласен с отмеченным ответом Дрю Холла. Ответ может использовать некоторые дополнительные заметки.

Для подавляющего большинства разработчиков программного обеспечения процессор и компилятор больше не имеют отношения к вопросу. Большинство из нас далеко за пределами 8088 и MS-DOS. Возможно, это относится только к тем, кто все еще разрабатывает встроенные процессоры...

В моей программной компании Math (add/sub/mul/div) следует использовать для всей математики. Хотя Shift следует использовать при преобразовании между типами данных, например. ushort to byte при n → 8, а не n/256.

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

Насколько мне известно, в некоторых машинах для умножения может потребоваться от 16 до 32 машинных циклов. Итак, Да, в зависимости от типа машины операторы быстрой смены быстрее, чем умножение/деление.

Однако у некоторых машин есть свой математический процессор, который содержит специальные инструкции для умножения/деления.

  • 7
    Люди, пишущие компиляторы для этих машин, также, вероятно, читали «Восторг хакеров» и соответственно оптимизировали.
0

Я думаю, что в одном случае, который вы хотите размножить или разделить на две силы, вы не ошибетесь в использовании операторов бит-сдвига, даже если компилятор преобразует их в MUL/DIV, потому что некоторые процессоры microcode ( на самом деле, макрос), так что для тех случаев вы достигнете улучшения, особенно если сдвиг больше 1. Или более явно, если у ЦП нет операторов с битрейтом, это будет MUL/DIV в любом случае, но если у процессора есть операторы с битрейтом, вы избегаете ветки микрокода, и это меньше инструкций.

Сейчас я пишу некоторый код, который требует много операций удвоения/сокращения пополам, потому что он работает с плотным двоичным деревом, и есть еще одна операция, которая, как я подозреваю, может быть более оптимальной, чем добавление - левая (мощность из двух умножаемых) сдвигов с добавлением. Это может быть заменено сдвигом влево и xor, если сдвиг шире, чем количество бит, которое вы хотите добавить, простым примером является (i < 1) ^ 1, который добавляет один к удвоенному значению. Это, конечно, не относится к правому сдвигу (мощность двух делений), потому что только левый (маломерный конец) сдвиг заполняет нуль нулями.

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

Кроме того, в алгоритмах, которые я пишу, они визуально представляют движения, которые происходят, поэтому в этом смысле они на самом деле более ясны. Левая часть бинарного дерева больше, а правая - меньше. Кроме того, в моем коде особое значение имеют нечетные и четные числа, а все левые дети в дереве нечетны, а все правые дети и корень - четные. В некоторых случаях, с которыми я еще не сталкивался, но, возможно, даже не думал об этом, x & 1 может быть более оптимальной операцией по сравнению с x% 2. x & 1 на четном числе будет производить ноль, но будет производить 1 для нечетного числа.

Переход немного дальше, чем просто нечетная/четная идентификация, если я получаю нуль для x & 3 Я знаю, что 4 является фактором нашего числа, и то же самое для x% 7 для 8 и т.д. Я знаю, что эти случаи, вероятно, имеют ограниченную полезность, но приятно знать, что вы можете избежать операции с модулем и вместо этого использовать побитовую логическую операцию, потому что побитовые операции почти всегда самые быстрые и наименее вероятные для двусмысленного компилятора.

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

0

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

Ниже приведен пример кода С++, который может выполнять более быстрое деление, выполняя 64-битное "Умножение на ответное". Как числитель, так и знаменатель должны быть ниже определенного порога. Обратите внимание, что он должен быть скомпилирован для использования 64-битных инструкций на самом деле быстрее обычного деления.

#include <stdio.h>
#include <chrono>

static const unsigned s_bc = 32;
static const unsigned long long s_p = 1ULL << s_bc;
static const unsigned long long s_hp = s_p / 2;

static unsigned long long s_f;
static unsigned long long s_fr;

static void fastDivInitialize(const unsigned d)
{
    s_f = s_p / d;
    s_fr = s_f * (s_p - (s_f * d));
}

static unsigned fastDiv(const unsigned n)
{
    return (s_f * n + ((s_fr * n + s_hp) >> s_bc)) >> s_bc;
}

static bool fastDivCheck(const unsigned n, const unsigned d)
{
    // 32 to 64 cycles latency on modern cpus
    const unsigned expected = n / d;

    // At least 10 cycles latency on modern cpus
    const unsigned result = fastDiv(n);

    if (result != expected)
    {
        printf("Failed for: %u/%u != %u\n", n, d, expected);
        return false;
    }

    return true;
}

int main()
{
    unsigned result = 0;

    // Make sure to verify it works for your expected set of inputs
    const unsigned MAX_N = 65535;
    const unsigned MAX_D = 40000;

    const double ONE_SECOND_COUNT = 1000000000.0;

    auto t0 = std::chrono::steady_clock::now();
    unsigned count = 0;
    printf("Verifying...\n");
    for (unsigned d = 1; d <= MAX_D; ++d)
    {
        fastDivInitialize(d);
        for (unsigned n = 0; n <= MAX_N; ++n)
        {
            count += !fastDivCheck(n, d);
        }
    }
    auto t1 = std::chrono::steady_clock::now();
    printf("Errors: %u / %u (%.4fs)\n", count, MAX_D * (MAX_N + 1), (t1 - t0).count() / ONE_SECOND_COUNT);

    t0 = t1;
    for (unsigned d = 1; d <= MAX_D; ++d)
    {
        fastDivInitialize(d);
        for (unsigned n = 0; n <= MAX_N; ++n)
        {
            result += fastDiv(n);
        }
    }
    t1 = std::chrono::steady_clock::now();
    printf("Fast division time: %.4fs\n", (t1 - t0).count() / ONE_SECOND_COUNT);

    t0 = t1;
    count = 0;
    for (unsigned d = 1; d <= MAX_D; ++d)
    {
        for (unsigned n = 0; n <= MAX_N; ++n)
        {
            result += n / d;
        }
    }
    t1 = std::chrono::steady_clock::now();
    printf("Normal division time: %.4fs\n", (t1 - t0).count() / ONE_SECOND_COUNT);

    getchar();
    return result;
}
0

Тест Python, выполняющий то же умножение 100 миллионов раз против тех же случайных чисел.

>>> from timeit import timeit
>>> setup_str = 'import scipy; from scipy import random; scipy.random.seed(0)'
>>> N = 10*1000*1000
>>> timeit('x=random.randint(65536);', setup=setup_str, number=N)
1.894096851348877 # Time from generating the random #s and no opperati

>>> timeit('x=random.randint(65536); x*2', setup=setup_str, number=N)
2.2799630165100098
>>> timeit('x=random.randint(65536); x << 1', setup=setup_str, number=N)
2.2616429328918457

>>> timeit('x=random.randint(65536); x*10', setup=setup_str, number=N)
2.2799630165100098
>>> timeit('x=random.randint(65536); (x << 3) + (x<<1)', setup=setup_str, number=N)
2.9485139846801758

>>> timeit('x=random.randint(65536); x // 2', setup=setup_str, number=N)
2.490908145904541
>>> timeit('x=random.randint(65536); x / 2', setup=setup_str, number=N)
2.4757170677185059
>>> timeit('x=random.randint(65536); x >> 1', setup=setup_str, number=N)
2.2316000461578369

Таким образом, при выполнении сдвига, а не умножения/деления на мощность двух в python, наблюдается небольшое улучшение (~ 10% для деления, ~ 1% для умножения). Если это не сила двух, то, вероятно, произойдет значительное замедление.

Снова эти # будут меняться в зависимости от вашего процессора, ваш компилятор (или интерпретатор - сделал в python для простоты).

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

0

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

  • 0
    или приведите число к unsigned
  • 4
    Вы уверены, что поведение переключения стандартизировано? У меня сложилось впечатление, что сдвиг вправо по отрицательным значениям определяется реализацией.
Показать ещё 2 комментария

Ещё вопросы

Сообщество Overcoder
Наверх
Меню