Как стать автором
Обновить

Комментарии 41

Как минимум производительность C# тестировать через Stopwatch неверно.
Следует использовать https://benchmarkdotnet.org/

Также C# проверяет границы массива каждый раз, в отличии от C++.

Чтобы этого избежать можно взять указатель на массив или воспользоваться MemoryMarshal.GetArrayDataReference

Я пробовал делать через указатель, используя fixed в unsafe mode — получилось ещё хуже. fixed даёт прирост производительности, когда используется за пределами цикла.

Разве в простых for-циклах по диапазону оптимизатор не справляется с удалением этих проверок?

Так и есть, но в функции SinCos нет цикла.

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

Из описания к таблице ничего не понятно. Что за цифры в таблице? Это соотношение производительности? Соотношение времени выполнения? Значение больше 1 — это превосходство предложенной реализации или стандартной?
Это отношение времени выполнения библиотечной реализации ко всем прочим. Sin7 выполняется в 6 раз быстрее, чем Math.Sin, то есть затрачивает в 6 раз меньше времени.
НЛО прилетело и опубликовало эту надпись здесь

можно было сделать xf unsigned и заменить битовые операции на деление и остаток.
Вот только для x > max_int могут быть проблемы с этим методом, технически там тоже уб в этом пребразовнии. Практически если число такое большое что погрешность точности порядка пи, синус всё равно не посчитать.

Да, это важное замечание, спасибо. Добавил уточнение в статью.

Кстати implementation-defined там только для отрицательных пишут.

НЛО прилетело и опубликовало эту надпись здесь

Интересный у вас Си, std::chrono нету, а std::cout есть )))

В этот статье написано, что в 8087 использовался CORDIC. В библиотечной функции тоже он?

Выкусите, те кто говорят, что математика не нужна в программировании! /раунд/

я последний раз про синус лет 30 назад слышал, в школе это было

Сам по себе скалярный синус не особо интересен. Интересно вычисление синуса в векторных регистрах. И тут реализаций огромное множество. Хотелось бы видеть сравнение с векторными библиотеками, например Intel SVML. Вот, например, реализация синуса под AVX-512:
https://github.com/numpy/SVML/blob/main/linux/avx512/svml_z0_sin_s_la.s
Впечатляет код?

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

Почему же? Это сравнение наглядно покажет, насколько бесперспективно написание велосипедов.


Мой личный рекорд на этом поприще

Мой личный рекорд: реализация FFT на ~10-15% быстрее, чем fftw для изображения определённого размера. Причём не на ассмеблере, а на C++ с интринсиками. Но времени убил очень много.


а вот алгоритмы типа FFT, свёртки, рекурсивных фильтров, численного интегрирования или перемножения матриц — самое то.

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

А там особо ничего, кроме как доступа к памяти и кэшу, оптимизировать и не получится
То был список алгоритмов, в которых важна не только скорость, но и точность, поскольку все они страдают накоплением погрешности. SSE/AVX можно использовать для повышения их точности без особого ущерба производительности — просто это менее очевидно, чем распараллеливание однотипных вычислений.

Минус полиномами Чебышева считал ещё ZX Spectrum basic, внутри своего "калькулятора"

Прикольно.
Как ни странно, тема очень даже актуальная. Пример, мне надо генерировать синусоиду с помощью ШИМ на микроконтроллере, и для каждой итерации брать значение синуса. Всё хорошо, когда есть табличные значения, а если у тебя точка не попадает в таблицу? Брать линейную интерполяцию между точками? В данном случае погрешность не очень большая, но всё же.

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

Как-то так
// начальная инициализация
cs = 1; // текущий косинус
sn = 0; // текущий синус

dx = pi / 65536; // приращение фазы
cosdx = cos(dx);
sindx = sin(dx);

// цикл генерации синусоиды
while (true)
{
	tt = cs * cosdx - sn * sindx;
	sn = sn * cosdx + cs * sindx;
	cs = tt;

	amp = sqrt(cs*cs+sn*sn);
	cs /= amp;
	sn /= amp;
}

Правда, при такой реализации фаза неизбежно будет «плавать».

UPD: возможно, что можно увеличить устойчивость такого метода при использовании ещё и предыдущих значений.

Тут хватило бы билинейной интерполяции. Типа

F(x) = x[n] + K1*(x-x[n]) + K2*(x-x[n])^2

где x[n] ближайшее значение из таблицы

K1 можно взять или из аналогичной таблицы значений K1 посчитать как x[n+1] - x[n], линейная интерполяция

K2 взять из таблицы ранее рассчитанной, билинейная интерполяция

члены третьей и далее степени вряд ли нужны в данном контексте, при обычном ШИМ управлении двигателем, излишняя точность утонет в "шуме" общем системе.

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

Вот тут пример линейной интерполяции на С++ для ЦАП, подробно всё расписано, точности для практических применений хватает с запасом

Ну раз на то пошло дело, то есть ещё один путь.
1) используя многочлен низкой степени или таблицу с интерполяцией генерируем «грязную» синусоиду, с гармониками. Причём математически можно совершенно точно вычислить амплитуду всех гармоник (через разложение в ряд Фурье).
2) Пропускаем эту синусоиду через рекурсивный высокодобротный bandpass-фильтр, подавляющим первую гармонику до приемлемого уровня.
К слову, приведённая в статье аппроксимация многочленом 7-й степени (с 5-ю умножениями) даёт подавление гармоник на 125 дБ:

спектр


Для управления двигателем должно хватить. И его возможно даже можно улучшить, если минимизировать отклонения в частотной, а не во временной области.
А если нужен 3-фазный ток — то можно сэкономить, генерируя не 3 синусоиды, а две: sin(x), cos(x) и использовать равенства
sin(x+2*pi/3) = 0.5*(-sin(x)+sqrt(3)*cos(x))
sin(x-2*pi/3) = 0.5*(-sin(x)-sqrt(3)*cos(x))
>из математического сопроцессора FPU процессоров x86/x64, которая тоже умеет считать и синус, и косинус одновременно.

Но зачем? Там же аппаратный баг у Intel, который так и не исправили. FPU FSIN и FCOS ошибаются очень сильно. Все пользуется кодом из glibc и sincos из AMD /Microsoft C library (MIT лицензия).
retrocomputing.stackexchange.com/questions/16523/intel-cpu-bug-in-the-90sfsincos
Там же аппаратный баг у Intel, который так и не исправили
Баг был у fdiv, а не fsincos, и его не только исправили, но ещё и процессоры всем поменяли.

FPU FSIN и FCOS ошибаются очень сильно
А чуть более подробно можно? При каких значениях аргумента какого порядка ошибки возникают. Потому как в своих испытаниях ничего такого я не заметил.
www.reddit.com/r/programming/comments/2is4nj/comment/cl54afb
randomascii.wordpress.com/2014/10/09/intel-underestimates-error-bounds-by-1-3-quintillion
FSIN уже не исправишь, так как это сломает ожидания целой пачки програмного обеспечения. AMD попытались, но пришлось убирать. (У них один и тот же код для это в микроинструкциях.)

В ссылке сверху все описано, но я могу дать ссылки на обсуждение в glibc.

sourceware.org/bugzilla/show_bug.cgi?id=13658
github.com/bminor/glibc/commit/b35fe25ed9b3b406754fe681b1785f330d9faf62

На x86_64 они используют sinf кстати.

Очень вероятно, что то что вы тестируете это как раз програмный код оттуда.
Я так понял, речь идёт о значении аргумента за пределами значения ±263. В таком случае это скорее «не баг, а фича»©, поскольку описывается в документации. Но за наводку спасибо — потестирую этот момент в своей реализации синуса повышенной точности.
Вы имеете в виду пример с 2646693125139304345? Нет это просто кто-то сильно увлекся. Но там уже ошибка при long-double precision the fsin fails to meet its guarantees at numbers as small as 3.01626.

Я физик, поэтому бесконечно далек от таких проблем, но мне всегда казалось что для вычисления синуса логично просто решать задачу Коши x'' + x = 0, x(0) = 0, x'(0) = 1

Тем более там есть интеграл x^2 + x'^2 = 1, который позволит даже грубым методам удержать погрешность в разумных пределах.

Почему не вычисляют так?

Решать задачу коши Рунге Куттой? Это надо сделать сколько шагов, несколько десятков, сложнее чем просто подставить число в многочлен 13го порядка.

Ну а теперь попробуйте найти ответ на микроконтроллере типа Attiny 13, где 64 байта памяти, 1024 байт для программы. Для вычисления синуса можно выделить 200 байт на флеше и 8 байт оперативной памяти. Ну и уложиться желательно в 50-100 тактов процессора. Аппаратного умножения нет, деления нет.

Примерно такие же ограничения будут при формировании синусоиды на частоте 1000 МГц на ПЛИС, минимум логических элементов и высокие требования к быстродействию.

Теперь понял, спасибо

Взято из Quake III Arena?

Ну, тогда считаем корень, быстрее всех )))

float Q_rsqrt( float number )
{	
	const float x2 = number * 0.5F;
	const float threehalfs = 1.5F;

	union {
		float f;
		uint32_t i;
	} conv = {number}; // member 'f' set to value of 'number'.
	conv.i = 0x5f3759df - ( conv.i >> 1 );
	conv.f *= threehalfs - x2 * conv.f * conv.f;
	return conv.f;
}

или вот так

float Q_rsqrt( float number )
{
	long i;
	float x2 ,y;
	const float threehalfs = 1.5F;

	x2 = number * 0.5F;
	y = number;
	i = * ( long * ) &y;
	i = 0x5f3759df - ( i >> 1);
	y = * ( float * ) &i;
	y = y * (threehalfs - ( x2 * y * y ) ); // 1st iteration
	// y = y * ( threehalfs - ( x2 * y * y ) ); //2nd iteration 

	return y;
}

 При использовании многочленов Чебышева количество верных знаков ориентировочно соответствует степени многочлена. Для примера возьмём 7-ю степень — её вполне достаточно для задач визуализации:  

Если под визуализацией понимать рисование на экране полной или почти полной синусоиды (с размахом вертикальной оси порядка 1), то 7 знаков - это явный перебор. Вполне хватит трех, максимум четырех. Пикселов- то у нас всего E+3. Нет смысла считать точнее.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий