Как я могу улучшить этот метод квадратного корня?

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

Пока что у меня это:

public static double SquareRoot(double x) {
    if (x == 0) return 0;

    double r = x / 2; // this is inefficient, but I can't find a better way
                      // to get a close estimate for the starting value of r
    double last = 0;
    int maxIters = 100;

    for (int i = 0; i < maxIters; i++) {
        r = (r + x / r) / 2;
        if (r == last)
            break;
        last = r;
    }

    return r;
}

Он отлично работает и каждый раз дает точно такой же ответ, что и метод Math.Sqrt() .NET Framework. Однако, как вы, наверное, догадались, это медленнее, чем нативный метод (примерно на 800 тиков). Я знаю, что этот конкретный метод никогда не будет быстрее, чем собственный метод, но мне просто интересно, есть ли какие-либо оптимизации, которые я могу сделать.

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

И прежде чем вы скажете: «Почему бы просто не использовать вместо этого Math.Sqrt()?»... Я делаю это в качестве учебного упражнения и не собираюсь фактически использовать этот метод в каком-либо производственном коде.


person David Brown    schedule 13.05.2009    source источник


Ответы (12)


Во-первых, вместо проверки на равенство (r == last) вы должны проверять сходимость, где r близко к last, где close определяется произвольным эпсилоном:

eps = 1e-10  // pick any small number
if (Math.Abs(r-last) < eps) break;

Как упоминается в статье в Википедии, на которую вы ссылаетесь, вы не эффективно вычисляете квадратные корни с помощью метода Ньютона - вместо этого вы используете логарифмы.

person Ankur Goel    schedule 13.05.2009
comment
опечатка: s/метод Ньютона/метод Вавилона -- метод Ньютона отлично подходит для скорости сходимости (с некоторыми оговорками относительно того, сходится ли он) - person Jason S; 13.05.2009
comment
Если корень больше 2^52*eps, то вполне возможно, что r колеблется вокруг корня и что Math.Abs(r-last) никогда не меньше, чем eps. Следовательно, хотя это предложение немного лучше, чем исходная программа, оно все же может привести к гораздо большему количеству итераций, чем необходимо. - person Accipitridae; 14.05.2009
comment
Это на самом деле сбрасывает около 100 тиков, что кажется странным, потому что он выполняет дополнительный метод вместе со сравнением. Но я не буду жаловаться. ;) - person David Brown; 14.05.2009
comment
@David: я уверен, что Math.Abs ​​встроен JIT - person BlueRaja - Danny Pflughoeft; 01.02.2010

float InvSqrt (float x){
  float xhalf = 0.5f*x;
  int i = *(int*)&x;
  i = 0x5f3759df - (i>>1);
  x = *(float*)&i;
  x = x*(1.5f - xhalf*x*x);
  return x;}

Это мой любимый быстрый квадратный корень. На самом деле это обратный квадратный корень, но вы можете инвертировать его после, если хотите .... Я не могу сказать, будет ли это быстрее, если вам нужен квадратный корень, а не обратный квадратный корень, но это чертовски круто все равно .
http://www.beyond3d.com/content/articles/8/

person Chris H    schedule 01.02.2010
comment
Чертовски сумасшедший, хотя я не думаю, что это возможно даже в C # - person BlueRaja - Danny Pflughoeft; 01.02.2010
comment
Вы можете создать объединение для решения проблемы с указателем, просто используя StructLayoutAttribute с LayoutKind.Explicit. - person John Leidegren; 23.09.2012

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

person Jasiu    schedule 13.05.2009
comment
+1, алгоритмические улучшения обычно превосходят микрооптимизации, такие как замена деления битовым сдвигом. - person Richard; 13.05.2009
comment
Я не понимаю, как использование другого алгоритма является хорошим ответом на вопрос, как мне выполнить этот алгоритм быстрее? Он уже объяснил, что делает это просто потому, что хочет, поэтому советовать ему сделать что-то совершенно другое — бесполезное предложение. - person Chad Birch; 13.05.2009
comment
Метод Ньютона быстро сходится и вообще не представляет проблемы. Настоящая проблема — это первое приближение. С#, по-видимому, не позволяет возиться с битами, что возможно в C/С++. - person Accipitridae; 14.05.2009

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

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

person Richard    schedule 13.05.2009

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

person AlbertoPL    schedule 13.05.2009
comment
Сдвиг битов отлично работает для int (и т. д.), но работает ли он для double? Кажется, это даже не определено... - person Marc Gravell; 13.05.2009
comment
Разрыв/возврат по сравнению с возвратом настолько минимален; Я не думаю, что вы когда-нибудь заметите разницу здесь - person Marc Gravell; 13.05.2009
comment
Он пытается спасти клещей, поэтому я предлагаю даже самые тривиальные вещи. - person AlbertoPL; 13.05.2009
comment
+1! Я хотел бы отметить два ответа. Использование return r вместо break определенно дало разницу в скорости (хотя и очень небольшую, но, как вы сказали, здесь я работаю в тиках). - person David Brown; 14.05.2009

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

Используя таблицу для получения начальных 4 битов (например), у вас будет 8 бит после 1-й итерации, затем 16 бит после второй и все биты, которые вам нужны, после четвертой итерации (поскольку double хранит 52+1 бит мантиссы).

Для поиска в таблице вы можете извлечь мантисса в [0,5,1[ и показатель степени из входных данных (используя функцию, например frexp), а затем нормализовать мантисса в [64,256[, используя умножение на подходящую степень 2.

mantissa *= 2^K
exponent -= K

После этого ваш входной номер по-прежнему будет mantissa*2^exponent. K должен быть равен 7 или 8, чтобы получить четный показатель степени. Вы можете получить начальное значение для итераций из таблицы, содержащей все квадратные корни целой части мантиссы. Выполните 4 итерации, чтобы получить квадратный корень r из мантиссы. Результатом является r*2^(exponent/2), построенный с использованием функции, подобной ldexp.

РЕДАКТИРОВАТЬ. Ниже я привожу код C++, чтобы проиллюстрировать это. Функция OP sr1 с улучшенным тестом занимает 2,78 с для вычисления 2 ^ 24 квадратных корней; моя функция sr2 занимает 1,42 с, а аппаратный sqrt - 0,12 с.

#include <math.h>
#include <stdio.h>

double sr1(double x)
{
  double last = 0;
  double r = x * 0.5;
  int maxIters = 100;
  for (int i = 0; i < maxIters; i++) {
    r = (r + x / r) / 2;
    if ( fabs(r - last) < 1.0e-10 )
      break;
    last = r;
  }
  return r;
}

double sr2(double x)
{
  // Square roots of values in 0..256 (rounded to nearest integer)
  static const int ROOTS256[] = {
    0,1,1,2,2,2,2,3,3,3,3,3,3,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,6,6,6,6,6,6,6,6,6,6,6,6,
    7,7,7,7,7,7,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,9,9,9,9,9,
    9,9,9,9,9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,11,11,11,11,11,
    11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,12,12,12,12,12,12,12,12,12,12,12,12,
    12,12,12,12,12,12,12,12,12,12,12,12,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,
    13,13,13,13,13,13,13,13,13,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,
    14,14,14,14,14,14,14,14,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,
    15,15,15,15,15,15,15,15,15,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16 };

  // Normalize input
  int exponent;
  double mantissa = frexp(x,&exponent); // MANTISSA in [0.5,1[ unless X is 0
  if (mantissa == 0) return 0; // X is 0
  if (exponent & 1) { mantissa *= 128; exponent -= 7; } // odd exponent
  else { mantissa *= 256; exponent -= 8; } // even exponent
  // Here MANTISSA is in [64,256[

  // Initial value on 4 bits
  double root = ROOTS256[(int)floor(mantissa)];

  // Iterate
  for (int it=0;it<4;it++)
    {
      root = 0.5 * (root + mantissa / root);
    }

  // Restore exponent in result
  return ldexp(root,exponent>>1);
}

int main()
{
  // Used to generate the table
  // for (int i=0;i<=256;i++) printf(",%.0f",sqrt(i));

  double s = 0;
  int mx = 1<<24;
  // for (int i=0;i<mx;i++) s += sqrt(i); // 0.120s
  // for (int i=0;i<mx;i++) s += sr1(i);  // 2.780s
  for (int i=0;i<mx;i++) s += sr2(i);  // 1.420s
}
person Eric Bainville    schedule 13.05.2009
comment
Существуют ли frexp и ldexp в C#? Я не знаю об этих функциях или чем-то, что могло бы их заменить. Проблема с решением ОП в том, что в C# трудно найти начальное приближение. - person Accipitridae; 14.05.2009
comment
Используйте магическое число Джона Кармака для приближения: codemaestro.com/reviews/9 - person Ankur Goel; 14.05.2009
comment
Я нашел C#-версии frexp и ldexp в Google Code Search, но этот пример на самом деле намного медленнее моего исходного кода. Конечно, это также может быть проблемой с реализацией frexp и ldexp. Однако я нахожу этот код действительно интересным. Спасибо за публикацию! - person David Brown; 14.05.2009

Определите допуск и вернитесь раньше, когда последующие итерации попадают в этот допуск.

person Sinan Ünür    schedule 13.05.2009

Поскольку вы сказали, что приведенный ниже код недостаточно быстр, попробуйте следующее:

    static double guess(double n)
    {
        return Math.Pow(10, Math.Log10(n) / 2);
    }

Он должен быть очень точным и, надеюсь, быстрым.

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

    public static double digits(double x)
    {
        double n = Math.Floor(x);
        double d;

        if (d >= 1.0)
        {
            for (d = 1; n >= 1.0; ++d)
            {
                n = n / 10;
            }
        }
        else
        {
            for (d = 1; n < 1.0; ++d)
            {
                n = n * 10;
            }
        }


        return d;
    }

    public static double guess(double x)
    {
        double output;
        double d = Program.digits(x);

        if (d % 2 == 0)
        {
            output = 6*Math.Pow(10, (d - 2) / 2);
        }
        else
        {
            output = 2*Math.Pow(10, (d - 1) / 2);
        }

        return output;
    }
person Unknown    schedule 13.05.2009
comment
Это работает, но вычисление занимает в 3 раза больше времени, чем простое использование x/2. - person David Brown; 14.05.2009
comment
Вы имеете в виду, что это занимает в 3 раза больше времени, чем x/2, или вся программа? Потому что это должно дать гораздо лучшую оценку, чем x/2. - person Unknown; 14.05.2009

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

Первый заключался в использовании аппроксимации ряда Тейлора первого порядка по x0:

    Func<double, double> fNewton = (b) =>
    {
        // Use first order taylor expansion for initial guess
        // http://www27.wolframalpha.com/input/?i=series+expansion+x^.5
        double x0 = 1 + (b - 1) / 2;
        double xn = x0;
        do
        {
            x0 = xn;
            xn = (x0 + b / x0) / 2;
        } while (Math.Abs(xn - x0) > Double.Epsilon);
        return xn;
    };

Во-вторых, попробовать третий заказ (более дорогой), повторить

    Func<double, double> fNewtonThird = (b) =>
    {
        double x0 = b/2;
        double xn = x0;
        do
        {
            x0 = xn;
            xn = (x0*(x0*x0+3*b))/(3*x0*x0+b);
        } while (Math.Abs(xn - x0) > Double.Epsilon);
        return xn;
    };

Я создал вспомогательный метод для определения времени функций

public static class Helper
{
    public static long Time(
        this Func<double, double> f,
        double testValue)
    {
        int imax = 120000;
        double avg = 0.0;
        Stopwatch st = new Stopwatch();
        for (int i = 0; i < imax; i++)
        {
            // note the timing is strictly on the function
            st.Start();
            var t = f(testValue);
            st.Stop();
            avg = (avg * i + t) / (i + 1);
        }
        Console.WriteLine("Average Val: {0}",avg);
        return st.ElapsedTicks/imax;
    }
}

Оригинальный метод был быстрее, но опять же, может быть интересным :)

person ccook    schedule 31.05.2009

Замена «/ 2» на «* 0,5» делает это примерно в 1,5 раза быстрее на моей машине, но, конечно, не так быстро, как нативная реализация.

person Kristof Verbiest    schedule 30.07.2010

Что ж, нативная функция Sqrt(), вероятно, не реализована на C#, она, скорее всего, будет реализована на низкоуровневом языке и наверняка будет использовать более эффективный алгоритм. Поэтому пытаться соответствовать его скорости, вероятно, бесполезно.

Однако, что касается просто попытки оптимизировать вашу функцию для heckuvit, на странице Википедии, на которую вы ссылаетесь, рекомендуется, чтобы «начальное предположение» было 2 ^ этаж (D/2), где D представляет количество двоичных цифр в числе. Вы могли бы попробовать, я не вижу ничего другого, что можно было бы значительно оптимизировать в вашем коде.

person Chad Birch    schedule 13.05.2009

Вы можете попробовать r = x >> 1;

вместо / 2 (также в другом месте у вас устройство на 2). Это может дать вам небольшое преимущество. Я бы также переместил 100 в цикл. Наверное, ничего, но мы говорим здесь о клещах.

просто сейчас проверю.

РЕДАКТИРОВАТЬ: исправлено > в >>, но это не работает для двойников, так что неважно. встраивание 100 не дало мне увеличения скорости.

person Noam Gal    schedule 13.05.2009
comment
Я думаю, что это не сработает, потому что x›1 будет истинным или ложным, вместо этого должно быть ››. - person Xn0vv3r; 13.05.2009