Кажется, задача вычисления абсолютного значения (или модуля) числа совершенно тривиальна. Если число отрицательно, давайте сменим знак. Иначе оставим как есть. На Java это будет выглядеть примерно так:
public static double abs(double value) {
if (value < 0) {
return -value;
}
return value;
}
Вроде бы это слишком просто даже для вопроса на собеседовании на позицию джуна. Есть ли тут подводные камни?
Вспомним, что в стандарте IEEE-754 вообще и в Java в частности есть два нуля: +0.0 и -0.0. Это такие братья-близнецы, их очень легко смешать и перепутать, но вообще-то они разные. Разница проявляется не только в текстовом представлении, но и в результате выполнения некоторых операций. Например, если поделить единицу на +0.0 и -0.0, то мы получим кардинально разные ответы: +Infinity и -Infinity, отличие между которыми уже сложно игнорировать. Однако, например, в операциях сравнения +0.0 и -0.0 неразличимы. Поэтому реализация выше не убирает минус у -0.0. Это может привести к неожиданным результатам. Например:
double x = -0.0;
if (1 / abs(x) < 0) {
System.out.println("oops");
}
Казалось бы, обратное к модулю x
число не может быть отрицательным, какое бы ни было x
. Но в данном случае может. Если у вас есть садистские наклонности, попросите джуна на собеседовании написать метод abs
. Когда же он выдаст код вроде того что в начале статьи, можете спросить, выполнится ли при каком-нибудь x
условие 1 / abs(x) < 0
. После таких собеседований про вашу компанию будут ходить легенды.
Ну ладно, проблему мы нашли. А как её исправить? Наивно добавить if (value < 0 || value == -0.0)
не получится, потому что +0.0 == -0.0
. В итоге мы сделаем ещё хуже: теперь будет выдаваться -0.0
для положительного нуля на входе. Чтобы надёжно отличить отрицательный нуль, есть метод Double.compare
:
public static double abs(double value) {
if (value < 0 || Double.compare(value, -0.0) == 0) {
return -value;
}
return value;
}
Это работает. Но метод становится ужасно медленным для такой тривиальной операции. Double.compare
устроен не так уж просто, нам потребуется пара дополнительных сравнений для положительного числа, три сравнения для -0.0 и целых четыре сравнения для +0.0. Если посмотреть на реализацию Double.compare
, можно понять, что нам нужна только часть связанная с doubleToLongBits
. Этот метод реинтерпретирует битовое представление double
-числа как битовое представление long
-числа (и там, и там восемь байт). А со сравнением целых чисел никаких сюрпризов нет. Поэтому можно упростить так:
private static final long MINUS_ZERO_LONG_BITS =
Double.doubleToLongBits(-0.0);
public static double abs(double value) {
if (value < 0 ||
Double.doubleToLongBits(value) == MINUS_ZERO_LONG_BITS) {
return -value;
}
return value;
}
Однако, оказывается, doubleToLongBits
тоже не совсем тривиален, потому что он канонизирует NaN'ы. Есть много способов закодировать not-a-number в виде double
, но только один из них канонический. Эти разные NaN'ы совсем-совсем близнецы, их не отличишь ни сравнением через Double.compare
, никакой операцией, ни строковым представлением. Но в памяти компьютера они выглядят по-разному. Чтобы не было сюрпризов, doubleToLongBits
приводит любой NaN
к каноническому виду, который записывается в long
как 0x7ff8000000000000L
. Конечно, это лишние проверки, которые нам здесь тоже не нужны.
Что же делать? Оказывается, можно использовать doubleToRawLongBits
, который никаких умностей с NaN
'ами не делает и возвращает всё как есть:
private static final long MINUS_ZERO_LONG_BITS =
Double.doubleToRawLongBits(-0.0);
public static double abs(double value) {
if (value < 0 ||
Double.doubleToRawLongBits(value) == MINUS_ZERO_LONG_BITS) {
return -value;
}
return value;
}
Этот метод JIT-компилятор в идеале может вообще удалить полностью, потому что речь идёт просто про реинтерпретацию набора бит в процессоре, чтобы типы данных сошлись. А сами биты остаются одни и те же и процессору обычно наплевать на типы данных. Хотя говорят, что всё-таки это может привести к пересылке из регистра с плавающей точкой в регистр общего назначения. Но всё равно очень быстро.
Ладно, у нас осталось два ветвления для всех положительных чисел и нулей. Всё равно кажется, что много. Мы знаем, что ветвления — это плохо, если branch predictor не угадает, они могут очень дорого стоить. Можно ли сделать меньше? Оказывается, можно любой нуль превратить в положительный, если вычесть его из 0.0
:
System.out.println(0.0-(-0.0)); // 0.0
System.out.println(0.0-(+0.0)); // 0.0
Таким образом, можно написать:
public static double abs(double value) {
if (value == 0) {
return 0.0 - value;
}
if (value < 0) {
return -value;
}
return value;
}
Зачем так сложно, спросите вы. Ведь можно просто вернуть 0.0 в первом условии. Кроме того, у нас всё равно два сравнения. Однако можно заметить, что для обычных отрицательных чисел 0.0 - value
и просто -value
дают одинаковый результат. Поэтому первые две ветки легко схлопнуть в одну:
public static double abs(double value) {
if (value <= 0) {
return 0.0 - value;
}
return value;
}
Отлично, у нас теперь всегда одна ветка. Победа? Но как насчёт сделать всегда ноль веток? Возможно ли это?
Если посмотреть на представление числа double в стандарте IEEE-754, можно заметить, что знак — это просто старший бит. Соответственно, нам нужно просто безусловно сбросить этот старший бит. Остальная часть числа при выполнении этой операции не меняется. В этом плане дробные числа даже проще целых, где отрицательные превращаются в положительные через двоичное дополнение. Сбросить старший бит можно через операцию &
с правильной маской. Но для этого надо интерпретировать дробное число как целое (и мы уже знаем как это сделать), а потом интерпретировать назад (для этого есть longBitsToDouble
, и он тоже практически бесплатный):
public static double abs(double value) {
return Double.longBitsToDouble(
Double.doubleToRawLongBits(value) & 0x7fffffffffffffffL);
}
Этот способ действительно не содержит ветвлений, и профилирование показывает, что пропускная способность метода при определённых условиях увеличивается процентов на 10%. Предыдущая реализация с одним ветвлением была в стандартной библиотеке Java с незапамятных времён, а вот в грядущей Java 18 уже закоммитили улучшенную версию.
В ряде случаев, впрочем, эти улучшения ничего не значат, потому что JIT-компилятор может использовать соответствующую ассемблерную инструкцию при её наличии и полностью проигнорировать Java-код. Например, на платформе ARM используется инструкция VABS. Так что пользы тут мало. Но всё равно интересная статья получилась!
Комментарии (102)
netricks
16.08.2021 09:54+12Суть проблемы понятна. А почему бы просто не возвращать положительный ноль, если x==0?
tagir_valeev Автор
16.08.2021 10:11+4Хороший вариант! Но отдельное условие для нуля всё равно будет.
AxisPod
16.08.2021 10:18+1Math.signum(value) * value
Не?
tagir_valeev Автор
16.08.2021 12:35+1signum - тоже нетривиальная операция с условиями. Да и умножение не на константу. Заметно медленнее будет скорее всего. Но я не проверял!
torbasow
16.08.2021 10:34+43Пишем языки высокого уровня.
Обнаруживаем, что они работают как-то не так.
Оперируем битами вручную.static_cast
16.08.2021 11:15+13Ага, забавно каким алгоритмическими кровями автор приходит к тривиальной для любого сишника реализации. При этом финальное решение полностью ортогонально концепции оригинального языка.
ainoneko
16.08.2021 18:42+1к тривиальной для любого сишника реализации.
Причём то, что реализация работает и для
(я проверял), вообще говоря, не обязано было получиться (если не знать, как там внутри всё устроено).Float.NEGATIVE_INFINITY
(Напоминает историю (нагуглить не удалось), когда в советское время отдел программистов переходил с ассемблера на ЯВУ (не Джаву ^_^).
Почти у всех (кроме одного) эффективность программ упала.
Причина успеха единственного — он представлял, в какие ассемблерные инструкции транслировалась программа )
GospodinKolhoznik
16.08.2021 20:18+1На сях же решение будет чуть чуть отличаться. Я правда не в курсе как сейчас в си, но раньше же количество бит в double зависело от архитектуры машины, а значит это надо как то в коде учитывать.
А на джаве действительно проще, код будет работать хоть на суперкомпьютере, хоть на микроконтроллере.
Miiko
16.08.2021 20:50+3На джаве действительно проще - ни на суперкомпьютере, ни на микроконтроллере она работать просто не будет ;).
Чудес-то не бывает - если количество бит в "родном" double другое, чем прописано в стандарте языка, то эффективно реализовать этот тип на данной архитектуре будет в принципе невозможно.
novoselov
16.08.2021 11:27В java можно сделать intrinsic функцию и писать там что хочешь.
tagir_valeev Автор
16.08.2021 12:36+5Стоит уточнить, что авторы виртуальной машины могут это сделать. Обычный пользователь JVM не имеет такой роскоши :-)
SlFed
16.08.2021 10:43+1А если так :
double x = -0.0;
x= x-1+1;После этого разве x не равен +0.0 ?
ksbes
16.08.2021 10:47Даже не оптимизатор, а просто компилятор эту строку выкинет.
ShadowTheAge
16.08.2021 16:17+9Не имеет права выкидывать. Только если в компиляторе стоит опция типа fast math (она никогда не стоит по умолчанию). Потому что floating point math не ассоциативна и не дистрибутивна.
tagir_valeev Автор
16.08.2021 12:34+1Вариант интересный, но на низком уровне может быть гораздо более сложный. Вряд ли это будет быстрее, чем `0.0 - x`.
AlexMih
16.08.2021 14:41+7Вспомнилось...
"...В 60-е - 70-е годы, когда компьютеры были большими, каждая линейка компьютеров имела свою программную реализацию вычислений с плавающей запятой, свои форматы представления чисел, точность, представимые диапазоны и правила округления. Соответственно, чудеса, вроде описанных в "Неочевидных особенностях вещественных чисел", были у каждого свои. По воспоминаниям старожилов, на некоторых машинах число могло выглядеть отличным от нуля в операциях сравнения и сложения, но быть чистым нулем при умножении и делении. Чтобы без страха поделить на такое число, его следовало умножить на 1.0 и лишь потом сравнить с нулем. А другие машины могли выдать ошибку переполнения при умножении на 1.0 вполне нормального числа. Были такие малюсенькие числа (но не нули), которые давали переполнение при делении на самих себя. В программах были обычными шаманские вставки вроде X = (X + X) - X. Соответственно, одна и та же программа, даже написанная на стандартном FORTRAN'е, могла давать разные результаты на разных машинах..."
(с) Загадки округления
Dima_Sharihin
16.08.2021 11:10+5Сначала пишут на жабе, а потом выжимают проценты из тривиальных операций.
Как хорошо замечено в конце статьи, все современные FPU имеют аппаратную инструкцию взятия модуля, которая просто сбрасывает бит знака для IEEE-754 представления.
tagir_valeev Автор
16.08.2021 12:37+10Так джава - очень высокопроизводительный язык! В ней всегда выжимают проценты.
dolovar
16.08.2021 11:12+7в операциях сравнения +0.0 и -0.0 неразличимы
Если на входе будет +0.0, то будет ли на выходе победа?System.out.println(-0.0-(+0.0)); // -0.0
Отлично, у нас теперь всегда одна ветка. Победа?if (value <= 0) { return -0.0 - value; }
tagir_valeev Автор
16.08.2021 12:31+1О, спасибо! Я действительно немножко оплошал и неправильно объяснил. Поправил статью.
speshuric
16.08.2021 11:26+11Всегда, когда вижу числа с плавающей точкой (double/float) в задачах отличных от физики, начинаю паниковать и включается режим "сапёра" с тщательным анализом граничных случаев и максимальной изоляции этого кода. Если вижу double/float в финансовых приложениях, "дёргаю стоп-кран" и предлагаю избавиться от него. Если вижу битовые манипуляции с такими типами, то паникую еще больше (но, к сожалению, если уж пришлось прийти к битовым манипуляциям над флоатами, то это не от хорошей жизни и обычно оправдано).
95% разработчиков легко допускает ошибки в работе с этими типами на уровне "обычных" операций (сравнить на равенство, сложить-вычесть в неправильном порядке, применить ассоциативность/коммутативность/дистрибутивность не думая, преобразования в другие типы и т.п.). Не меньше 80% разработчиков не напишут корректно даже элементарного метода Гаусса с первой попытки.
Причём не стоит думать, что "ну я -то точно легко справлюсь". До нас в этой ловушке побывали и разработчики процессоров, и разработчики ОС, и разработчики Excel, и разработчики стандартных библиотек.
PS: а статья @lany, как всегда, хороша тем, что заставляет подумать.
PPS: угадайте, что меня напрягает в JavaScript :)
JustDont
16.08.2021 11:50-1В JS есть прекрасная целочисленная математика. Если только вы сами собственными руками не возьмете float.
sergeyns
16.08.2021 12:35вижу double/float в финансовых приложениях
Хм, а как вы от них избавляетесь? Очень часто невозможно предсказать какую минимальную дробность могут принимать значения. При расчете какой-нибудь себестоимоимости запросто может быть важен 4-5-6 знак после запятой.. Вводить сразу дробность в 10е-9 ? Тогда вам может int64 не хватить...
speshuric
16.08.2021 13:48+7Полный ответ и в статью не поместится, не то что в комментарий. Потому и "стоп-кран". Дальше надо смотреть на конкретную задачу и требования. Где-то перейти в BigDecimal, где-то изолировать плавающую точку, где-то изменить исходную задачу. При расчёте себестоимости в ядре вполне может быть (чаще всего должна быть) СЛАУ, которую, наверное, можно считать в double и изолировать, "но это не точно". Но оставлять незащищенным числовой тип данных в котором нельзя написать
if (a==b)
(а кто-нибудь так напишет) страшновато.
Tschumin
16.08.2021 14:15согласен.
Вариант, если просто поступать так - разделяя, например, центы и доллары на целочисленные разделы и работая в BigInteger? насколько я помню BigDecimal тоже проблему не снимают....
AlexMih
16.08.2021 15:48А сколько будет стоить капля спирта в вашей системе?
0 долларов 0 центов? Накапайте мне ведерко...
suns
17.08.2021 04:28К слову, у некоторых банков есть ровно такая проблема при конвертации валют, позволяющая получить профит от конвертации маленьких сумм
Решается все просто - либо делают изначально высокие лимиты, либо мониторят
noittom
17.08.2021 21:04Можно не обращать внимания пока идут внутренние вычисления, а на выходи форматировать.
Ну и непримитивные типы это мусор (память)
Bakuard
16.08.2021 14:56+5Не меньше 80% разработчиков не напишут корректно даже элементарного метода Гаусса с первой попытки.
Надо заметить, что корректная реализация метода Гаусса с учетом всех нюансов - задача не такая уж и простая. На бумаге он, конечно, элементарен. Но это на бумаге.
speshuric
16.08.2021 17:42+1Да, но с другой стороны, это базовая (модельная) задача. Реальные задачи зачастую гораздо больше и сложнее.
Beholder
16.08.2021 13:00Ну а просто посмотреть, как сделано в JDK, который оптимизируют и шлифуют уже много-много лет?
@HotSpotIntrinsicCandidate public static double abs(double a) { return (a <= 0.0D) ? 0.0D - a : a; }
Причём аннотация вот эта означает, что реально в машинном коде может быть вставлено что-то другое, та же операция с битами или инструкция FPU (fabs).
tagir_valeev Автор
16.08.2021 13:25+11То есть вы думаете, что я сослался на пулл-реквест, где этот код изменён на более свежий, но не глянул, что было до этого? :-)
tagir_valeev Автор
16.08.2021 13:26Аннотированных так методов, кстати, в разы больше, чем настоящих интринсиков. Поэтому доверять этой аннотации нельзя, надо смотреть конкретно в исходники JVM (для C2 - opto/library_call.cpp)
gorilych
16.08.2021 14:35+1это собеседование по Java (в которой надо просто использовать библиотечный метод, а не заниматься ерундой) или на знание стандарта IEEE?
Amomum
16.08.2021 14:57+4Double.doubleToRawLongBits(value) & 0x7fffffffffffffffL);
я бы записал какDouble.doubleToRawLongBits(value) & ~(1L<<63));
ибо так лучше видно, что это 63 бит (ну и так немножко короче).
orionll
16.08.2021 16:04+1Интересно, что в джавадоке метода реализация без ветвления упомянута, причём ещё с Java 9
As implied by the above, one valid implementation of this method is given by the expression below which computes a double with the same exponent and significand as the argument but with a guaranteed zero sign bit indicating a positive value: Double.longBitsToDouble((Double.doubleToRawLongBits(a)<<1)>>>1)
Странно, что только в Java 18 её догадались скопировать из джавадока в сам метод )))
tagir_valeev Автор
16.08.2021 18:16+1Там не всё так просто. Когда этот тикет появился, с интринсиками дела обстояли туго, и всякие
doubleToRawLongBits
были реально медленными, поэтому такое изменение не имело смысла. Потом времена изменились.static_cast
16.08.2021 19:17А там разве не что-то типа return *((long*)(&a)) внутри?
tagir_valeev Автор
16.08.2021 19:19+1Java вообще не так работает. В JNI методе можно оно и так, только на сам JNI оверхед будет не меньше сотни наносекунд.
IBAH_II
16.08.2021 16:50-1inline float absF(float a) { return (*(((unsigned long*)(&a))))&0x7FFFFFF;} // :)
maxim_ge
16.08.2021 17:38Math.abs
не избавляет от чудес:double a = Math.abs(0. / 0.); double b = a; if ( a != b ) { System.out.println("oops"); }
Понятно, что
Math.abs()
тут для отвода глаз, но тем не менее. Проблема не в том, что велосипедный abs() сработал некорректно, а в том, что поделили на ноль. На ноль делить нельзя, даже на -0., и это надо проверять перед делением:double x = -0.0; if ( x == 0. ) { System.out.println("oops, x == 0."); }
ainoneko
16.08.2021 18:56Проблема не в том, что велосипедный abs() сработал некорректно, а в том, что поделили на ноль. На ноль делить нельзя, даже на -0.,
По-моему, проблема (если она есть) тут в том, что «не-число» (в данном случае это должно быть «любое число», так как 0*икс == 0 при любом «обычном» икс) не равно «не-числу», что логично (как и NULL в SQL).
(А делить на ноль в Java можно, и результат даже получается достаточно разумным.)maxim_ge
16.08.2021 19:28(А делить на ноль в Java можно, и результат даже получается достаточно разумным.)
Не всегда:
int x = 0; int y = 0; System.out.println("x / y = " + (x / y));
В целом, согласен, есть определенный смысл в -/+ Infinity для вычислений с плавающей точкой, но мне пока не выпал случай этот смысл сознательно использовать.
Получить исключение от целочисленного деления на ноль гораздо более реально.
diakin
16.08.2021 18:26public static double abs(double value) { if (value+1 < 1) { return -value; } return value; }
Не проканает?
flx0
16.08.2021 20:37+1Нет.
-1e-100 + 1 == 1MacIn
16.08.2021 23:38Это из-за приведения? А с 1.0?
flx0
17.08.2021 00:06+1Нет, это из-за формата, в котором хранится floating point. Оно не просто так называется числом с плавающей точкой. Оно состоит из мантиссы и порядка (и их знаковых бит) в виде
M * 2^E
Так, в 64-битном double под мантиссу M отведено 52 бита и 10 под порядок E.
Чтобы сложить два числа, их мантиссы нужно выровнять битовым сдвигом так чтобы точка оказалась в одном и том же месте. Если порядки этих чисел различаются на 52 и больше, то при сдвиге мантиссы меньшего из них какой-либо информации об ее значении в двоичном представлении числа просто не останется. Если разница меньше, то потеряется часть точности.
usa_habro_user
16.08.2021 18:35+2Вспомним, что в стандарте IEEE-754 вообще и в Java в частности есть два нуля: +0.0 и -0.0.
А в чем вообще физический/практический смысл наличия положительного и отрицательного нулей в Java?
Punk_Joker
16.08.2021 19:17+3Это особенность не Java как таковой. А формата кодирования чисел с плавающей запятой, которому следуют большинство ЯП.
usa_habro_user
16.08.2021 19:31-3Я сомневаюсь, что приведенный в начале статьи пример с "-infinity" и "+infinity", также будет работать для C# или C++.
И что именно в "формате кодирования чисел с плавающей" мешает существованию одного единственного нуля? Проясните, пожалуйста (я без издевки спрашиваю, если что).johndow
16.08.2021 19:51Полагаю то, что для знака выделен 1 бит и всё (0, infinity, NaN хоть для него знак и не имеет смысла) может быть либо отрицательным либо положительным.
Makeman
18.08.2021 01:55В C# и C++ работает так же, поскольку поведение
double
стандартизировано и в конечном счёте сводится к одним и тем же арифметическим инструкциям процессора, какой бы язык мы ни использовали.Насколько сам понимаю, в формате с плавающей запятой под знак числа выделен отдельный бит, то есть отрицательный ноль получается сам собой из положительного путём инвертирования знакового бита. В целочисленной же арифметике отрицательные числа записываются в дополнительном коде (как такового знакового бита нет), из-за чего само собой выходит, что +0 и -0 побитово эквивалентны, если же инвертировать у +0 условный "знаковый" бит, то получится уже не -0, а минимально возможное отрицательное число, например, -128 для типа
byte
.
TakashiNord
16.08.2021 20:19-1а может так, сойдет?
tolerance = 0.00000001
EQ_ge { s t } { return ( s > (t - tolerance) ) }
Abs { v } {
if EQ_ge { v 0.0 } { return v }
return ((-1)*v)
}
:)
KvanTTT
16.08.2021 20:37Интересно, а в .NET как модуль вычисляется?
AnarchyMob
17.08.2021 23:17KvanTTT
19.08.2021 11:50+2Ага, а в ассемблере они раскрываются вот в такое (x64):
C.Abs(Double) L0000: vzeroupper L0003: vmovsd xmm0, [C.Abs(Double)] L000b: vandps xmm0, xmm0, xmm1 L000f: ret
Видимо
vandps
и обнуляет знаковый бит.На x86 используются команды FPU:
C.Abs(Double) L0000: fld qword [esp+0x4] L0004: fabs L0006: ret 0x8
По ссылке доступны функции для целых и вещественных типов. Что удивительно — ассемблерный код для первых содержит больше инструкций, чем для вторых.
shybovycha
17.08.2021 03:16-1Напомнило факториал на хаскеле.
Не спорю, раз в столько-то там лет оно может и будет иметь какое-нибудь значение в проекте, но подозреваю, что в большинстве случаев
#define abs(x) x < 0 ? -x : x
будет точно так же эффективно в плане перформанса и многократно эффективнее в плане читабельности.tagir_valeev Автор
18.08.2021 12:43+2В плане и перформанса, и читабельности в Java, разумеется, надо использовать метод стандартной библиотеки, а не изобретать велосипед. Статья не предполагает, что написание такого метода вам необходимо для вашего сурового энтерпрайза. Статья помогает разобраться во внутреннем устройстве вещей, которые нас окружают.
alex103
17.08.2021 07:31+6(шутка)
А давайте придумаем числа, чтобы посчитать сколько у нас яблок..
А давайте придумаем такое число 0, когда у нас нет ни одного яблока..
А давайте придумаем отрицательные числа, чтобы посчитать какого количества яблок у нас нету..
.... а давайте придумаем такое число -0, чтобы указать что у нас нету ни одного яблока, которого у нас нету..
О! А давайте придумаем функцию Abs() ...
Xao_Fan-Tzilin
17.08.2021 09:35Хмм, а разве нельзя представить число (хоть 0, хоть -0.0, хоть +0.0) в строковом виде, а там сделать проверку наличия символа "минус" (или как он там будет называться - дефис, тире или ещё как), при нахождении его удалить и полученную строку снова конвертировать в числовой вид и дальше уже оперировать с ним..?
Мнение непросвещённого, так что не обессудьте.
tagir_valeev Автор
17.08.2021 09:37+3Это ужасно медленно. Невообразимо медленно. Может сотни наносекунд занять.
Xao_Fan-Tzilin
17.08.2021 09:48Но по идее правильно? На Луа такое прокатывает.
В принципе, можно и без проверки на наличие минуса вытащить без минуса.. :) Но я догадываюсь, что всё равно ответ будет - "медленно".
tagir_valeev Автор
18.08.2021 12:41+5Ну это из разряда проверять истинность булевой переменной через
String.valueOf(myFlag).length() == 4
. Работает, конечно, вопросов нет. Может есть языки, где такой подход идиоматичен.
key08rus
17.08.2021 12:14+1public static double abs(double value) {
return Double.longBitsToDouble(
Double.doubleToRawLongBits(value) & 0x7fffffffffffffffL);
}
Если без «магии» и платформонезависимо, то, наверное, лучше просто copySign()public static double abs(double value) {
return Math.copySign(value,1);
}tagir_valeev Автор
17.08.2021 12:26Так это решение абсолютно платформонезависимо! А copySign внутри сделает то же самое. Возможно, JIT даже докрутит ваш вариант до моего.
key08rus
17.08.2021 12:32Глянул в исходники Math, там действительно похоже. Только не конкретная маска используется, а (DoubleConsts.SIGN_BIT_MASK). Подозреваю, что эта константа точно равна 0x7fffffffffffffffL (я так то не знаток java, я эмбед-сишник, поэтому с типами ооочень осторожен)
UPD:public static double copySign(double magnitude, double sign) { return Double.longBitsToDouble((Double.doubleToRawLongBits(sign) & (DoubleConsts.SIGN_BIT_MASK)) | (Double.doubleToRawLongBits(magnitude) & (DoubleConsts.EXP_BIT_MASK | DoubleConsts.SIGNIF_BIT_MASK))); }
Так что вместо 0x7fffffffffffffffL можно (нужно) подставить
(DoubleConsts.EXP_BIT_MASK | DoubleConsts.SIGNIF_BIT_MASK) и JIT докрутит до конкретного значения (=0x7fffffffffffffffL)
и итоговая функция будетpublic static double abs(double value) { return Double.longBitsToDouble(Double.doubleToRawLongBits(value) & (DoubleConsts.EXP_BIT_MASK | DoubleConsts.SIGNIF_BIT_MASK)); }
tagir_valeev Автор
17.08.2021 14:09(DoubleConsts.EXP_BIT_MASK | DoubleConsts.SIGNIF_BIT_MASK)
докрутит javac. Это по стандарту константа времени компиляции, она уже в байткод ляжет в виде конкретного числа. А вот сделать инлайнинг, подставить параметр sign и вычислить(Double.doubleToRawLongBits(sign) & (DoubleConsts.SIGN_BIT_MASK)
до константы 0 и удалить - уже задача JIT-компилятора (вполне посильная).
MKMatriX
17.08.2021 20:45-1Я понимаю что js не джава и т.д. но можно x<<1>>1 (обнулить двумя сдвигами первый бит), ну или хотя бы x & MAX_POSITIVE_DOUBLE, а если у вас есть поинтеры, то можно вообще по поинтеру переписать первый бит.
Или в джаве с этими способами туго?
INSTE
18.08.2021 11:47signed integer overflow это норм в JS?
MKMatriX
18.08.2021 13:46Просто сдвиг происходит в окне памяти переменной, этот бит не уйдет куда-то в overflow. Конечно для языков с нормальным управлением памятью это минус, но в js это нужно крайне редко. Вроде sharedArrayBuffer для этого есть. В общем в js уже даже null pointer (x is undefined) редко встречается. Скриптовый язык)
INSTE
18.08.2021 22:23Что такое «окно памяти переменной»?
MKMatriX
19.08.2021 09:37Область оперативки под эту значение этой переменной, очевидно что в js напрямую управлять поинтерами и давать доступ к физической памяти нельзя, (т.е. с поинтерами чуть-чуть можно). И нельзя ни при каких условиях выходить за рамки виртуалки под вкладку. В противном случае js из одной вкладки браузера мог бы, теоретически, читать оперативку всего компа. Впрочем зачастую он это и может, например если не стоят патчи от spectre и meltdown и их аналогов для amd. В таком грустном мире живем)
INSTE
19.08.2021 11:22А сколько бит выделено под эту переменую? Насколько ее можно сдвинуть влево?
MKMatriX
19.08.2021 11:46Я подозреваю бит под значение, ибо переменная в js это не только ее значение, грубо говоря все переменные в js это enum т.е. занимают весьма много места изначально вне зависимости от типа.
Сдвигать можно насколько угодно, просто лишние биты удалятся, а вместо недостающих вставится 0. Это ожидаемое поведение ибо в js нельзя просто так поставить переменные в памяти рядом (можно). По размерам... Не знаю насколько js стандартизирован, поэтому теоретически размеры переменных в разных браузерах/системах/ноде могут быть разными, но если верить https://exploringjs.com/impatient-js/ch_numbers.html то числа с точкой это 64 бита, и 53 для инта, только инт нифига не инт)) Т.е. есть еще Bigints которые не ограниченны. Но сдвигать можно все равно на любое число, просто свдиг больше размера числа его обнулит. Также сдвиг приводит число с точно к инту, типа здраствуйте магическая константа из дума) Т.е. ```123.123>>0 === 123 //true```
mayorovp
19.08.2021 17:10Ваше решение даже в js работать не будет, потому что это решение для целых, а надо для вещественных.
MKMatriX
20.08.2021 02:19-1Руководствовался больше названием) Да в js битовые операции числам с точкой недоступны. Когда-то писал себе утилитку чтобы их хотя бы посмотреть в битовом виде. Однако в js -0 это всегда int XD поэтому в принципе всей этой мороки не будет) Тем более я не решение предлагал, а спрашивал будет ли это работать?)
Т.е. изначально мой вопрос можно было сформулировать как: "А почему нельзя просто обнулить первый бит"?
Ведь это не требует ветвлений, или даже чтения переменной до конца. Основная проблема это NaN которые тоже Double. Впрочем в конце статьи уже есть это решение и про NaN ни слова, видимо NaN в этом бите не нуждается.
mayorovp
20.08.2021 11:54+2Однако в js -0 это всегда int XD поэтому в принципе всей этой мороки не будет)
Да ладно?
> const abs = x => x < 0 ? -x : x; < undefined > 1 / abs(-0) < -Infinity
MKMatriX
21.08.2021 15:30-1Эм, я про мороку с тем, что эти способы в js с даблом не сработают да и перейдут сразу в Math.abs)
Правда если верить https://galactic.ink/journal/2011/11/bitwise-gems-and-other-optimizations/ то ваше решение все равно будет работать быстрее хоть и не будет работать для -0. Хотя глядя на 1 / (-0 >> 0) === Infinity (тоже самое с parseInt и parseFloat для -0) можно решить что -0 в js не обязательно int.
mayorovp
21.08.2021 17:52-0 в js не обязательно int
Вообще-то в js нет никаких int, есть только number.
И есть операции, которые работают с number как c целым числом — в частности, сдвиги.MKMatriX
22.08.2021 00:02Я не про тип в js, а про то как число хранится в памяти. Ибо целые, большие целые и с точкой хранятся по разному. Да и большие целые формально в js не number. Плюс через sharedArrayBuffer типы чуть более явные. Но его в js не часто используют, разве что игры оптимизировать.
mayorovp
22.08.2021 00:39А какая разница как оно там хранится в памяти? Это всего лишь деталь реализации, но если спецификация JS требует различать -0 и +0 — они будут различаться, иначе это баг и он рано или поздно будет исправлен.
FGV
А смысл деления на 0 какой? Собственно как только получили инф или нан дальше уже можно не считать, т.к. или ошибка входных данных или кривая математика.
Torvald3d
Никто не будет специально делить на ноль - ноль может получиться в результате вычислений. Смысл деления на ноль есть, например в компьютерной графике (в шейдерах) при вычислении освещения может получится деление на ноль и, грубо говоря, получив бесконечность пиксель может быть белым, а минус бесконечность - черным. Лишние условия в шейдере - потеря производительности, поэтому если такой момент стандартизирован, то лучше им пользоваться.
INSTE
В посте все же явно не шейдер на жаве пишут.
funny_falcon
Я встречал пример с построением графика. Система с поддержкой -0.0 рисовала правильный график, а без поддержки - с артефактами. И как раз из-за подобного деления на переменную, стремящуюся к нулю (и в какой-то момент становящейся +0 или -0), где-то внутри формулы.
К сожалению, давно это было, ссылку не отыщу.
Makeman
Поведение
double
стандартизировано, поэтому рассмотренные подходы, скорее всего, будут справедливы и для множества других языков программирования.INSTE
double в java гарантированно хранится в ieee754, это в спеке так описано? Если да, то вопросов нет.
tagir_valeev Автор
Как хранится - это личное дело виртуальной машины. В спеке описано поведение. Поведение соответствует IEEE-754 с некоторыми упрощениями (например, никаких signalling NaN нету).
FGV
дык а не проще для этого случая проверку предусмотреть? а то иначе придется предусматривать проверку после деления на инф :)
Torvald3d
Это относительно дорогая операция на гпу. Особенно, если у вас вектор из 4х значений, часть из которых - нули.