Более быстрый способ преобразования из строки в общий тип T, когда T является типом значения?

Кто-нибудь знает быстрый способ в VB перейти от строки к универсальному типу T, ограниченному типом значения (Of T as Structure), когда я знаю, что T всегда будет некоторым числовым типом?

Это слишком медленно, на мой вкус:

Return DirectCast(Convert.ChangeType(myStr, GetType(T)), T)

Но, похоже, это единственный разумный способ получить от String --> T. Я пытался использовать Reflector, чтобы увидеть, как работает Convert.ChangeType, и хотя я могу преобразовать String в заданный числовой тип с помощью взломанной версии этого кода, я понятия не имею, как втиснуть этот тип обратно в T, чтобы он мог быть возвращены.

Я добавлю, что часть снижения скорости, которую я вижу (в цикле синхронизации), связана с тем, что возвращаемое значение присваивается значению Nullable(Of T). Если я строго типизирую свой класс для определенного числового типа (например, UInt16), то я могу значительно повысить производительность, но тогда класс нужно будет дублировать для каждого числового типа, который я использую.

Было бы почти неплохо, если бы был конвертер в/из T во время работы над ним в общем методе/классе. Может есть, а я не замечаю его существования?


Заключение.
При тестировании трех приведенных ниже реализаций и моей исходной формы DirectCast/ChangeType подход @peenut к использованию подготовленного делегата для извлечения метода Parse из базового типа работает. Однако проверка ошибок не выполняется, поэтому разработчики должны помнить, что это следует использовать только с теми типами значений, для которых доступен метод Parse. Или расширьте приведенное ниже, чтобы выполнить проверку ошибок.

Все прогоны проводились на 32-битной системе под управлением Windows Server 2003 R2 с 4 ГБ оперативной памяти. Каждый «прогон» — это 1 000 000 выполнений (операций) тестируемого метода, синхронизированных с помощью StopWatch и сообщаемых в миллисекундах.

Оригинал DirectCast(Convert.ChangeType(myStr, GetType(T)), T):

1000000 ops: 597ms
Average of 1000000 ops over 10 runs: 472ms
Average of 1000000 ops over 10 runs: 458ms
Average of 1000000 ops over 10 runs: 453ms
Average of 1000000 ops over 10 runs: 466ms
Average of 1000000 ops over 10 runs: 462ms


Использование System.Reflection и вызов InvokeMethod для доступа к методу Parse:

1000000 ops: 12213ms
Average of 1000000 ops over 10 runs: 11468ms
Average of 1000000 ops over 10 runs: 11509ms
Average of 1000000 ops over 10 runs: 11524ms
Average of 1000000 ops over 10 runs: 11509ms
Average of 1000000 ops over 10 runs: 11490ms


Подход Конрада к генерации кода IL для доступа к методу Parse и сохранения вызова в делегате:

1000000 ops: 352ms
Average of 1000000 ops over 10 runs: 316ms
Average of 1000000 ops over 10 runs: 315ms
Average of 1000000 ops over 10 runs: 314ms
Average of 1000000 ops over 10 runs: 314ms
Average of 1000000 ops over 10 runs: 314ms


Подход peenut к использованию делегата для прямого доступа к методу Parse:

1000000 ops: 272ms
Average of 1000000 ops over 10 runs: 272ms
Average of 1000000 ops over 10 runs: 275ms
Average of 1000000 ops over 10 runs: 274ms
Average of 1000000 ops over 10 runs: 272ms
Average of 1000000 ops over 10 runs: 273ms



Для сравнения, подход Пината почти на 200 мс быстрее при выполнении 1 000 000 раз в узком цикле, поэтому его подход выигрывает. Хотя Конрад не отставал и сам по себе является захватывающим исследованием таких вещей, как ILGenerator. Реквизит всем, кто внес свой вклад!


person Kumba    schedule 06.01.2011    source источник


Ответы (4)


Да, я знаю о более быстром решении :-)

Более быстрое решение - использовать подготовленный делегат для данного (общего) типа T. Если вас интересует только String-> (встроенный числовой тип), вы можете просто получить метод Parse с одним аргументом (String).

Программа для проверки скорости возможностей. Обратите внимание, что только первые два метода являются общими, а третий и четвертый методы предназначены только для сравнения.

Imports System.Reflection

Module Module1

    Public Class Parser(Of T As Structure)

        Delegate Function ParserFunction(ByVal value As String) As T

        Public Shared ReadOnly Parse2 As ParserFunction = GetFunction()

        Private Shared Function GetFunction() As ParserFunction
            Dim t As Type = GetType(T)
            Dim m As MethodInfo = t.GetMethod("Parse", New Type() {GetType(String)})
            Dim d As ParserFunction = DirectCast( _
               ParserFunction.CreateDelegate(GetType(ParserFunction), m),  _
               ParserFunction)
            Return d
        End Function


        Public Shared Function Parse1(ByVal value As String) As T
            Return DirectCast(Convert.ChangeType(value, GetType(T)), T)
        End Function
    End Class

    Sub Main()

        Dim w As New Stopwatch()

        'test data:
        Dim arrStr() As String = New String(12345678 - 1) {}
        Dim r As New Random
        For i As Integer = 0 To arrStr.Length - 1
            arrStr(i) = r.Next().ToString()
        Next
        Dim arrInt1() As Integer = New Integer(arrStr.Length - 1) {}
        Dim arrInt2() As Integer = New Integer(arrStr.Length - 1) {}


        Console.WriteLine("1. method - Convert.ChangeType:")
        w.Reset()
        w.Start()
        For i As Integer = 0 To arrStr.Length - 1
            arrInt1(i) = Parser(Of Integer).Parse1(arrStr(i))
        Next
        w.Stop()
        Console.WriteLine(w.Elapsed)
        Console.WriteLine()

        Console.WriteLine("2. method - prepared delegate:")
        w.Reset()
        w.Start()
        For i As Integer = 0 To arrStr.Length - 1
            arrInt2(i) = Parser(Of Integer).Parse2(arrStr(i))
        Next
        w.Stop()
        Console.WriteLine(w.Elapsed)
        Console.WriteLine()

        Console.WriteLine("3. method - Integer.Parse:")
        w.Reset()
        w.Start()
        For i As Integer = 0 To arrStr.Length - 1
            arrInt2(i) = Integer.Parse(arrStr(i))
        Next
        w.Stop()
        Console.WriteLine(w.Elapsed)
        Console.WriteLine()

        Console.WriteLine("4. method - CType:")
        w.Reset()
        w.Start()
        For i As Integer = 0 To arrStr.Length - 1
            arrInt2(i) = CType(arrStr(i), Integer)
        Next
        w.Stop()
        Console.WriteLine(w.Elapsed)
        Console.WriteLine()
    End Sub
End Module

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

1. method - Convert.ChangeType:
00:00:03.5176071

2. method - prepared delegate:
00:00:02.9348792

3. method - Integer.Parse:
00:00:02.8427987

4. method - CType:
00:00:05.0542241

Отношение времен: 3,5176071 / 2,9348792 = 1,20

person peenut    schedule 13.01.2011
comment
Черт побери, я собирался опубликовать то же самое решение. :-( - person Konrad Rudolph; 13.01.2011
comment
Да, должна быть привилегия блокировки вопроса, которая означала бы: я в порядке, я знаю ответ, не беспокойтесь... Без шуток, идите и предложите это где-нибудь. - person peenut; 13.01.2011
comment
@peenut: ерунда. Вы были быстрее (и я знал решение только потому, что нашел ссылку на Delegate.CreateDelegate где-то еще на Stack Overflow). - person Konrad Rudolph; 13.01.2011
comment
Делегаты действительно неприятны для глаз, когда вы новичок в них. - person Kumba; 14.01.2011
comment
Помогите мне немного понять это... Я знаю, что Delegate Function ParserFunction(ByVal value As String) As T похож на указатель на функцию в C. Но почему Parse2 является общедоступным общим свойством, которое просто извлекает частную неразделяемую функцию GetFunction()? Нельзя ли просто обойти этот шаг и сделать GetFunction общедоступным? Единственным ключевым отличием является использование ReadOnly, которое нельзя использовать в объявлении функции, поэтому, если это сделано таким образом в качестве механизма защиты, то я это понимаю. - person Kumba; 14.01.2011
comment
@peenut: Ваш действительно быстрее. не намного по сравнению с Конрадом, но достаточно зацикленный и рассчитанный по времени, он в конечном итоге превосходит его. - person Kumba; 14.01.2011
comment
Единственное, что я могу придумать, это какая-то проверка ошибок, чтобы обеспечить существование метода Parse для T. Но в то же время любой, кто использует такое продвинутое решение, должен знать лучше. - person Kumba; 14.01.2011
comment
Здесь согласен с Кумбой. Настоящая степень делегата в этом решении делегата — это строка ParserFunction.CreateDelegate(GetType(ParserFunction), m). Я не понимаю, зачем беспокоить дополнительный уровень делегата с Parse2 As ParserFunction, когда все может быть внутри общедоступной функции. Разве смысл экономить ресурсы на ParseMethod = MyType.GetMethod()? Кроме того, я нахожу ParserFunction.CreateDelegate запутанным, потому что, насколько я понимаю, CreateDelegate не зависит ни от чего, исходящего от ParserFunction.; если не ошибаюсь, [Delegate].CreateDelegate() добивается того же. - person Ama; 27.10.2019

Вот другой подход, в котором используется DynamicMethod, упомянутый ранее.

Опять же, я не смог протестировать код VB (компилятор Mono давится тестовым вызовом, но не самим кодом), но я считаю, что это правильно. Его эквивалент C# работает, а приведенный ниже код представляет собой перевод 1:1:

public class Parser(of T as Structure)
    delegate function ParserFunction(value as String) as T
    private shared readonly m_parse as ParserFunction

    shared sub new()
        dim tt as Type = gettype(T)
        dim argumentTypes as Type() = new Type() { gettype(String) }
        dim typeDotParse as MethodInfo = tt.GetMethod("Parse", argumentTypes)
        dim method as new DynamicMethod("Parse", tt, argumentTypes)

        dim il as ILGenerator = method.GetILGenerator()
        il.Emit(OpCodes.Ldarg_0)
        il.Emit(OpCodes.Call, typeDotParse)
        il.Emit(OpCodes.Ret)

        m_parse = directcast( _
            method.CreateDelegate(gettype(ParserFunction)), _
            ParserFunction)
    end sub

    public shared function Parse(byval value As String) As T
        return m_parse(value)
    end function
end class

Этот код можно очистить, если у вас установлена ​​последняя версия VB. Опять же, компилятор Mono еще не знает Option Infer и тому подобное.

Что делает этот код, так это компилирует отдельный метод синтаксического анализа для каждого кода, для которого он вызывается. Этот метод синтаксического анализа просто делегирует фактический синтаксический анализ общему методу T.Parse типа (например, Integer.Parse). После компиляции этот код не требует дополнительного приведения, упаковки и Nullable.

Код вызывается следующим образом:

Dim i As Integer = Parser(Of Integer).Parse("42")

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

person Konrad Rudolph    schedule 12.01.2011
comment
Мне нужно будет проверить это позже сегодня... Я думаю, вы также дали мне еще один ответ на вопрос, который я еще не думал задавать - встроенный IL (в некотором смысле). - person Kumba; 12.01.2011
comment
Кроме того, чтобы добавить, может ли это быть передано в общий класс, чтобы у меня было что-то вроде myNum = Parser(Of T).Parse(myStr), когда я ограничиваю T до Structure и не получаю никаких штрафов за скорость? - person Kumba; 12.01.2011
comment
@Кумба: да, может. В этом и была вся цель, не так ли? ;-) Однако во время выполнения произойдет сбой, если тип T не имеет общедоступного общего метода Parse (но все базовые типы имеют). - person Konrad Rudolph; 12.01.2011
comment
Да, ограничение T «Структурой» должно позаботиться об этом. В худшем случае можно обернуть его в блок Try...Catch, если кто-то хочет рискнуть съесть удар по производительности, если он когда-либо сработает. - person Kumba; 13.01.2011
comment
@Konrad: приведенный выше код не работает в VS 2010 с внутренним исключением со ссылкой на Calling convention must be varargs.. Не так много запросов в Google для такого сообщения об исключении, поэтому я немного в неведении. Он терпит неудачу конкретно на линии il.EmitCall(OpCodes.Call, typeDotParse, argumentTypes). - person Kumba; 13.01.2011
comment
Неправильно, вам не нужно создавать новую функцию, у вас уже есть та, которая вам нужна — Parse. Посмотрите на мое решение ;-) - person peenut; 13.01.2011
comment
@Kumba: я исправил ошибку, попробуйте еще раз (но решение peenut лучше!). Ошибка была в том, что EmitCall работает только для vararg-функций (это даже задокументировано, я как-то пропустил)… для всех остальных функций достаточно обычного Emit. Я протестировал код на Mono, и там метод EmitCall работает, хотя в документации это не так. - person Konrad Rudolph; 13.01.2011
comment
Я попробую (и арахис тоже) позже. Спасибо! Также похоже, что вы нашли ошибку Mono. У меня есть планы протестировать мой проект на Mono, когда я продвинусь дальше, чтобы увидеть, насколько он будет совместим с Linux. Должно быть интересно. Даже если оба решения работают, жаль, что мы не можем давать частичные ответы, как на бирже экспертов... - person Kumba; 14.01.2011

Скорее всего, это не ответ, а другой вопрос. Чего вы добьетесь, имея этот метод. Давайте представим, что вы каким-то образом реализовали такой метод (извините за C# в сообщении Vb.Net, но, надеюсь, вы поняли идею):

T Convert<T>(string strInput) { ... }

и вы будете использовать этот метод только для ограниченного диапазона типов: double, int, Int16 и т. д. Таким образом, вы будете использовать его следующим образом:

double x = Convert<double>(myStr);

Я не вижу никакой пользы в таком методе, потому что по той же причине без этого метода вы бы написали:

double x = double.Parse(myStr);

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

person Snowbear    schedule 12.01.2011
comment
Я думаю, вы пропустили то, что я пытаюсь сделать. Предположим, общий метод Public Function Foo(Of T As Structure)(ByVal str As String) As T. Я должен вернуть тип T, предположительно, в переменную в универсальном классе (Dim tmp As T). Если в Foo я делаю Dim tmpDbl As Double = Double.Parse(str), то при попытке Return tmpDbl IDE выдаст ошибку, т.к. Double нельзя преобразовать в T. Следовательно, комбинация DirectCast/Convert.ChangeType, описанная в моем исходном посте, медленная. - person Kumba; 13.01.2011
comment
Спасибо, это то, что я упустил - этот дженерик можно использовать в других универсальных классах. - person Snowbear; 13.01.2011

У меня нет компилятора VB, поэтому я не могу его протестировать, но следующее работает на C#. Я сомневаюсь, что это быстрее, так как использует отражение:

Public Shared Function Parse(Of T As Strcture)(ByVal value As String) As T
    Dim type = GetType(T)
    Dim result = type.InvokeMember( _
        "Parse", _
        BindingFlags.Public Or BindingFlags.Static Or BindingFlags.InvokeMethod, _
        Nothing, Nothing, new Object() { value })
    Return DirectCast(result, T)
End Function

Один из способов ускорить это — создать динамический метод из члена T.Parse вместо InvokeMember и кэшировать этот динамический метод для каждого типа. Это будет означать большие накладные расходы для первого вызова (компиляция динамического метода), но последующие запуски будут быстрее.

person Konrad Rudolph    schedule 12.01.2011
comment
Неправильно, вам не нужно создавать новую функцию, у вас уже есть та, которая вам нужна — Parse. Посмотрите на мое решение ;-) - person peenut; 13.01.2011