Всякий раз, когда мне нужно деление, например, проверка условий, я хотел бы реорганизовать выражение деления на умножение, например:
Оригинальная версия:
if(newValue / oldValue >= SOME_CONSTANT)
Новая версия:
if(newValue >= oldValue * SOME_CONSTANT)
Потому что я думаю, что можно избежать:
Деление на ноль
Переполнение, когда
oldValue
очень мало
Это правильно? Есть ли проблема для этой привычки?
oldValue >= 0
?Ответы:
Два общих случая для рассмотрения:
Целочисленная арифметика
Очевидно, что если вы используете целочисленную арифметику (которая усекает), вы получите другой результат. Вот небольшой пример в C #:
Выход:
Арифметика с плавающей точкой
Помимо того факта, что деление может дать другой результат, когда оно делится на ноль (оно генерирует исключение, а умножение - нет), оно также может привести к несколько иным ошибкам округления и другому результату. Простой пример в C #:
Выход:
Если вы мне не верите, вот Скрипка, которую вы можете выполнить и увидеть сами.
Другие языки могут отличаться; Имейте в виду, однако, что C #, как и многие языки, реализует стандартную библиотеку IEEE (IEEE 754) с плавающей запятой, поэтому вы должны получить те же результаты в других стандартизированных временах выполнения.
Заключение
Если вы работаете с нуля , вы, вероятно, в порядке.
Если вы работаете над унаследованным кодом, а приложение является финансовым или другим чувствительным приложением, которое выполняет арифметику и должно обеспечивать согласованные результаты, будьте очень осторожны при переключении между операциями. Если вам необходимо, убедитесь, что у вас есть модульные тесты, которые обнаружат любые незначительные изменения в арифметике.
Если вы просто делаете такие вещи, как подсчет элементов в массиве или другие общие вычислительные функции, вы, вероятно, будете в порядке. Однако я не уверен, что метод умножения делает ваш код более понятным.
Если вы реализуете алгоритм в спецификации, я бы вообще ничего не изменил, не только из-за проблемы округления ошибок, но и чтобы разработчики могли просмотреть код и отобразить каждое выражение обратно в спецификацию, чтобы убедиться, что реализации нет. недостатки.
источник
Мне нравится ваш вопрос, поскольку он потенциально охватывает много идей. В целом, я подозреваю, что ответ - это зависит , вероятно, от задействованных типов и возможного диапазона значений в вашем конкретном случае.
Мой первоначальный инстинкт - размышлять о стиле , т.е. Ваша новая версия менее понятна читателю вашего кода. Я полагаю, мне придется подумать секунду или две (или, возможно, дольше), чтобы определить намерение вашей новой версии, тогда как ваша старая версия сразу станет понятной. Читаемость является важным атрибутом кода, поэтому в вашей новой версии есть цена.
Вы правы, что новая версия избегает деления на ноль. Конечно, вам не нужно добавлять охрану (по линии
if (oldValue != 0)
). Но имеет ли это смысл? Ваша старая версия отражает соотношение между двумя числами. Если делитель равен нулю, то ваше соотношение не определено. Это может быть более значимым в вашей ситуации, т.е. Вы не должны давать результат в этом случае.Защита от переполнения является дискуссионной. Если вы знаете, что
newValue
всегда больше, чемoldValue
, то, возможно, вы могли бы сделать этот аргумент. Однако могут быть случаи, когда(oldValue * SOME_CONSTANT)
также будут переполнены. Так что я не вижу здесь большого выигрыша.Может быть аргумент, что вы получаете лучшую производительность, потому что умножение может быть быстрее, чем деление (на некоторых процессорах). Тем не менее, должно быть много расчетов, подобных этим, чтобы получить значительный выигрыш, т.е. остерегайтесь преждевременной оптимизации.
Размышляя над всем вышеизложенным, в целом, я не думаю, что можно получить что-то большее с вашей новой версией по сравнению со старой версией, особенно с учетом снижения ясности. Однако могут быть конкретные случаи, когда есть некоторая выгода.
источник
Нет.
Вероятно, я бы назвал эту преждевременную оптимизацию в широком смысле, независимо от того, оптимизируете ли вы производительность , как обычно относится эта фраза, или что-либо еще, что можно оптимизировать, например, счетчик ребер , строки кода или в более широком смысле, такие вещи, как «дизайн».
Реализация такого рода оптимизации в качестве стандартной операционной процедуры подвергает риску семантику вашего кода и потенциально скрывает границы. Крайние случаи, которые вы считаете целесообразными для бесшумного устранения, в любом случае, возможно, должны быть явно рассмотрены . И бесконечно легче отлаживать проблемы вокруг шумных краев (тех, которые генерируют исключения), а не тех, которые молча терпят неудачу.
И в некоторых случаях даже выгодно «деоптимизировать» ради читабельности, ясности или ясности. В большинстве случаев ваши пользователи не заметят, что вы сохранили несколько строк кода или циклов ЦП, чтобы избежать обработки крайних случаев или обработки исключений. С другой стороны, неловкий или молчаливый код будет влиять на людей - по крайней мере, на ваших коллег. (А также, следовательно, стоимость создания и поддержки программного обеспечения.)
По умолчанию все, что является более «естественным» и читаемым в отношении домена приложения и конкретной проблемы. Держите это простым, явным и идиоматическим. Оптимизируйте по мере необходимости для получения значительных выгод или для достижения законного порога юзабилити.
Также обратите внимание: компиляторы часто оптимизируют деление для вас в любом случае - когда это безопасно .
источник
Используйте тот, который менее глючит и имеет более логичный смысл.
Обычно деление на переменную в любом случае является плохой идеей, так как обычно делитель может быть нулевым.
Деление на константу обычно просто зависит от логического значения.
Вот несколько примеров, чтобы показать, что это зависит от ситуации:
Деление хорошее:
Умножение плохое:
Умножение хорошо:
Разделение плохое:
Умножение хорошо:
Разделение плохое:
источник
(ptr2 - ptr1) * 3 >= n
что его так же легко понять, как выражениеptr2 - ptr1 >= n / 3
? Это не заставит твой мозг сломаться и вернуться, пытаясь расшифровать смысл утроения различий между двумя указателями? Если это действительно очевидно для вас и вашей команды, тогда я полагаю, что для вас это больше; Я просто должен быть в медленном меньшинстве.n
и произвольное число 3 сбивают с толку в обоих случаях, но, замененные разумными именами, нет, я не нахожу ни одну более запутывающей, чем другую.Делать что-либо «по возможности» очень редко хорошая идея.
Ваш приоритет номер один должен быть правильностью, сопровождаемой удобочитаемостью и ремонтопригодностью. Слепая замена деления на умножение, когда это возможно, часто приводит к сбою в отделе правильности, иногда только в редких случаях, и поэтому трудно найти случаи.
Делайте то, что правильно и наиболее читабельно. Если у вас есть веские доказательства того, что написание кода наиболее читабельным способом вызывает проблемы с производительностью, вы можете рассмотреть возможность его изменения. Уход, математика и кодовые обзоры - ваши друзья.
источник
Что касается читабельности кода, я думаю, что умножение на самом деле более читабельно в некоторых случаях. Например, если есть что-то, что вы должны проверить
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 лет написания встроенного программного обеспечения, и продолжаю .)
Однако, если у вас есть реальный риск переполнения / недополнения в вашем приложении, то это, вероятно, не правильное решение. Скорее всего, вам следует обычно проверять числовую стабильность вашего алгоритма или, возможно, просто переходить к более точному представлению.
И если у вас нет доказанного риска переполнения / недостаточного заполнения, то вы ни о чем не беспокоитесь. Это означает, что вы буквально должны доказать, что вам это нужно, с цифрами, в комментариях рядом с кодом, который объясняет сопровождающему, почему это необходимо. Как главный инженер, проверяющий чужой код, если бы я столкнулся с кем-то, кто прилагал дополнительные усилия, я лично не согласился бы на меньшее. Это своего рода противоположность преждевременной оптимизации, но обычно она имеет одну и ту же основную причину - одержимость деталями, которая не имеет функциональной разницы.
источник
Инкапсулируйте условную арифметику в значимых методах и свойствах. Мало того, что хорошее именование скажет вам, что означает «A / B» , проверка параметров и обработка ошибок также могут быть аккуратно скрыты.
Важно отметить, что поскольку эти методы состоят из более сложной логики, внешняя сложность остается очень управляемой.
Я бы сказал, что замена умножения кажется разумным решением, потому что проблема плохо определена.
источник
Я думаю, что не может быть хорошей идеей заменить умножения на деления, потому что процессор ALU (Arithmetic-Logic Unit) выполняет алгоритмы, хотя они реализованы аппаратно. Более сложные методы доступны в новых процессорах. Обычно процессоры стремятся распараллелить операции пар битов, чтобы минимизировать требуемые тактовые циклы. Алгоритмы умножения можно распараллелить довольно эффективно (хотя требуется больше транзисторов). Алгоритмы деления не могут быть распараллелены так эффективно. Самые эффективные алгоритмы деления довольно сложны. Как правило, они требуют больше тактов на бит.
источник