Я работаю над математической библиотекой и хотел бы иметь возможность преобразовать ее для использования новых интерфейсов INumber<T>
в System.Numerics
. Приведенные здесь методы часто находятся на горячем пути, поэтому было бы хорошо, если бы они были как можно более быстрыми, но при этом с ними было бы приятно работать. Я сравнивал их по ходу дела и заметил кое-что, что кажется немного странным.
В некоторых случаях мы делим значение массива по его индексу из-за использования System.Numerics
, это невозможно сделать с помощью неявного преобразования, как если бы вы определяли тип, рекомендуемым методом, по-видимому, является использование INumber<T>.Create{Checked|Saturating|Truncating}(int n)
Методы тестирования, которые я использую, довольно просты, например:
public static double[] DivideByImplicit(double[] values)
{
double[] result = new double[values.Length];
for (int i = 1; i < result.Length; i++)
{
result[i] = values[i] / i;
}
return result;
}
// Overloads for other types (int, long, float, decimal)
// ...
public static T[] DivideByChecked<T>(T[] values) where T : INumber<T>
{
T[] result = new T[values.Length];
for (int i = 1; i < values.Length; i++)
{
result[i] = values[i] / T.CreateChecked(i);
}
return result;
}
// Same again but for Saturating/Truncating
// ...
Для int
, long
и decimal
результаты находятся максимум в пределах нескольких процентных пунктов, но когда дело доходит до float
и double
, падение производительности гораздо больше: ~ 25 % для double, но до 100 % увеличения времени для float. :
Почему при преобразовании в float
/double
влияние намного выше, чем в любой другой числовой тип? Для краткости не показано, но я также запускал тесты для массивов размером 1, 100 и 1_000_000, и замедление не было заметно на 1 или 100, но было аналогичным на 1_000_000.
@SvyatlavDanyliv Ах, извините, я виноват в плохом названии, я обновил результаты тестового метода, чтобы показать фактический вызываемый метод.
Я предполагаю, что конвертация с int
на int/long
практически бесплатна. Конверсия из int
в float/double
нет. JIT-компилятор, конечно, может делать некоторые махинации, если знает конкретные типы. С дженериками вероятность правильной оптимизации меньше, даже если это технически возможно. Например, неясно, будут ли встраиваться вызовы Create*
(что было бы необходимо). То же самое относится и к конверсии из int
в decimal
. Однако десятичные дроби по своей природе медленны, поэтому я думаю, что накладные расходы зависят от других факторов. В любом случае, я не думаю, что здесь вы получите однозначный ответ.
Потому что здесь присутствует бокс? T.CreateChecked(value)
выполняет проверку типа и приводит к object
. Посмотрите, как double
это делает: source.dot.net/#System.Private.CoreLib/src/libraries/…
@freakish да, примерно этого я и ожидал, меня поставил в тупик главным образом тот факт, что замедление у разных типов сильно различается. Взглянув на JIT ASM в Sharplab, я вижу, что замедление примерно коррелирует с «длиной» создаваемых инструкций ASM. Полагаю, мой оставшийся вопрос заключается в том, почему/если существуют ограничения, мешающие команде компилятора/dotnet сделать его более оптимальным.
@ JBrown521 JBrown521 ну, это чисто умозрительно. Но помните, что JIT-сборка сама по себе не бесплатна. И поэтому написание JIT требует соблюдения баланса: вы хотите максимально оптимизировать вещи, но не более того. Потому что обычно сам процесс оптимизации требует времени, а оптимизация высокого уровня обычно занимает много времени. С другой стороны, большая часть кода не требует дорогостоящих оптимизаций (которые не бесплатны, даже если в конечном итоге они ничего не делают). Так возможно, именно здесь и проходит граница? Опять же: это мое предположение, но это знают только разработчики.
@Dennis_E всегда задействован бокс (в том числе и надолго).
не может воспроизвести отклонение более 10% для плавающего режима. Должно быть, это мой старый ноутбук.
Бокс @Dennis_E, конечно, можно оптимизировать с помощью JIT.
CreateChecked
может быть медленным, если тип, который вам нужен, не тот, который вы передаете. Вы можете заставить цикл использовать другую переменную цикла, которая напечатана как T
, и использовать ее для своего деления.
public static T[] DivideByChecked<T>(T[] values) where T : INumber<T>
{
T[] result = new T[values.Length];
var tI = T.MultiplicativeIdentity;
for (int i = 1; i < values.Length; i++)
{
result[i] = values[i] / tI;
tI++;
}
return result;
}
Будет ли увеличение двойного значения снижать точность?
Не целые числа, нет, пока вы не дойдете до 2^53, см. stackoverflow.com/a/1848762/14868997 Мне не нужно вам говорить, что это глупо большое число, намного превышающее максимальную длину массива.
Может быть, лучше показать сравнение
DivideByImplicit
иDivideByChecked
дляfloat
иdouble
? Также вы можете проанализировать то, что скомпилировано, с помощью Sharplab.io