Какой самый быстрый способ получить абсолютное значение числа

Какой самый быстрый способ реализовать операцию, возвращающую абсолютное значение числа?

x=root(x²)

or

if !isPositive(x):
    x=x*(-1)

На самом деле этот вопрос можно перевести так: насколько быстр if (и почему, пожалуйста).

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


person Diones    schedule 20.03.2009    source источник
comment
Это абсолютное значение, а не модуль....   -  person kquinn    schedule 20.03.2009
comment
По крайней мере, здесь, в Румынии, мы используем английский эквивалент модуля/модуля для абсолютного значения. Я предполагаю, что это явление распространяется и на другие языки.   -  person Eduard - Gabriel Munteanu    schedule 20.03.2009
comment
Ах, в американском английском абсолютное значение - это расстояние от 0 на числовой прямой. То есть абсолютное значение -4 равно 4. абсолютное значение 12 равно 12.   -  person Perchik    schedule 20.03.2009
comment
Хотя кажется, что Википедия упоминает использование модуля в значении абсолютного значения: en.wikipedia.org/wiki/Absolute_value< /а>   -  person Eduard - Gabriel Munteanu    schedule 20.03.2009
comment
Я думаю, что эти англоговорящие пуристы не могут отличить модуль от модуля. Модуль — это допустимый английский термин для обозначения абсолютного значения действительного или комплексного числа.   -  person Violet Giraffe    schedule 06.11.2014
comment
Метод Square/SquareRoot также подвержен переполнению.   -  person    schedule 01.02.2020


Ответы (15)


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

Эмпирические правила из моих сборочных дней:

  • Целочисленная или побитовая операция: 1 цикл
  • Добавление/подчинение/множение с плавающей запятой: 4 цикла
  • div с плавающей запятой: ~ 30 циклов
  • Возведение в степень с плавающей запятой: ~ 200 циклов
  • sqrt с плавающей запятой: ~60 циклов в зависимости от реализации
  • Условная ветвь: ср. 10 циклов, лучше, если они хорошо спрогнозированы, гораздо хуже, если спрогнозированы неверно
person kquinn    schedule 20.03.2009
comment
Для fp add/sub/mul это задержки. Пропускная способность по-прежнему составляет не менее 1 за такт, если вы не ограничиваете задержку. Кроме того, целочисленное умножение составляет 3 цикла задержки на современном x86. См. руководства по оптимизации Agner Fog, чтобы узнать больше о разнице между пропускной способностью и задержкой для конвейерных ЦП (и выполнения вне очереди). - person Peter Cordes; 11.11.2018
comment
Также обратите внимание, что любой приличный компилятор увидит, что делает этот конкретный if, и скомпилирует его только в побитовую операцию, которая очищает знаковый бит числа с плавающей запятой или двойного числа (современные FPU, такие как x86 с SSE), или специальную инструкцию, такую ​​​​как устаревшая x87 fabs, которая делает то же самое на x87 FPU, который не поддерживает произвольные побитовые значения для чисел с плавающей запятой. - person Peter Cordes; 05.01.2021
comment
Или, по крайней мере, вы на это надеетесь; практика сложнее godbolt.org/z/4K5W61. Вот почему вы должны на самом деле использовать fabs(x) в C, который компилируется максимально эффективно, не беспокоя компилятор с нулевым знаком и специальным регистром NaN. например if (x<0) x = -x; или x = (x<0) ? -x : x; оба должны оставить только отрицательный ноль, потому что он сравнивает == 0,0). Но в любом случае, (-1)*x можно оптимизировать только до xorps, чтобы перевернуть бит знака. - person Peter Cordes; 05.01.2021

Существует отличный трюк для вычисления абсолютного значения целого числа с дополнением до 2 без использования оператора if. Теоретически, если значение отрицательное, вы хотите переключить биты и добавить один, в противном случае вы хотите передать биты как есть. XOR 1 переключает A, а XOR 0 оставляет A нетронутым. Итак, вы хотите сделать что-то вроде этого:

  uint32_t temp = value >> 31;     // make a mask of the sign bit
  value ^= temp;                   // toggle the bits if value is negative
  value += temp & 1;               // add one if value was negative

В принципе можно сделать всего за три инструкции по сборке (без ветки). И вы хотели бы думать, что функция abs(), которую вы получаете с math.h, делает это оптимально.

Нет ветвей == лучшая производительность. Вопреки ответу @paxdiablo выше, это действительно важно в глубоких конвейерах, где чем больше ветвей у вас есть в вашем коде, тем больше вероятность того, что ваш предсказатель ветвления ошибется и вам придется откатиться и т. д. Если вы избегаете ветвления, где возможно, в вашем ядре все будет продолжаться полным ходом :).

person vicatcu    schedule 15.01.2010
comment
кстати, это предполагает, что значение является int32_t (т.е. подписанным), если это не так, вы должны привести его как таковое, прежде чем сдвигать его - person vicatcu; 09.01.2012
comment
Вместо value += temp & 1 я предлагаю более простой value -= temp, и нет причин использовать беззнаковый тип для temp. - person Qwertie; 26.02.2013
comment
Также обратите внимание, что некоторые компиляторы на некоторых процессорах могут исключить ветвь, подразумеваемую (x < 0 ? -x : x), с помощью инструкции условного перемещения; в этом случае этот трюк не быстрее, чем стандартный Abs. - person Qwertie; 26.02.2013
comment
Я предполагаю, что это решение не сработает на архитектурах с обратным порядком байтов (например, Xbox 360). Я прав? - person Juan Campa; 01.06.2013
comment
ничего из этого не имеет для меня смысла, где я могу начать копать!?!?! :( - person Muhammad Umer; 09.08.2013
comment
Именно то, что я пришел сюда искать! Поэтому, если ваша ситуация допускает ошибку, равную единице, вы можете просто замаскировать бит знака! Почему я не подумал об этом? ржу не могу. - person Dmitri; 31.01.2014
comment
Хороший. Это лучше, чем ветвление; чтобы назвать одну причину: современные процессоры будут векторизовать это при использовании в цикле. См. graphics.stanford.edu/~seander/bithacks.html#IntegerAbs. для немного более общего подхода - и вы можете заметить патент, если вы находитесь в США. - person atlaste; 13.07.2015
comment
пфф зачем столько усилий? Есть ли причина, по которой ((value >> 31) | 1) * value недостаточно? умножение не дорого. - person M.kazem Akhgary; 25.12.2017
comment
XOR 1 инвертирует A, если вы читаете 1 как 1111...1. Предполагается, что правый сдвиг (››31) заполняет левую часть копиями самого левого но. Это называется арифметическим сдвигом. Хороший ответ, этот маленький момент смутил меня. - person Polymer; 09.01.2020

Вычисление квадратного корня, вероятно, одна из худших вещей, которые вы можете сделать, потому что это очень медленно. Обычно для этого есть библиотечная функция; что-то вроде Math.Abs(). Умножение на -1 также не нужно; просто верните -x. Поэтому хорошим решением будет следующее.

(x >= 0) ? x : -x

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

person Daniel Brückner    schedule 20.03.2009
comment
Почему этот ответ не имеет больше голосов?! Это компилируется в mov eax, edi; neg eax; cmovl eax, edi; ret и не требует никаких комментариев, чтобы объяснить все эти биты. - person Indiana Kernick; 10.06.2018

Для полноты вот способ сделать это для IEEE float в системах x86 на C++:

*(reinterpret_cast<uint32_t*>(&foo)) &= 0xffffffff >> 1;
person awdz9nld    schedule 26.07.2012
comment
@Stefnotch берет адрес 32-битной переменной с плавающей запятой foo, приводит к 32-битному целочисленному указателю без знака, разыменовывает его и применяет битовую маску, которая сохраняет все биты, кроме бита знака (MSB). - person awdz9nld; 13.07.2016
comment
Этот ответ неверен. Если вы удалите битовый знак -1, вы получите не 1, а вместо этого очень большое значение. Дополнение Lookup 2, чтобы понять, почему. - person Julien__; 21.12.2016
comment
@Julien__ Я думаю, вы неправильно понимаете, что здесь происходит. мы манипулируем необработанными битами числа с плавающей запятой - результирующий битовый шаблон используется не как целое число со знаком, а как число с плавающей запятой - person awdz9nld; 04.01.2017
comment
@MartinKällman, упс, ты прав. Моя ошибка. В то время я манипулировал целыми числами и пропустил плавающую часть ответа. - person Julien__; 04.01.2017

Какой самый быстрый способ получить абсолютное значение числа

Я думаю, что «правильного» ответа здесь нет. Вероятно, самый быстрый способ получить абсолютное число — использовать Intel Intrinsic. См. https://software.intel.com/sites/landingpage/IntrinsicsGuide/ и ищите «vpabs» (или другую встроенную функцию, которая выполняет работу для вашего процессора). Я почти уверен, что это превзойдет все остальные решения здесь.

Если вам не нравятся встроенные функции (или вы не можете их использовать или ...), вы можете проверить, достаточно ли умен компилятор, чтобы выяснить, является ли вызов «собственного абсолютного значения» (std::abs в С++ или Math.Abs(x) в С#) автоматически изменится на встроенный - в основном это включает просмотр дизассемблированного (скомпилированного) кода. Если вы используете JIT, убедитесь, что оптимизация JIT не отключена.

Если это также не дает вам оптимизированных инструкций, вы можете использовать метод, описанный здесь: https://graphics.stanford.edu/~seander/bithacks.html#IntegerAbs .

person atlaste    schedule 13.07.2015
comment
pabsd отлично подходит, если у вас есть массив значений или иначе вы можете хранить свои данные только в векторном регистре, но neg/cmov более эффективен, чем копирование из целочисленных регистров в XMM и обратно. Вы должны почти всегда использовать std::abs и позволить компилятору автоматически векторизовать, если он хочет, в противном случае эффективно встроить его. - person Peter Cordes; 11.11.2018

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

Извлечение квадратного корня из числа, вероятно, будет намного медленнее (метод Ньютона, например, будет использовать много-много операторов if на уровне машинного кода).

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

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

person paxdiablo    schedule 20.03.2009

Операция по модулю используется для нахождения остатка, вы имеете в виду абсолютное значение. Я изменил вопрос, потому что должно быть так: если !pos(x), то x = x*-1. (не пропало)

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

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

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

Умножение на -1 и проверка того, больше ли значение 0, можно свести к одной ассемблерной инструкции.

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

person Brian R. Bondy    schedule 20.03.2009
comment
Я предполагаю, что профессор думает о том, что операторы If заполняют конвейер. Чего, я уверен, больше не происходит в современных процессорах. - person Ray; 20.03.2009
comment
Этот профессор - идиот - вызовы функции root() также забивают конвейер. - person paxdiablo; 20.03.2009

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

Кроме того, вместо (x = x * -1) почему бы не сделать (x = 0 - x)? Может быть, компилятор оптимизирует их одинаково, но не проще ли второй?

person thomasrutter    schedule 20.03.2009
comment
Кроме того, вместо (x = x * -1) почему бы не сделать (x = 0 - x)? Может быть, компилятор оптимизирует их одинаково, но не проще ли второй? Конечно, я просто никогда так не думал... - person Diones; 20.03.2009

Вы используете сборку 8086? ;-)

                ; abs value of AX
   cwd          ; replicate the high bit into DX
   xor  ax, dx  ; take 1's complement if negative; no change if positive
   sub  ax, dx  ; AX is 2's complement if it was negative The standard
                : absolute value method works on any register but is much
                ; slower:

   or   bx, bx  ; see if number is negative
   jge  notneg  ; if it is negative...
   neg  bx      ; ...make it positive
notneg:         ; jump to here if positive

(явно украдено)

person Mark Maxham    schedule 20.03.2009
comment
Используйте test same,same, а не or same,same для эффективности (Проверьте, равен ли регистр нулю с CMP reg,0 по сравнению с OR reg,reg?). И если вы не программируете для настоящего древнего процессора, используйте cmov вместо условного перехода. - person Peter Cordes; 11.11.2018

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

person Neoheurist    schedule 21.10.2014

То, что быстрее, очень зависит от того, какой компилятор и на какой процессор вы ориентируетесь. На большинстве процессоров и всех компиляторах x = (x>=0)? х:-х; это самый быстрый способ получить абсолютное значение, но на самом деле часто стандартные функции уже предлагают это решение (например, fabs()). Он скомпилирован в сравнение, за которым следует инструкция условного присваивания (CMOV), а не в условный переход. Однако на некоторых платформах эта инструкция отсутствует. Хотя компилятор Intel (но не Microsoft или GCC) автоматически конвертировал бы if() в условное присваивание и даже пытался бы оптимизировать циклы (если это возможно).

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

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

Связанный с этим вопрос: Почему инструкция перехода ЦП медленная?

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

person Swift - Friday Pie    schedule 18.03.2015
comment
gcc выполняет if-преобразование в CMOV без веток. См. флаг оптимизации gcc -O3 делает код медленнее, чем -O2, в случае, когда он имеет неприятные последствия с отсортированными данными. sqrt — это одна инструкция на x86, но она медленная и доступна только для float/double/long double, а не для целых чисел. Показатели пропускной способности/задержки аналогичны (но медленнее) делению FP: деление с плавающей запятой и умножение с плавающей запятой. - person Peter Cordes; 11.11.2018
comment
Однако целочисленное умножение приятно и быстро. Не то, чтобы это вряд ли имело значение, это бесполезный строительный блок для abs. Просто требуется mov / neg/cmov, чтобы сделать это за 3 мкп с задержкой в ​​2 цикла. - person Peter Cordes; 11.11.2018

Я занимаюсь программированием ретро-графики на C для 8088/8086, и вызов abs() занимает много времени, поэтому я заменил его на:

/* assuming 'i' is int; this WILL NOT WORK on floating point */
if (i < 0) {
    i = ~i + 1;
}

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

person Neil C. Obremski    schedule 21.11.2016
comment
Любой современный компилятор может встроить abs в код, который компилируется не менее эффективно. (например, neg/cmov на современном x86). Самостоятельный взлом дополнения 2 бесполезен; вы могли бы также просто использовать i = -i, потому что x86 имеет инструкцию neg, которая быстрее, чем NOT / INC (на случай, если у вас есть наивный компилятор, который не распознает идентификатор дополнения 2 и оптимизирует его до neg или sub). - person Peter Cordes; 11.11.2018

Для полноты, если вы имеете дело с числами с плавающей запятой, вы всегда можете сделать что-то вроде n * sign(n), где sign — это функция, которая возвращает +1, если число положительное, и -1, если отрицательное. В C это будет что-то вроде copysign(1.0, n) или (n > 0) - (n < 0).

В настоящее время большинство машин используют IEEE 754 в качестве формата с плавающей запятой, поэтому вы можете напрямую очистить бит знака:

float fabs(float x) {
    char *c = &x;
    c[0] &= 7;
    return *(float *)c;
}

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

person Mad Physicist    schedule 30.07.2020

Интересно, если что-то не так с этим решением. Там есть

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

Может слишком много инструкций?

Мое решение

xabs = (x < 0)*(-x) + (x >=0)*x
  • 2 целочисленных сравнения
  • 2 умножения

Старое решение

xtest = (x < 0)*x;           // xtest = x if is negative, otherwise zero
xabs = (x - xtest) - xtest;  // Order of instructions taken into account

Неопределенное поведение при отрицании INT_MIN

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

xabs =   (x < -INT_MAX)*INT_MAX            //  x < -INT_MAX < 0  --> xabs = INT_MAX
       + ((x >= -INT_MAX)&&(x < 0))*(-x)   // -INT_MAX =< x < 0  --> xabs = -x
       + (x >= 0)*x                        // 0 <= x             --> xabs = +x
  • 5 целочисленных сравнений
  • 3 целочисленных умножения

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

if ( x < 0 )
{
  if ( x >= -INT_MAX )
  {
    x = -x;
  }
  else
  {
    x = INT_MAX;
  }
}
     
person ChaosOptimum    schedule 22.04.2021

Для списка отрицательных чисел:

если у вас в памяти хранится ноль, просто используйте 0 - x, где x — отрицательное число.

Или, если у вас нет нуля в памяти:

x-x-x, где x — отрицательное число.

Или, со скобками для ясности:

(x) - (x) - (x) => (-n) - (-n) - (-n), где x = -n

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

person Code Bag    schedule 08.09.2019