Быстрый целочисленный sqrt с использованием Math.Sqrt

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

Я подумываю просто привести значение к удвоению и использовать Math.Sqrt, который, как я предполагаю, имеет аппаратное ускорение и должен быть достаточно быстрым. Но могу ли я рассчитывать на то, что это будет совершенно детерминировано на всех платформах для всех возможных входных данных (скажем, uint64), включая приведение обратно к целому числу? Судя по моим поискам в Google по этому вопросу, кажется, что sqrt с плавающей запятой обычно детерминирован на разных платформах (в отличие, например, от cos/tan), но знает ли кто-нибудь больше об этом?

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

ipodtouch0218 25.07.2024 21:48

Можете ли вы отказаться от поддержки 32-битного x86? Это одна из самых больших «проблемных платформ» из-за странностей 80-битных промежуточных результатов x87.

user555045 25.07.2024 21:57

Большинство аппаратных алгоритмов sqrt в наши дни точно округлены, но путем тестирования невозможно гарантировать, что в 2^64 нет ни одного особого случая, где они различаются. Чтобы тщательно проверить каждую возможность, потребуется около 4 x 10 ^ 9 с ~ 130 лет. Вы можете уменьшить его до 0,26 года для каждого 53-битного значения мантиссы и нечетного/четного показателя степени. Мы полагаемся на формальные доказательства того, что sqrt округляется правильно. На новейшем оборудовании sqrt скорость почти такая же, как у деления, что весьма впечатляет. Если вам нужно быть уверенным, примените одну итерацию int64 NR к результату FP sqrt.

Martin Brown 25.07.2024 22:10
en.wikipedia.org/wiki/…
Dmitry Bychenko 25.07.2024 23:23

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

jdweng 25.07.2024 23:23

возможно, было бы намного лучше использовать Math.ReciprocalSqrtEstimate и Math.ReciprocalEstimate и выполнить еще несколько итераций Ньютона-Рафсона

phuclv 26.07.2024 05:14

@ user555045 на самом деле поддержка x87 для 64-битных целочисленных аргументов полной точности без каких-либо округлений означает, что в этом случае он должен возвращать идеальные результаты. Это реальный мир округления до ближайшего, который вызывает проблемы с обычной двойной точностью.

Martin Brown 26.07.2024 15:13

@MartinBrown отличается от других - это проблема, и даже если x87 может вычислить точный ответ, он недетерминирован при использовании из языка высокого уровня: в любой момент 80-битное число с плавающей запятой может быть перенесено в память и перезагружено как 64-битное число с плавающей запятой. , когда компиляторы захотят это сделать.

user555045 26.07.2024 21:18
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
8
111
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

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

sqrt(0) == 0
sqrt(1) == 1
sqrt(2) == 1
sqrt(3) == 1
sqrt(4) == 2
sqrt(5) == 2
...
sqrt(48) == 6
sqrt(49) == 7
... 

Если это ваша задача и вы хотите получить детерменистический результат на разных платформах, придерживайтесь целочисленных значений, например O(log(n)) алгоритм временной сложности

  public static int IntSqrt(int n) {
    ArgumentOutOfRangeException.ThrowIfNegative(n);

    var c = 0;
    var d = 1 << 30; 
                         
    while (d > n) 
      d >>= 2;
    
    for (int x = n; d != 0; d >>= 2) 
      if (x >= c + d) {      
        x -= c + d;        
        c = (c >> 1) + d;  
      }
      else 
        c >>= 1;           
 
    return c; 
  } 

Скрипка

Подробности см. https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Binary_numeral_system_.28base_2.29

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

Ollhak 26.07.2024 09:00

Итак, что мне нужно знать, так это могу ли я вызвать (int)Math.Sqrt((double)integerValue) для любого целочисленного значения (ulong), могу ли я рассчитывать на получение одного и того же результата на всех платформах, например для различных процессоров и операционных систем?

Ollhak 26.07.2024 09:02

@Ollhak: Я сомневаюсь, что аппаратное обеспечение NPU различается, по крайней мере, для Intel (у них внутри 80-битная логика, так называемая «расширенная» или «длинная двойная») и AMD (они работают с 64-битными числами с плавающей запятой). Технически мы можем попытаться принудительно использовать только 64-битный fp, но в Java (где есть ключевое слово «strictfp») и в этом случае компиляторы добавляют нежелательное округление.

Dmitry Bychenko 26.07.2024 09:49

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

Вот метод проверки:

for (long expect = 0, min = 0, odd = 1; expect <= 46340; //sqrt(max-int)
    expect++, min += odd, odd += 2)
{
    var max = min + odd - 1;
    if ((int)Math.Sqrt(min) != expect || (int)Math.Sqrt(max) != expect)
        throw new NotSupportedException();
}

Говоря оптимистично, современные чипы должны быть способны выдавать правильные результаты для sqrt в диапазоне int.

Ответ принят как подходящий

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

Существует также более тревожная проблема, когда входное значение превышает 2^52, поэтому мантисса двойной плавающей точки не может точно представлять входное значение y. Для некоторых неудачных значений y округление до ближайшего приведет к ложному ответу на x, который не удовлетворяет x*x <= y. Они довольно редки. Я выбрал их и получил 1:10^8 в начале проблемы и 3:10^7 для чисел >2^63.

Я не обнаружил никаких сбоев для y < 10^11, но это капля в море по сравнению с общим диапазоном int64. Мне кажется, что вы можете безопасно использовать sqrt(y) при условии, что вы очистите результат, чтобы защититься от редких исключений, когда ошибка округления вызывает проблемы (и правила округления или защитные цифры могут немного отличаться в зависимости от некоторых более быстрых процессоров).

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

#include <iostream>
#include <math.h>
#include <inttypes.h>

unsigned int safesqrt(uint64_t y)
{
  // defined to *always* give a deterministic result such that x*x <= y
  // sqrt(y) becomes tricky when y exceeds the 53 bit mantissa of double FP 
  // ironically I think this is one example where x87 80 bit FP would win out!
  // Its 64 bit mantissa can exactly represent every possible integer input.
  // 
  // double results round high for a small fraction of values (approx 1 in 10^8 rising to 3 in 10^7)
  // edge values for y where the mantissa cannot represent the full precision and 
  // sqrt(x) returns a rounded to nearest integer value are the problem

  int x = (unsigned) sqrt((double)y);
  uint64_t x2;
  x2 = x;
  x2 *= x2;
  if (x2 > y) x--; // defend against fail high when y > 0x0010 0000 0000 0000, 2^52
  return x;
}

int main()
{
  uint64_t base, x2, y, z;
  double dx;
  unsigned x, nrx, nrx2, lastbadx = 0, badcount = 0;
  int i = 1;
  base = 1 ;
  base <<= 62;
  printf("base = %18I64x\n", base);
  printf("     N \t\t  y\t\t\t x^2  \t\t     x (double)     (int) x \t NR(x) \t Nr(NR(x))  bad\n");
  for (y = base; y<base+100000000000; y++)
  {
      dx = sqrt((double)y);
      x = (unsigned)(dx);
      x2 = (uint64_t)x;
      x2 *= x2;
      if (x2 > y)  // in an ideal world this shouldn't happen
      {
          nrx = (int)((y / x + x) / 2);
          nrx2 = (int)((y / nrx + nrx) / 2);  // check to see if NR converges or oscillates
          if (x != lastbadx)
          {
              printf(" %5u : %20I64u  %20I64u  %18.10gf  %10u %10u %10u", i++, y, x2, dx, x, nrx, nrx2);
              lastbadx = x;
              badcount++;
          }
          else
              badcount++;
      }
      else
      {
          if (badcount) printf(" [%i]\n", badcount);
          badcount = 0;
      }
  }
}

Я оставил его включенным на ночь, и он обнаружил около 1400 плохих случаев в блоке y>2^63. Кстати, всего за несколько минут я не обнаружил никаких ошибок в первых целочисленных значениях 10 ^ 11 (поэтому 10 ^ 12, 10 ^ 13 можно было бы легко проверить методом перебора). Это все еще капля в море по сравнению с полным динамическим диапазоном 10^19.

Вы также можете защититься от сбоя, проверив это x2+2*x+1 > y, но я думаю, что способ округления до ближайшего и аппаратного sqrt с последующим усечением до целого числа, шансы на то, что это когда-либо сработает, исчезающе малы.

Математически доказано, что sqrt(x*x)== x для всех двойных значений IEEE-754, где x*x не переполняется и не переполняется: IEEE double такой, что sqrt(x*x) ≠ x , квадратный корень из квадрата x равен x, т. е. если x*x нет inf, то извлечение квадратного корня вернет исходное значение, даже если мантисса была округлена

phuclv 29.07.2024 08:48

@phuciv Спасибо за подтверждение. Он загружает 64-битное целое число в переменную с двойной плавающей запятой с 53-битной мантиссой и 11-битной экспонентой, которая округляется в большую сторону (т. е. до ближайшего) в нескольких критических крайних случаях, когда x > 2^52. Все ошибки происходят тогда, когда sqrt(y) является точным целочисленным значением. Возможно, мне следует добавить это наблюдение к ответу?

Martin Brown 29.07.2024 13:54

Другие вопросы по теме

Похожие вопросы

Консоль неправильно выводит следующую строку
Как заставить мою функцию правильно использовать обратный вызов после завершения асинхронных функций?
После перемещения файла в другое место и последующего создания файла с тем же именем в исходном месте время создания неверно
Как отправить команды в приложение WPF для отображения всплывающих подсказок и значков на панели задач с помощью именованных каналов IPC в С#?
Почему C# DateTime.Now/DateTime.UtcNow опережает SYSUTCDATETIME()/SYSDATETIME() SQL Server, хотя код C# выполняется до SQL-запроса
Как я могу использовать шаблон объявления вне условия if?
Получить документы/файлы с диска Share Point
Получение «System.IO.FileNotFoundException: не удалось загрузить файл или сборку Azure.Core, версия = 1.38.0.0» в приложении-функции Azure
Как динамически устанавливать дополнительные свойства в моделях с несколькими запросами в ASP.NET Core Web Api
Поиск самого длинного словаря. Ключевое совпадение во фразе