Fast Sin/Cos с использованием заранее вычисленного массива переводов

У меня есть следующий код, выполняющий функцию Sin/Cos, используя предварительно рассчитанную таблицу памяти. в следующем примере таблица имеет 1024 * 128 элементов, охватывающих все значения Sin/Cos от 0 до 2pi. Я знаю, что могу использовать симметрию Sin/Cos и удерживать только 1/4 значений, но их у меня будет больше "ifs" при вычислении значения.

private const double PI2 = Math.PI * 2.0; 
private const int TABLE_SIZE = 1024 * 128;
private const double TABLE_SIZE_D = (double)TABLE_SIZE;
private const double FACTOR = TABLE_SIZE_D / PI2;

private static double[] _CosineDoubleTable;
private static double[] _SineDoubleTable;

Установите таблицу переводов

private static void InitializeTrigonometricTables(){
   _CosineDoubleTable = new double[TABLE_SIZE];
   _SineDoubleTable = new double[TABLE_SIZE];

   for (int i = 0; i < TABLE_SIZE; i++){
      double Angle = ((double)i / TABLE_SIZE_D) * PI2;
      _SineDoubleTable[i] = Math.Sin(Angle);
      _CosineDoubleTable[i] = Math.Cos(Angle);
   }
}

Значение представляет собой double в радианах.

Value %= PI2;  // In case that the angle is larger than 2pi
if (Value < 0) Value += PI2; // in case that the angle is negative
int index = (int)(Value * FACTOR); //from radians to index and casted in to an int
double sineValue = _SineDoubleTable[index]; // get the value from the table

Я ищу более быстрый способ сделать это. Вышеуказанные 4 строки составляют ~ 25% от всего процесса (выполняются миллиарды раз).

Ответы

Ответ 1

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

Результаты, основанные на 1'000'000'000 итерациях со случайными значениями:

(1) 00:00:57.3382769  // original version
(2) 00:00:31.9445928  // optimized version
(3) 00:00:21.3566399  // Math.Sin

код:

static double SinOriginal(double Value)
{
    Value %= PI2;
    if (Value < 0) Value += PI2;
    int index = (int)(Value * FACTOR);
    return _SineDoubleTable[index];
}

static unsafe double SinOptimized(double* SineDoubleTable, double Value)
{
    int index = (int)(Value * FACTOR) % TABLE_SIZE;
    return (index < 0) ? SineDoubleTable[index + TABLE_SIZE]
                       : SineDoubleTable[index];
}

Программа тестирования:

InitializeTrigonometricTables();
Random random = new Random();

SinOriginal(random.NextDouble());
var sw = System.Diagnostics.Stopwatch.StartNew();
for (long i = 0; i < 1000000000L; i++)
{
    SinOriginal(random.NextDouble());
}
sw.Stop();
Console.WriteLine("(1) {0}  // original version", sw.Elapsed);

fixed (double* SineDoubleTable = _SineDoubleTable)
{
    SinOptimized(SineDoubleTable, random.NextDouble());
    sw = System.Diagnostics.Stopwatch.StartNew();
    for (long i = 0; i < 1000000000L; i++)
    {
        SinOptimized(SineDoubleTable, random.NextDouble());
    }
    sw.Stop();
    Console.WriteLine("(2) {0}  // optimized version", sw.Elapsed);
}

Math.Sin(random.NextDouble());
sw = System.Diagnostics.Stopwatch.StartNew();
for (long i = 0; i < 1000000000L; i++)
{
    Math.Sin(random.NextDouble());
}
sw.Stop();
Console.WriteLine("(3) {0}  // Math.Sin", sw.Elapsed);

Ответ 2

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

  • cos(x) = sin(pi/2-x).
  • sin(pi + x) = -sin(x)

Вы можете сделать код не ветвящимся. Преобразуйте сначала в формат int.

int index = (int)(Value * FACTOR);
index %= TABLE_SIZE; // one instuction (mask)
index = (index >= 0) ? index :TABLE_SIZE-index; // one instruction isel
double sineValue = _SineDoubleTable[index];

Сравните с Math.Sin в любом случае. Профиль профиля Priofile. (Ошибка кэширования может привести к замедлению вашего кода в реальных примерах.)

Ответ 3

Если вам приходится много раз вычислять его,

  • Используйте математическую библиотеку, специфичную для процессора, такую ​​как IKML или ACML и
    • Вычислить значения в группах (векторах).
    • Если вам нужны оба, всегда вычисляйте значение sin и cos для значения в то же время.
  • Проверьте сложность и дизайн вашего алгоритма.
  • Убедитесь, что вы используете весь процессор, предлагающий архитектуру x64, а также любые векторные инструкции, которые помогут.

Ответ 4

Есть несколько замечательных замечаний по быстрому вычислению синуса и косинуса здесь: http://www.research.scea.com/gdc2003/fast-math-functions.html

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

Ответ 5

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

Если значения близки к нулю, вы можете использовать

while(Value > PI2) Value -= PI2;
while(Value < 0) Value += PI2;

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

Ответ 6

Нет гарантии, что это принесет много пользы, но в зависимости от вашего процессора целочисленная математика часто быстрее, чем математика с плавающей запятой. В этом случае я бы, вероятно, перестроил первые три строки, чтобы сначала вычислить целое число, а затем уменьшить его диапазон (при необходимости). Конечно, как отметил BlueRaja, использование С++ почти наверняка поможет. Использование языка ассемблера, вероятно, не принесет много пользы, хотя для такого поиска таблицы компилятор С++ обычно может создавать довольно хороший код.

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

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

Ответ 7

Это будет чертовски быстро, как есть.

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

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

Ответ 8

Одна вещь, которую вы могли бы попробовать, - использовать тот факт, что cos (x) = sin (x + pi/2). И сделайте таблицу синусов на одну четверть больше, поэтому вы можете использовать ее как таблицу косинусов, начиная с четверти дюйма Не уверен, что С# позволяет получить указатель на середину таблицы, так как C будет. Но даже если это не так, сокращение использования кэша может стоить больше, чем добавленное время для смещения в таблицу синусов.

Это, выраженное с помощью C:

double* _CosineDoubleTable = &_SineDoubleTable[TABLESIZE / 4];

Ответ 9

"Результаты, основанные на 1'000'000'000 итераций со случайными значениями: (1) 00: 00: 57.3382769//исходная версия (2) 00: 00: 31.9445928//оптимизированная версия (3) 00: 00: 21.3566399//Math.sin"

@Джерри Коффин: Я не знаю, как вы это тестировали, но я реализовал точный алгоритм, предоставленный OP, и мои результаты заключались в том, что его код работает почти так же быстро, как и системный код. Возможно, в ваших тестах у вас все еще был подключен отладчик VS к процессу.

Мои условия тестирования:

  1. Рослин 3.0.19.17001.
  2. .NET Core 2.2.
  3. Prejiting.
  4. ЦИКЛЫ = 10000000.
  5. Скомпилировал исполняемый файл в автономный пакет выпуска, используя dotnet-warp.
  6. Оптимизация кода включена.

вот мой файл программы: https://pastebin.com/zAX3rCVd

var sw = new System.Diagnostics.Stopwatch();
sw.Start();
for (var i = 0; i < CYCLES; i++)
{
    UltimateTrigonometry.Sine(RANDOM_NUMBERS[i]);
}
sw.Stop();
deltaT = sw.Elapsed;

Этот метод протестировал время, deltaT алгоритмом OP на завершение, deltaT - это время возврата, используемое для сравнения с результатами реализации системы, UltimateTrigonometry - это то, что я назвал реализацией из OP.

При работе в режиме отладки система ок. На 55% быстрее, но как только я компилирую исполняемый файл as и запускаю его вне отладчика, системное решение незначительно быстрее. Я пробовал несколько конфигураций, где я отключил ту или иную функцию, а затем запустил код, вот результаты:

Примечание: я изменил процентное отображение с int на float

Из моего тестирования я могу сделать вывод, что метод, который использует OP, такой же быстрый, как и системная реализация: при использовании утилиты пользовательских массивов, использующей неуправляемые массивы с надлежащим приоритезированием memalloc, этот метод может даже превосходить системную скорость. Поскольку оптимизация кода так часто снижает скорость кода, никогда не используйте ее.