Почему INumber<T>.CreateX(int n) настолько медленный по сравнению с неявным преобразованием для чисел с плавающей запятой и двойной точности?

Я работаю над математической библиотекой и хотел бы иметь возможность преобразовать ее для использования новых интерфейсов 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. :

Метод Категории Считать Иметь в виду Ошибка стандартное отклонение Соотношение СоотношениеSD DivideByImplicitDecimal десятичная дробь 1000 29 292,733 нс 146,0540 нс 136,6190 нс 1.00 0,00 DivideByCheckedDecimal десятичная дробь 1000 28 884,550 нс 82,0562 нс 76,7554 нс 0,99 0,00 DivideBySaturatingDecimal десятичная дробь 1000 29 948,023 нс 61,6380 нс 54,6404 нс 1.02 0,01 DivideByTruncatingDecimal десятичная дробь 1000 30 367,067 нс 67,0287 нс 62,6987 нс 1.04 0,01 DivideByImplicitDouble двойной 1000 1067,421 нс 20,5178 нс 24,4250 нс 1.00 0,00 DivideByCheckedDouble двойной 1000 1342,562 нс 17,7660 нс 14,8354 нс 1,25 0,03 DivideBySaturatingDouble двойной 1000 1343,400 нс 16,4938 нс 13,7731 нс 1,25 0,04 DivideByTruncatingDouble двойной 1000 1394,057 нс 27,2936 нс 44,0740 нс 1.31 0,04 DivideByImplicitFloat плавать 1000 636,576 нс 4,9312 нс 4,6127 нс 1.00 0,00 DivideByCheckedFloat плавать 1000 1282,109 нс 13,9466 нс 12,3633 нс 2.01 0,02 DivideBySaturatingFloat плавать 1000 1288,927 нс 10,4365 нс 8,7149 нс 2.03 0,02 DivideByTruncatingFloat плавать 1000 1291,335 нс 20,6446 нс 17,2392 нс 2.03 0,04 DivideByImplicitInt интервал 1000 1210,849 нс 13,0819 нс 12,2368 нс 1.00 0,00 DivideByCheckedInt интервал 1000 1202,773 нс 7,3879 нс 6,5492 нс 0,99 0,01 DivideBySaturatingInt интервал 1000 1201,430 нс 8,0413 нс 6,7149 нс 0,99 0,01 DivideByTruncatingInt интервал 1000 1199,158 нс 9,0592 нс 7,0728 нс 0,99 0,01 DivideByImplicitLong длинный 1000 1712,002 нс 19,3905 нс 17,1891 нс 1.00 0,00 DivideByCheckedLong длинный 1000 1716,886 нс 17,9844 нс 16,8226 нс 1.00 0,01 DivideBySaturatingLong длинный 1000 1712,213 нс 31,2334 нс 29,2157 нс 1.00 0,02 DivideByTruncatingLong длинный 1000 1771,120 нс 34,2546 нс 40,7777 нс 1.03 0,03

Почему при преобразовании в float/double влияние намного выше, чем в любой другой числовой тип? Для краткости не показано, но я также запускал тесты для массивов размером 1, 100 и 1_000_000, и замедление не было заметно на 1 или 100, но было аналогичным на 1_000_000.

Может быть, лучше показать сравнение DivideByImplicit и DivideByChecked для float и double? Также вы можете проанализировать то, что скомпилировано, с помощью Sharplab.io

Svyatoslav Danyliv 02.07.2024 19:41

@SvyatlavDanyliv Ах, извините, я виноват в плохом названии, я обновил результаты тестового метода, чтобы показать фактический вызываемый метод.

JBrown521 02.07.2024 19:52

Я предполагаю, что конвертация с int на int/long практически бесплатна. Конверсия из int в float/double нет. JIT-компилятор, конечно, может делать некоторые махинации, если знает конкретные типы. С дженериками вероятность правильной оптимизации меньше, даже если это технически возможно. Например, неясно, будут ли встраиваться вызовы Create* (что было бы необходимо). То же самое относится и к конверсии из int в decimal. Однако десятичные дроби по своей природе медленны, поэтому я думаю, что накладные расходы зависят от других факторов. В любом случае, я не думаю, что здесь вы получите однозначный ответ.

freakish 02.07.2024 19:55

Потому что здесь присутствует бокс? T.CreateChecked(value) выполняет проверку типа и приводит к object. Посмотрите, как double это делает: source.dot.net/#System.Private.CoreLib/src/libraries/…

Dennis_E 02.07.2024 20:02

@freakish да, примерно этого я и ожидал, меня поставил в тупик главным образом тот факт, что замедление у разных типов сильно различается. Взглянув на JIT ASM в Sharplab, я вижу, что замедление примерно коррелирует с «длиной» создаваемых инструкций ASM. Полагаю, мой оставшийся вопрос заключается в том, почему/если существуют ограничения, мешающие команде компилятора/dotnet сделать его более оптимальным.

JBrown521 02.07.2024 20:03

@ JBrown521 JBrown521 ну, это чисто умозрительно. Но помните, что JIT-сборка сама по себе не бесплатна. И поэтому написание JIT требует соблюдения баланса: вы хотите максимально оптимизировать вещи, но не более того. Потому что обычно сам процесс оптимизации требует времени, а оптимизация высокого уровня обычно занимает много времени. С другой стороны, большая часть кода не требует дорогостоящих оптимизаций (которые не бесплатны, даже если в конечном итоге они ничего не делают). Так возможно, именно здесь и проходит граница? Опять же: это мое предположение, но это знают только разработчики.

freakish 02.07.2024 20:45

@Dennis_E всегда задействован бокс (в том числе и надолго).

Ivan Petrov 02.07.2024 20:48

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

Ivan Petrov 02.07.2024 20:49

Бокс @Dennis_E, конечно, можно оптимизировать с помощью JIT.

freakish 02.07.2024 20:49
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
9
63
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

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;
}

Будет ли увеличение двойного значения снижать точность?

Jeremy Lakeman 03.07.2024 04:12

Не целые числа, нет, пока вы не дойдете до 2^53, см. stackoverflow.com/a/1848762/14868997 Мне не нужно вам говорить, что это глупо большое число, намного превышающее максимальную длину массива.

Charlieface 03.07.2024 04:13

Другие вопросы по теме