Что мне делать: умножение или деление?

118

Вот забавный глупый вопрос:

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

y = x / 2.0;
// or...
y = x * 0.5;

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

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

Хотя лично меня интересует ответ для Python 2.4-2.5, не стесняйтесь также публиковать ответ для других языков! И если хотите, не стесняйтесь публиковать другие более изящные способы (например, с использованием операторов побитового сдвига).

Edmundito
источник
5
Вы запускали тест? Это всего лишь около десятка строк кода. Что вы узнали, запустив тест? [Подсказка: сделать это было бы быстрее, чем разместить здесь вопрос.]
С.Лотт,
4
Отличный вопрос, который вызвал довольно интересные ответы / дискуссии. Спасибо :)
stealthcopter 01
22
Даже если он узнал ответ путем тестирования, это все еще полезный вопрос, и он дал несколько интересных и полезных ответов. Также я хочу, чтобы люди придерживались сути и воздерживались от написания ответов и комментариев к ответам, предлагающим нерелевантные советы о том, стоит ли делать данную оптимизацию. Почему бы не предположить, что ОП задает вопрос, как написано, вместо того, чтобы предполагать, что он или она «действительно» хочет совета по переписыванию в более крупном масштабе.
Кевин Уайтфут
1
Деление происходит намного медленнее, чем умножение. Но некоторые умные компиляторы / виртуальные машины преобразуют деление в умножение, поэтому ваши тесты будут иметь одинаковые результаты (оба теста тестируют умножение).
Иван Кукир 04
4
Немного не по теме, но я просто хочу сказать, насколько я согласен с @KevinWhitefoot. Нет ничего более неприятного, чем чтение проповедников, а не случайные технические ответы на технические вопросы. Спасибо Кевину за ваш комментарий!
Жан-Франсуа

Ответы:

78

Python:

time python -c 'for i in xrange(int(1e8)): t=12341234234.234 / 2.0'
real    0m26.676s
user    0m25.154s
sys     0m0.076s

time python -c 'for i in xrange(int(1e8)): t=12341234234.234 * 0.5'
real    0m17.932s
user    0m16.481s
sys     0m0.048s

умножение на 33% быстрее

Lua:

time lua -e 'for i=1,1e8 do t=12341234234.234 / 2.0 end'
real    0m7.956s
user    0m7.332s
sys     0m0.032s

time lua -e 'for i=1,1e8 do t=12341234234.234 * 0.5 end'
real    0m7.997s
user    0m7.516s
sys     0m0.036s

=> нет реальной разницы

LuaJIT:

time luajit -O -e 'for i=1,1e8 do t=12341234234.234 / 2.0 end'
real    0m1.921s
user    0m1.668s
sys     0m0.004s

time luajit -O -e 'for i=1,1e8 do t=12341234234.234 * 0.5 end'
real    0m1.843s
user    0m1.676s
sys     0m0.000s

=> это всего на 5% быстрее

Выводы: в Python умножать быстрее, чем делить, но по мере приближения к ЦП с использованием более продвинутых виртуальных машин или JIT преимущество исчезает. Вполне возможно, что будущая виртуальная машина Python сделает это неактуальным.

Хавьер
источник
Спасибо за совет по использованию команды time для тестирования производительности!
Эдмундито,
2
Ваш вывод неверен. Это становится более актуальным по мере того, как JIT / VM становится лучше. Деление становится медленнее по сравнению с меньшими накладными расходами виртуальной машины. Помните, что компиляторы обычно не могут сильно оптимизировать числа с плавающей запятой, чтобы гарантировать точность.
rasmus
7
@rasmus: По мере того, как JIT становится лучше, становится более вероятным использование инструкции умножения ЦП, даже если вы просили деление.
Ben Voigt
68

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

Преждевременная оптимизация - это корень всех зол. Всегда помните о трех правилах оптимизации!

  1. Не оптимизируйте.
  2. Если вы эксперт, см. Правило №1
  3. Если вы специалист и можете обосновать необходимость, воспользуйтесь следующей процедурой:

    • Код не оптимизирован
    • определить, насколько быстро «достаточно быстро» - отметьте, для какого пользовательского требования / истории требуется эта метрика.
    • Напишите тест скорости
    • Протестируйте существующий код - если он достаточно быстрый, все готово.
    • Перекодировать оптимизировано
    • Протестируйте оптимизированный код. ЕСЛИ он не соответствует метрике, выбросьте его и оставьте оригинал.
    • Если он соответствует требованиям, оставьте исходный код в виде комментариев.

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

Билл К
источник
7
это не полная цитата Кнута; см. en.wikipedia.org/wiki/…
Джейсон С.
Нет, на эту тему есть около 40 различных цитат из самых разных источников. Я как бы складываю несколько вместе.
Bill K
В вашем последнем предложении неясно, когда применять правила №1 и №2, и мы возвращаемся с того места, где мы начали: нам нужно решить, какие оптимизации стоит, а какие нет. Делать вид, что ответ очевиден, не является ответом.
Мэтт
2
Это действительно вас смущает? Всегда применяйте правила 1 и 2, если вы на самом деле не соответствуете спецификациям клиента и хорошо знакомы со всей системой, включая язык и характеристики кэширования процессора. На этом этапе ТОЛЬКО следуйте процедуре в 3, не просто думайте: «Эй, если я кэширую эту переменную локально вместо вызова геттера, все, вероятно, будет быстрее. Сначала докажите, что это недостаточно быстро, затем протестируйте каждую оптимизацию отдельно и выбрасывайте те, которые не помогают. Документируйте все время и внимательно
Билл К.
49

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

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

Томас Оуэнс
источник
1
Когда я делал радиолокационные процессоры, одна операция имела значение. Но мы вручную оптимизировали машинный код для достижения производительности в реальном времени. Во всем остальном я голосую за простое и очевидное.
S.Lott
Я думаю, что для некоторых вещей вас может заинтересовать одна операция. Но я ожидал, что в 99% приложений это не имеет значения.
Томас Оуэнс,
27
Тем более, что ОП искал ответ на Python. Я сомневаюсь, что что-то, что требует такой эффективности, было бы написано на Python.
Эд С.
4
Деление, вероятно, является самой дорогой операцией в программе пересечения треугольников, которая является основой большинства трассировщиков лучей. Если вы сохраните обратное и умножите вместо деления, вы ощутите многократное ускорение.
solinent
@solinent - да, ускорение, но я сомневаюсь «во много раз» - деление с плавающей запятой и умножение не должны отличаться более чем примерно на 4: 1, если только рассматриваемый процессор действительно не оптимизирован для умножения, а не деления.
Джейсон С.
39

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

y = x / 3.0;
y = x * 0.333333;  // how many 3's should there be, and how will the compiler round?

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

x = 100.0;
x / 3.0 == x * (1.0/3.0)  // is false in the test I just performed

Проблема скорости, вероятно, будет иметь значение только в языках C / C ++ или JIT, и даже тогда, только если операция выполняется в цикле в узком месте.

Марк Рэнсом
источник
Деление будет точным, если вы делите на целые числа.
плинтус
7
Деление с плавающей точкой со знаменателем> числителем должно вводить бессмысленные значения в младшие биты; деление обычно снижает точность.
S.Lott
8
@ С.Лотт: Нет, это неправда. Все реализации с плавающей запятой, совместимые с IEEE-754, должны идеально округлять результаты каждой операции (т. Е. До ближайшего числа с плавающей запятой) по отношению к текущему режиму округления. Умножение на обратное всегда приведет к еще большей ошибке, по крайней мере, потому, что должно произойти еще одно округление.
Электро
1
Я знаю, что этому ответу более 8 лет, но он вводит в заблуждение; вы можете выполнять деление без значительной потери точности:y = x * (1.0/3.0); и компилятор обычно вычисляет 1/3 во время компиляции. Да, 1/3 не является идеальным представлением в IEEE-754, но когда вы выполняете арифметику с плавающей запятой, вы все равно теряете точность , независимо от того, выполняете ли вы умножение или деление, потому что биты младшего разряда округляются. Если вы знаете, что ваши вычисления настолько чувствительны к ошибке округления, вы также должны знать, как лучше всего решить эту проблему.
Jason S
1
@JasonS Я просто оставил программу работать на ночь, начиная с 1.0 и считая до 1 ULP; Я сравнил результат умножения на (1.0/3.0)с делением на 3.0. Я получил 1,0000036666774155, и в этом месте 7,3% результатов были другими. Я предполагаю, что они различались только на 1 бит, но поскольку арифметика IEEE гарантированно округляет до ближайшего правильного результата, я придерживаюсь своего утверждения, что деление более точное. Будет ли разница значительна - решать вам.
Марк Рэнсом
25

Если вы хотите оптимизировать свой код, но при этом сохранять ясность, попробуйте следующее:

y = x * (1.0 / 2.0);

Компилятор должен иметь возможность выполнять деление во время компиляции, поэтому вы получите умножение во время выполнения. Ожидаю, что точность будет такой же, как и в y = x / 2.0случае.

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

Джейсон С
источник
12
Как ни крути (и кто бы это ни делал) - это стандартная практика в мире встраиваемых систем, и программисты в этой области понимают это.
Jason S
4
+1 за то, что он единственный здесь, понимающий, что компиляторы не могут оптимизировать операции с плавающей запятой, как хотят. Они не могут даже изменить порядок операндов при умножении, чтобы гарантировать точность (если только не используется расслабленный режим).
rasmus
1
Боже мой, как минимум 6 программистов думают, что элементарная математика непонятна. AFAIK, умножение IEEE 754 коммутативно (но не ассоциативно).
maaartinus
13
Возможно, вы упускаете суть. Это не имеет ничего общего с алгебраической правильностью. В идеальном мире вы должны иметь возможность просто разделить на два:, y = x / 2.0;но в реальном мире вам, возможно, придется уговорить компилятор выполнить менее затратное умножение. Возможно, менее понятно, почему y = x * (1.0 / 2.0);лучше, и было бы яснее заявить y = x * 0.5;вместо этого. Но измените значение 2.0на a, 7.0и я лучше посмотрю, y = x * (1.0 / 7.0);чем y = x * 0.142857142857;.
Jason S
3
Это действительно проясняет, почему использование вашего метода более разборчиво (и точно).
Хуан Мартинес,
21

Просто собираюсь добавить что-нибудь для опции «другие языки».
C: Поскольку это просто академическое упражнение, которое на самом деле не имеет значения, я подумал, что внесу что-то другое.

Я скомпилировал в сборку без оптимизации и посмотрел на результат.
Код:

int main() {

    volatile int a;
    volatile int b;

    asm("## 5/2\n");
    a = 5;
    a = a / 2;

    asm("## 5*0.5");
    b = 5;
    b = b * 0.5;

    asm("## done");

    return a + b;

}

составлено с gcc tdiv.c -O1 -o tdiv.s -S

деление на 2:

movl    $5, -4(%ebp)
movl    -4(%ebp), %eax
movl    %eax, %edx
shrl    $31, %edx
addl    %edx, %eax
sarl    %eax
movl    %eax, -4(%ebp)

и умножение на 0,5:

movl    $5, -8(%ebp)
movl    -8(%ebp), %eax
pushl   %eax
fildl   (%esp)
leal    4(%esp), %esp
fmuls   LC0
fnstcw  -10(%ebp)
movzwl  -10(%ebp), %eax
orw $3072, %ax
movw    %ax, -12(%ebp)
fldcw   -12(%ebp)
fistpl  -16(%ebp)
fldcw   -10(%ebp)
movl    -16(%ebp), %eax
movl    %eax, -8(%ebp)

Однако, когда я изменил эти ints на doubles (что, вероятно, сделал бы python), я получил следующее:

деление:

flds    LC0
fstl    -8(%ebp)
fldl    -8(%ebp)
flds    LC1
fmul    %st, %st(1)
fxch    %st(1)
fstpl   -8(%ebp)
fxch    %st(1)

умножение:

fstpl   -16(%ebp)
fldl    -16(%ebp)
fmulp   %st, %st(1)
fstpl   -16(%ebp)

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

В качестве примечания, вы можете видеть, что в моей программе main()возвращается a + b. Когда я уберу ключевое слово volatile, вы никогда не угадаете, как выглядит сборка (за исключением настройки программы):

## 5/2

## 5*0.5
## done

movl    $5, %eax
leave
ret

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

Извините за слишком длинный ответ.

Карсон Майерс
источник
1
Это не «единичная инструкция». Он просто постоянно сворачивался.
kvanberendonck
5
@kvanberendonck Конечно это единственная инструкция. Подсчитайте их: movl $5, %eax название оптимизации не важно и даже не имеет значения. Вы просто хотели снисходительно отреагировать на ответ четырехлетней давности.
Carson Myers
2
Природу оптимизации по-прежнему важно понимать, потому что она зависит от контекста: она применяется только в том случае, если вы добавляете / умножаете / делите / и т. Д. константы времени компиляции, когда компилятор может просто выполнить всю математику заранее и переместить окончательный ответ в регистр во время выполнения. Деление намного медленнее, чем умножение в общем случае (делители времени выполнения), но я полагаю, что умножение на обратные числа помогает только в том случае, если вы в любом случае делите на один и тот же знаменатель более одного раза. Вы, наверное, все это знаете, но начинающим программистам это может понадобиться подробно, так что ... на всякий случай.
Mike S
10

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

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

Если вам все еще интересно, с точки зрения оптимизации низкого уровня:

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

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

Аналогия: кладете пирог в микроволновую печь во время просмотра телешоу. Общее время, которое у вас ушло от телешоу, - это то, сколько времени потребовалось, чтобы положить его в микроволновую печь и вынуть из нее. В остальное время вы все еще смотрели телешоу. Таким образом, если на приготовление пирога ушло 10 минут, а не 1 минута, на самом деле это не отнимало у вас больше времени на просмотр телевизора.

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

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

Джеймс Подеста
источник
7

Напишите, что более четко выражает ваши намерения.

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

Не делайте этого наоборот.

Джей Базузи
источник
6

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

Позвольте компилятору сделать работу за вас.

Бути-окса
источник
5

Если вы работаете с целыми числами или типами без чисел с плавающей запятой, не забудьте операторы сдвига битов: << >>

    int y = 10;
    y = y >> 1;
    Console.WriteLine("value halved: " + y);
    y = y << 1;
    Console.WriteLine("now value doubled: " + y);
sbeskur
источник
7
эта оптимизация автоматически выполняется незаметно в любом современном компиляторе.
Дастин Гетц,
Кто-нибудь проверял, проверяет ли (используя битовые операции), имеет ли операнд (?) Версию с возможностью сдвига, чтобы использовать это вместо этого? function mul (a, b) {если (b равно 2) вернуть << 1; если (b равно 4) вернуть << 2; // ... и т.д. return a * b; } Я предполагаю, что IF настолько дорогой, что будет менее эффективным.
Кристофер Лайтфут,
Это совсем не похоже на то, что я себе представлял; Неважно.
Кристофер Лайтфут,
Для операций с константой работу должен делать обычный компилятор; но здесь мы используем python, поэтому я не уверен, достаточно ли он умен, чтобы знать? (Так должно быть).
Кристофер Лайтфут,
Хороший ярлык, разве что не сразу понятно, что на самом деле происходит. Большинство программистов даже не распознают операторы битового сдвига.
Blazemonger
4

На самом деле, есть веская причина, по которой, как правило, умножение происходит быстрее, чем деление. Аппаратное деление с плавающей запятой выполняется либо с помощью алгоритмов сдвига и условного вычитания («длинное деление» с двоичными числами), либо - что более вероятно в наши дни - с помощью итераций, подобных алгоритму Гольдшмидта . Для сдвига и вычитания требуется по крайней мере один цикл на бит точности (итерации почти невозможно распараллелить, как и сдвиг-и-сложение умножения), а итерационные алгоритмы выполняют по крайней мере одно умножение на итерацию.. В любом случае весьма вероятно, что деление займет больше циклов. Конечно, это не учитывает причуды в компиляторах, перемещение данных или точность. Однако по большому счету, если вы кодируете внутренний цикл в чувствительной ко времени части программы,0.5 * xили, 1.0/2.0 * xскорее, x / 2.0это разумный поступок. Педантизм «кодируйте самое ясное» абсолютно верно, но все три из них настолько близки по удобочитаемости, что педантизм в данном случае просто педантичен.

Ген
источник
3

Я всегда знал, что умножение более эффективно.

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

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

Дэн Хьюитт
источник
2

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

Но я более склонен ответить «на самом деле это не имеет значения», если только профилирование не показало, что деление является узким местом по сравнению с умножением. Однако я предполагаю, что выбор умножения или деления не окажет большого влияния на производительность вашего приложения.

mipadi
источник
2

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

Симус
источник
2

Будьте осторожны с «угадывать умножение, как правило, лучше, поэтому я стараюсь придерживаться этого, когда пишу код»,

В контексте этого конкретного вопроса «лучше» здесь означает «быстрее». Что не очень полезно.

Думать о скорости может быть серьезной ошибкой. В конкретной алгебраической форме вычислений есть серьезные ошибки.

См. Раздел Арифметика с плавающей запятой с анализом ошибок . См. Раздел « Основные вопросы арифметики с плавающей запятой и анализа ошибок» .

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

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

>>> for i in range(7):
...     a=1/(10.0**i)
...     b=(1/10.0)**i
...     print i, a, b, a-b
... 
0 1.0 1.0 0.0
1 0.1 0.1 0.0
2 0.01 0.01 -1.73472347598e-18
3 0.001 0.001 -2.16840434497e-19
4 0.0001 0.0001 -1.35525271561e-20
5 1e-05 1e-05 -1.69406589451e-21
6 1e-06 1e-06 -4.23516473627e-22

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

С. Лотт
источник
1

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

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

Кристофер Лайтфут
источник
1

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

Стив
источник
1

Java android, профилированный на Samsung GT-S5830

public void Mutiplication()
{
    float a = 1.0f;

    for(int i=0; i<1000000; i++)
    {
        a *= 0.5f;
    }
}
public void Division()
{
    float a = 1.0f;

    for(int i=0; i<1000000; i++)
    {
        a /= 2.0f;
    }
}

Полученные результаты?

Multiplications():   time/call: 1524.375 ms
Division():          time/call: 1220.003 ms

Деление примерно на 20% быстрее умножения (!)

PiotrK
источник
1
Чтобы быть реалистичным, вы должны тестировать a = i*0.5, а не a *= 0.5. Вот как большинство программистов будут использовать операции.
Blazemonger
1

То же, что и посты №24 (умножение быстрее) и №30, но иногда они оба так же легко понять:

1*1e-6F;

1/1e6F;

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

Крис
источник
1

Есть разница, но она зависит от компилятора. Сначала на vs2003 (c ++) у меня не было существенной разницы для типов double (64-битная плавающая точка). Однако, снова запустив тесты на vs2010, я обнаружил огромную разницу, до 4 раз быстрее при умножении. Отслеживая это, кажется, что vs2003 и vs2010 генерируют другой код fpu.

На Pentium 4, 2,8 ГГц, vs 2003:

  • Умножение: 8,09
  • Дивизион: 7.97

На Xeon W3530, vs2003:

  • Умножение: 4,68
  • Дивизион: 4.64

На Xeon W3530, vs2010:

  • Умножение: 5,33
  • Дивизион: 21.05

Похоже, что на vs2003 деление в цикле (так что делитель использовался несколько раз) было переведено в умножение на обратное. На vs2010 эта оптимизация больше не применяется (я полагаю, потому что результаты этих двух методов немного отличаются). Также обратите внимание, что процессор выполняет деление быстрее, как только ваш числитель равен 0,0. Я не знаю точного алгоритма, встроенного в чип, но, возможно, он зависит от числа.

Изменить 18-03-2013: наблюдение за vs2010

gast128
источник
Интересно, есть ли причина, по которой компилятор не может заменить, например, n/10.0выражением формы (n * c1 + n * c2)? Я ожидал, что на большинстве процессоров деление займет больше времени, чем два умножения и деление, и я считаю, что деление на любую константу может дать правильно округленный результат во всех случаях, используя указанную формулировку.
supercat 03
1

Вот глупый забавный ответ:

x / 2.0 не является эквивалентно х * 0,5

Допустим, вы написали этот метод 22 октября 2008 года.

double half(double x) => x / 2.0;

Теперь, 10 лет спустя, вы узнаете, что можете оптимизировать этот фрагмент кода. На этот метод ссылаются сотни формул в вашем приложении. Вы меняете его и получаете заметное улучшение производительности на 5%.

double half(double x) => x * 0.5;

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

double quantize(double x)
{
    if (half(x) > threshold))
        return 1;
    else
        return -1;
}

Итог: как только вы согласитесь на одно из двух, придерживайтесь его!

l33t
источник
1
Downvote? Как насчет комментария, объясняющего ваши мысли? Этот ответ определенно актуален на 100%.
l33t
В информатике умножение / деление значений с плавающей запятой на степени 2 осуществляется без потерь, если только значение не денормализуется или не выходит за пределы.
Soonts
Поскольку во время деления числа с плавающей запятой не без потерь, на самом деле не имеет значения, верно ли ваше утверждение. Хотя я был бы очень удивлен, если бы это было так.
l33t
1
«Плавающая точка не без потерь во время деления» только тогда, когда вы строите с использованием древнего компилятора, который испускает устаревший код x87. На современном оборудовании просто наличие переменной float / double без потерь, 32 или 64 бит IEEE 754: en.wikipedia.org/wiki/IEEE_754 Из-за того, как работает IEEE 754, когда вы делите на 2 или умножаете на 0,5, вы уменьшаете экспонента на 1, остальные биты (знак + мантисса) не меняются. И оба 2и 0.5числа могут быть представлены в IEEE 754 точно, без потери точности (в отличие, например, от 0.4или 0.1, они не могут).
Soonts
0

Что ж, если мы предположим, что операция сложения / вычитания стоит 1, тогда умножим затраты на 5 и разделим затраты примерно на 20.

matma
источник
Откуда у вас эти числа? опыт? внутреннее чувство? статья в инете? как бы они изменились для разных типов данных?
kroiz
0

После такого долгого и интересного обсуждения вот мой взгляд на это: на этот вопрос нет окончательного ответа. Как отмечали некоторые люди, это зависит как от оборудования (см. Piotrk и gast128 ), так и компилятора (см. Тесты @Javier ). Если скорость не критична, если вашему приложению не требуется обрабатывать в реальном времени огромный объем данных, вы можете выбрать для ясности использование деления, тогда как, если скорость обработки или загрузка процессора являются проблемой, умножение может быть самым безопасным. Наконец, если вы точно не знаете, на какой платформе будет развернуто ваше приложение, тест не имеет смысла. А для ясности кода достаточно одного комментария!

Жан-Франсуа
источник
-3

Технически деления не существует, есть просто умножение на обратные элементы. Например, вы никогда не делите на 2, вы фактически умножаете на 0,5.

«Деление» - давайте обманывать себя тем, что оно существует на секунду - всегда сложнее умножения, потому что для «деления» xна yединицу сначала нужно вычислить такое значение y^{-1}, y*y^{-1} = 1а затем произвести умножение x*y^{-1}. Если вы уже знаете, y^{-1}то не вычисление yдолжно быть оптимизацией.

satnhak
источник
3
Что полностью игнорирует реальность обеих команд, существующих в кремнии.
NPSF3000,
@ NPSF3000 - Я не слежу. Предполагая, что существуют обе операции, он просто утверждает, что операция деления неявно включает в себя вычисление обратного умножения и умножения, что всегда будет сложнее, чем просто выполнение одного умножения. Кремний - это деталь реализации.
satnhak
@ BTyler. Если обе команды существуют в кристалле, и обе команды занимают одинаковое количество циклов [как и следовало ожидать], то насколько сложны инструкции, совершенно не имеет значения с точки зрения производительности.
NPSF3000
@ NPSF3000 - но они не занимают одинаковое количество циклов, потому что умножение происходит быстрее.
satnhak