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 к процессу.
Мои условия тестирования:
- Рослин 3.0.19.17001.
- .NET Core 2.2.
- Prejiting.
- ЦИКЛЫ = 10000000.
- Скомпилировал исполняемый файл в автономный пакет выпуска, используя dotnet-warp.
- Оптимизация кода включена.
вот мой файл программы: 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, этот метод может даже превосходить системную скорость. Поскольку оптимизация кода так часто снижает скорость кода, никогда не используйте ее.