Как деление заменить умножением
Перейти к содержимому

Как деление заменить умножением

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

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

Потому что я думаю, что можно избежать:

Деление на ноль

Переполнение, когда oldValue очень мало

Это правильно? Есть ли проблема для этой привычки?

Два общих случая для рассмотрения:

Целочисленная арифметика

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

Арифметика с плавающей точкой

Помимо того факта, что деление может дать другой результат, когда оно делится на ноль (оно генерирует исключение, а умножение — нет), оно также может привести к несколько иным ошибкам округления и другому результату. Простой пример в C #:

Если вы мне не верите, вот Скрипка, которую вы можете выполнить и увидеть сами.

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

Заключение

Если вы работаете с нуля , вы, вероятно, в порядке.

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

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

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

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

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

Вы правы, что новая версия избегает деления на ноль. Конечно, вам не нужно добавлять охрану (по линии if (oldValue != 0) ). Но имеет ли это смысл? Ваша старая версия отражает соотношение между двумя числами. Если делитель равен нулю, то ваше соотношение не определено. Это может быть более значимым в вашей ситуации, т.е. Вы не должны давать результат в этом случае.

Защита от переполнения является дискуссионной. Если вы знаете, что newValue всегда больше, чем oldValue , то, возможно, вы могли бы сделать этот аргумент. Однако могут быть случаи, когда (oldValue * SOME_CONSTANT) также будут переполнены. Так что я не вижу здесь большого выигрыша.

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

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

Нет.

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

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

И в некоторых случаях даже выгодно «деоптимизировать» ради читабельности, ясности или ясности. В большинстве случаев ваши пользователи не заметят, что вы сохранили несколько строк кода или циклов ЦП, чтобы избежать обработки крайних случаев или обработки исключений. С другой стороны, неловкий или молчаливый код будет влиять на людей — по крайней мере, на ваших коллег. (А также, следовательно, стоимость создания и поддержки программного обеспечения.)

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

Также обратите внимание: компиляторы часто оптимизируют деление для вас в любом случае — когда это безопасно .

Используйте тот, который менее глючит и имеет более логичный смысл.

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

Вот несколько примеров, чтобы показать, что это зависит от ситуации:

Деление хорошее:

Умножение плохое:

Умножение хорошо:

Разделение плохое:

Умножение хорошо:

Разделение плохое:

Делать что-либо «по возможности» очень редко хорошая идея.

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

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

Что касается читабельности кода, я думаю, что умножение на самом деле более читабельно в некоторых случаях. Например, если есть что-то, что вы должны проверить newValue , увеличилось ли оно на 5 или более процентов выше oldValue , тогда 1.05 * oldValue есть порог, по которому нужно тестировать newValue , и естественно написать

Но остерегайтесь отрицательных чисел при рефакторинге вещей таким образом (либо заменяя деление умножением, либо заменяя умножение делением). Два условия, которые вы рассматривали, эквивалентны, если oldValue гарантированно не будет отрицательным; но предположим, что newValue на самом деле -13,5 и oldValue -10,1. затем

оценивается как истина , но

оценивается как ложное .

Компилятор фактически делает умножение, если целое число инвариантно! Не разделение. Это происходит даже для не степеней 2 значения. Сила 2 делений использует явно битовые сдвиги и поэтому еще быстрее.

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

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

Подумайте также об архитектуре, на которой работает ваш код. Особенно ARM имеет крайне медленное деление; вам нужно вызвать функцию для деления, в ARM нет инструкции деления.

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

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

И наоборот, что будет, если oldValue оно очень большое? У вас те же проблемы, только наоборот.

Если вы хотите избежать (или минимизировать) риск переполнения / недополнения, лучший способ — проверить, newValue является ли он наиболее близким по величине к oldValue или к SOME_CONSTANT . Затем вы можете выбрать соответствующую операцию деления, либо

и результат будет наиболее точным.

Для деления на ноль, по моему опыту, это почти никогда не подходит для «решения» в математике. Если у вас есть деление на ноль в ваших непрерывных проверках, то почти наверняка у вас есть ситуация, которая требует некоторого анализа, и любые вычисления, основанные на этих данных, не имеют смысла. Явная проверка деления на ноль почти всегда является подходящим ходом. (Обратите внимание, что здесь я говорю «почти», потому что я не претендую на непогрешимость. Я просто отмечу, что я не помню, чтобы видел веские причины для этого через 20 лет написания встроенного программного обеспечения, и продолжаю .)

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

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

Деление через умножение на обратное число

Если тебя интересует сам алгоритм, то смысл его сводится к следующему: операцию деления Z/D можно заменить на умножение Z*(1/D). Весь вопрос в том, как определить 1/D. Для этого используются два метода — разложение в ряд Тейлора и метод Ньютона-Рафсона.
Первый: D=1+X, тогда 1/D = (1-X)*(1+X^2)*(1+X^4)*(1+X^8)*(1+X^16)*.
Второй сводится к решению уравнения f(X) = 1/X — D = 0, т.е. X = 1/D, которое может быть найдено с помощью реккурентного соотношения
X(i+1) = X(i)*(2-X(i)D).

все проще мне нужно просто разделить двоичную «1» на делитель, и потом сдвинуть дробную часть — в сектор целой, и полученное двоичное целое умножить на делимое ))

Так действительно проще. Только как ты будешь делить 1 на делимое?

так — как еслибы это было число с плавающей точкой: например 1/2 будет выглядеть 0000.1000 — теперь сдвину дробную часть в область целой, и умножу делимое на 1000 ))

Если тебя это устраивает, то замечательно. Просто это совсем не замена деления умножением.

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

Урок 37 Бесплатно Деление

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

Деление отрицательных чисел

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

Деление отрицательных чисел

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

Пример:

Допустим надо разделить -15 на -5

1) Найдем модули от этих чисел:

2) Посчитаем частное этих двух чисел:

Это и будет ответ.

Пример:

Разделим -132 на -3

1) Находим модули этих чисел:

2) Посчитаем частное модулей:

Это правило работает для нецелых чисел:

1) Считаем модули:

2) Выполняем деление:

И еще несколько примеров уже менее подробно:

Пройти тест и получить оценку можно после входа или регистрации

Деление чисел с разными знаками

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

На сколько изменилась зарплата каждого конкретного работника?

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

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

Деление чисел с разными знаками

Воспользуемся им для решения задачи:

1) Берем модули чисел:

2) Считаем частное:

3) И приписываем к результату минус:

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

Теперь посмотрим, как разделить положительное число на отрицательное.

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

Деление чисел с разными знаками

Допустим, необходимо разделить 161 на -7:

1) Посчитаем модули:

2) Посчитаем частное:

3) И приписываем к нему минус:

Это и будет ответом.

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

Правило: чтобы посчитать частное чисел с разными знаками, необходимо посчитать частное их модулей и приписать к нему минус.

Пройти тест и получить оценку можно после входа или регистрации

Сведение деления к умножению

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

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

И это верно для отрицательных чисел тоже.

Сведение деления к умножению

Посмотрим, как это происходит на примерах.

1-й способ: воспользоваться правилом для деления отрицательных чисел:

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

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

Такой же подход работает и для деления чисел с разными знаками.

Пример:

1-й способ: воспользуемся правилом для деления чисел с разными знаками:

2-й способ: заменим деление на умножение и воспользуемся правилом для умножения чисел с разными знаками:

Можно заметить, что результаты совпадают.

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

Пройти тест и получить оценку можно после входа или регистрации

Определение знака частного

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

Правило: частное двух отрицательных чисел всегда число положительное

Пример:

Частное \(\mathbf<-32>\) и \(\mathbf<-4>\) будет больше нуля.

Правило: частное положительного числа и отрицательного меньше нуля

Пример 1:

Частное 45 и \(\mathbf<-5>\) будет меньше нуля.

Пример 2:

Частное \(\mathbf<-36>\) и 3-х будет меньше нуля.

Также вне зависимости от знаков делить на 0 нельзя ни положительное, ни отрицательное число.

делить на 0 нельзя

И если делимое равно нулю, то и частное будет равняться нулю (если такое деление вообще возможно, то есть если делитель не равен нулю).

если делимое равно нулю, то и частное будет равняться нулю

Правило: если делимое равно нулю, а делитель — нет, то частное также равняется нулю.

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

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

Ведь частное двух положительных чисел, очевидно, будет положительным.

А по правилу, частное двух отрицательных чисел равно частному модулей этих чисел, то есть частному положительных чисел.

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

Мы умеем представлять деление как умножение.

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

Как видите, есть разные способы это доказать.

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

Или же можно снова пойти по аналогии с умножением.

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

С нулем также можно прибегнуть к аналогии с умножением.

И аналогично тому, как умножение нуля на отрицательное число даст 0, то и деление 0 на отрицательное число, будет нулем.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *