Вычисление против таблиц поиска для производительности синусоидности?

Скажем, вам нужно было вычислить синус (косинус или касательный - независимо), где домен находится между 0,01 и 360,01. (используя С#)

Что будет более результативным?

  • Использование Math.Sin
  • Использование массива поиска с заранее рассчитанными значениями

Я бы прострелил, что с учетом домена вариант 2 будет намного быстрее. В какой момент точности домена (0.0000n) производительность вычисления превышает поиск.

Ответы

Ответ 1

Обновление: прочитайте до конца. Похоже, что таблица поиска работает быстрее, чем Math.Sin.

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

for (int i = 0; i < audiodata.Length; i++)
{
    audiodata[i] *= 0.5; 
}

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

for (int i = 0; i < audiodata.Length; i++)
{
    audiodata[i] = Math.Sin(audiodata[i]);
}

Если разница между Math.Sin и простым умножением существенная, я бы предположил, что разница между Math.Sin и поиском также будет существенной.

Я не знаю, хотя, и мой компьютер с Visual Studio находится в подвале, и я слишком устал, чтобы потратить 2 минуты, чтобы определить это.

Обновление: ОК, тестирование заняло более 2 минут (более 20), но похоже, что Math.Sin как минимум в два раза быстрее таблицы поиска (с использованием словаря). Вот класс, который делает Sin, используя Math.Sin или таблицу поиска:

public class SinBuddy
{
    private Dictionary<double, double> _cachedSins
        = new Dictionary<double, double>();
    private const double _cacheStep = 0.01;
    private double _factor = Math.PI / 180.0;

    public SinBuddy()
    {
        for (double angleDegrees = 0; angleDegrees <= 360.0; 
            angleDegrees += _cacheStep)
        {
            double angleRadians = angleDegrees * _factor;
            _cachedSins.Add(angleDegrees, Math.Sin(angleRadians));
        }
    }

    public double CacheStep
    {
        get
        {
            return _cacheStep;
        }
    }

    public double SinLookup(double angleDegrees)
    {
        double value;
        if (_cachedSins.TryGetValue(angleDegrees, out value))
        {
            return value;
        }
        else
        {
            throw new ArgumentException(
                String.Format("No cached Sin value for {0} degrees",
                angleDegrees));
        }
    }

    public double Sin(double angleDegrees)
    {
        double angleRadians = angleDegrees * _factor;
        return Math.Sin(angleRadians);
    }
}

И вот код теста/синхронизации:

SinBuddy buddy = new SinBuddy();

System.Diagnostics.Stopwatch timer = new System.Diagnostics.Stopwatch();
int loops = 200;

// Math.Sin
timer.Start();
for (int i = 0; i < loops; i++)
{
    for (double angleDegrees = 0; angleDegrees <= 360.0; 
        angleDegrees += buddy.CacheStep)
    {
        double d = buddy.Sin(angleDegrees);
    }
}
timer.Stop();
MessageBox.Show(timer.ElapsedMilliseconds.ToString());

// lookup
timer.Start();
for (int i = 0; i < loops; i++)
{
    for (double angleDegrees = 0; angleDegrees <= 360.0;
        angleDegrees += buddy.CacheStep)
    {
        double d = buddy.SinLookup(angleDegrees);
    }
}
timer.Stop();
MessageBox.Show(timer.ElapsedMilliseconds.ToString());

Использование значения шага в 0,01 градуса и циклический просмотр всего диапазона значений 200 раз (как в этом коде) занимает около 1,4 секунды при использовании Math.Sin и около 3,2 секунды при использовании справочной таблицы словаря. Снижение значения шага до 0,001 или 0,0001 делает поиск еще хуже по сравнению с Math.Sin. Кроме того, этот результат еще более благоприятствует использованию Math.Sin, поскольку SinBuddy.Sin выполняет умножение для поворота угла в градусах в угол в радианах при каждом вызове, а SinBuddy.SinLookup просто выполняет прямой поиск.

Это на дешевом ноутбуке (без двухъядерных процессоров или чего-то необычного). Роберт, ты да человек! (Но я все еще думаю, что должен получить чек, потому что я сделал работу).

Обновление 2: ОК, я идиот... Оказывается, что остановка и перезапуск секундомера не сбрасывает истекшие миллисекунды, поэтому поиск только показался в два раза быстрее, потому что время включало время вызовов Math.Sin. Кроме того, я перечитал вопрос и понял, что вы говорите о кэшировании значений в простом массиве, а не об использовании словаря. Вот мой модифицированный код (я оставляю старый код как предупреждение для будущих поколений):

public class SinBuddy
{
    private Dictionary<double, double> _cachedSins
        = new Dictionary<double, double>();
    private const double _cacheStep = 0.01;
    private double _factor = Math.PI / 180.0;

    private double[] _arrayedSins;

    public SinBuddy()
    {
        // set up dictionary
        for (double angleDegrees = 0; angleDegrees <= 360.0; 
            angleDegrees += _cacheStep)
        {
            double angleRadians = angleDegrees * _factor;
            _cachedSins.Add(angleDegrees, Math.Sin(angleRadians));
        }

        // set up array
        int elements = (int)(360.0 / _cacheStep) + 1;
        _arrayedSins = new double[elements];
        int i = 0;
        for (double angleDegrees = 0; angleDegrees <= 360.0;
            angleDegrees += _cacheStep)
        {
            double angleRadians = angleDegrees * _factor;
            //_cachedSins.Add(angleDegrees, Math.Sin(angleRadians));
            _arrayedSins[i] = Math.Sin(angleRadians);
            i++;
        }
    }

    public double CacheStep
    {
        get
        {
            return _cacheStep;
        }
    }

    public double SinArrayed(double angleDegrees)
    {
        int index = (int)(angleDegrees / _cacheStep);
        return _arrayedSins[index];
    }

    public double SinLookup(double angleDegrees)
    {
        double value;
        if (_cachedSins.TryGetValue(angleDegrees, out value))
        {
            return value;
        }
        else
        {
            throw new ArgumentException(
                String.Format("No cached Sin value for {0} degrees",
                angleDegrees));
        }
    }

    public double Sin(double angleDegrees)
    {
        double angleRadians = angleDegrees * _factor;
        return Math.Sin(angleRadians);
    }
}

И код теста/времени:

SinBuddy buddy = new SinBuddy();

System.Diagnostics.Stopwatch timer = new System.Diagnostics.Stopwatch();
int loops = 200;

// Math.Sin
timer.Start();
for (int i = 0; i < loops; i++)
{
    for (double angleDegrees = 0; angleDegrees <= 360.0; 
        angleDegrees += buddy.CacheStep)
    {
        double d = buddy.Sin(angleDegrees);
    }
}
timer.Stop();
MessageBox.Show(timer.ElapsedMilliseconds.ToString());

// lookup
timer = new System.Diagnostics.Stopwatch();
timer.Start();
for (int i = 0; i < loops; i++)
{
    for (double angleDegrees = 0; angleDegrees <= 360.0;
        angleDegrees += buddy.CacheStep)
    {
        double d = buddy.SinLookup(angleDegrees);
    }
}
timer.Stop();
MessageBox.Show(timer.ElapsedMilliseconds.ToString());

// arrayed
timer = new System.Diagnostics.Stopwatch();
timer.Start();
for (int i = 0; i < loops; i++)
{
    for (double angleDegrees = 0; angleDegrees <= 360.0;
        angleDegrees += buddy.CacheStep)
    {
        double d = buddy.SinArrayed(angleDegrees);
    }
}
timer.Stop();
MessageBox.Show(timer.ElapsedMilliseconds.ToString());

Эти результаты совершенно разные. Использование Math.Sin занимает около 850 миллисекунд, таблица поиска в словаре - около 1300 миллисекунд, а таблица поиска на основе массива - около 600 миллисекунд. Таким образом, похоже, что (правильно написанная [gulp]) таблица поиска на самом деле немного быстрее, чем использование Math.Sin, но ненамного.

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

Ответ 2

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

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

Ответ 3

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

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

Ответ 4

Ответ на этот вопрос полностью зависит от того, сколько значений находится в вашей таблице поиска. Вы говорите, что "домен находится между 0,01 и 360,01", но вы не говорите, сколько значений в этом диапазоне может использоваться, или насколько точны ваши ответы. Простите меня за то, что вы не ожидаете увидеть значительные цифры, используемые для передачи неявного значения в ненаучном контексте.

Для ответа на этот вопрос по-прежнему требуется дополнительная информация. Каково ожидаемое распределение значений между 0,01 и 360,01? Вы обрабатываете много данных, кроме простого вычисления sin()?

36000 значений двойной точности занимает более 256 тыс. в памяти; таблица поиска слишком велика, чтобы входить в кеш L1 на большинстве машин; если вы бежите прямо через стол, вы пропустите L1 один раз за один размер (кешлайн)/размер (двойной) доступа и, вероятно, попадете в L2. Если, с другой стороны, ваши обращения к таблице более или менее случайны, вы будете пропускать L1 почти каждый раз, когда вы выполняете поиск.

Это также сильно зависит от математической библиотеки платформы, на которой вы находитесь. Обычные i386 реализации функции sin, например, варьируются от ~ 40 циклов до 400 циклов или даже больше, в зависимости от вашей точной микроархитектуры и поставщика библиотеки. Я не приурочил библиотеку Microsoft, поэтому я не знаю, где будет реализована реализация С# Math.sin.

Так как нагрузки от L2, как правило, быстрее 40 циклов на платформе с правильной платформой, разумно ожидать, что таблица поиска будет быстрее рассмотрена изолированно. Однако я сомневаюсь, что вы вычисляете sin() изолированно; если ваши аргументы sin() прыгают по всей таблице, вы будете продувать другие данные, необходимые для других шагов вашего вычисления из кеша; хотя вычисление sin() становится быстрее, замедление в других частях вашего вычисления может более чем перевесить ускорение. Только тщательное измерение действительно может ответить на этот вопрос.

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

Ответ 5

Поскольку вы упоминаете преобразования Фурье как приложение, вы также можете рассмотреть возможность вычисления ваших синусов/косинусов с помощью уравнений

sin (x + y) = sin (x) cos (y) + cos (x) sin (y)

cos (x + y) = cos (x) cos (y) - sin (x) sin (y)

т.е. вы можете вычислить sin (n * x), cos (n * x) для n = 0, 1, 2... итеративно из sin ((n-1) * x), cos ((n-1) * x) и константы sin (x), cos (x) с 4 умножениями. Конечно, это работает, только если вы должны оценить sin (x), cos (x) в арифметической последовательности.

Сравнение подходов без реальной реализации затруднено. Это зависит от того, насколько хорошо ваши таблицы вписываются в кеши.

Ответ 6

Math.Sin быстрее. Люди, которые написали, умны и используют поиск таблиц, когда они точны и быстрее, и используют математику, когда это происходит быстрее. И нет ничего об этом домене, который делает его особенно быстрым, первое, что реализовано большинством реализаций функций триггера, - это в любом случае отобразить в благоприятный домен.

Ответ 7

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

Но нет причин для повторного пересчета одного и того же значения снова и снова.