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

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

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

и так далее.

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

16 ответов


короткий ответ: маловероятно.

длинный ответ: В вашем компиляторе есть оптимизатор, который знает, как умножать так быстро, как ваша целевая архитектура процессора способна. Лучше всего четко сообщить компилятору о своем намерении (т. е. i*2, а не i

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


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

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

и

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

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

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


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

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


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

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


действительно ли быстрее использовать say (i

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

тематическое исследование-от 486 до core 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 цикла для целочисленного умножения. Вы можете найти задержки и thoughput в приложении C "руководства по оптимизации архитектуры Intel® 64 и IA-32", которое находится наhttp://www.intel.com/products/processor/manuals/.

(от некоторых Intel blurb)

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

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

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

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

Примеры (с гипотетической assembly language)

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...............

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

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

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

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

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

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

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

общие решения против частичного решения

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

умножение и деление можно достигнуть используя операторы бита...

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

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

согласно стандарту C++ 5.8:

- 3-Значение E1 >> E2 является E1 сдвинутых вправо E2 битовых позиций. Если E1 имеет тип unsigned или если E1 имеет знаковый тип и неотрицательное значение, в результате получается интегральная часть коэффициента Е1, деленная на величину 2, возведенную в степень Е2. Если E1 имеет знаковый тип и отрицательное значение, результирующее значение определяется реализацией.

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

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

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

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


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

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

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

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


сдвиг, как правило, намного быстрее, чем умножение на уровне инструкций, но вы вполне можете тратить свое время на преждевременную оптимизацию. Компилятор может выполнять эти оптимизации в 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

инструкции Shift и integer multiply имеют аналогичную производительность на большинстве современных процессоров - инструкции integer multiply были относительно медленными еще в 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

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


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

пиксельный хруст в драйвере видеокарты? Весьма вероятно, да!

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

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


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

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

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


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

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


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

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

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


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


тест 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% для умножения). Если его не-сила двух, вероятно, значительное замедление.

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

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


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

Ниже приведен пример кода c++, который может выполнять более быстрое деление, делая 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;
}

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

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

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

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

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

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