Более быстрый способ преобразования из строки в общий тип T, когда T является типом значения?
Автор вопроса: KumbaОтветов: 4
Кто-нибудь знает быстрый способ в VB перейти от строки к универсальному типу T, ограниченному типом значения (Of T as Structure), когда я знаю, что 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 и сообщаемых в миллисекундах.
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. Реквизит всем, кто внес свой вклад!
Более быстрое решение - использовать подготовленный делегат для данного (общего) типа 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 случайных целых чисел. Вывод программы для меня:
Вот другой подход, в котором используется 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")
Если не считать единовременных затрат на компиляцию, этот метод должен быть максимально быстрым, поскольку других затрат нет: просто вызов функции для фактической процедуры синтаксического анализа. Это не быстрее, чем это.
Ответ 3 · Snowbear
Скорее всего, это не ответ, а другой вопрос. Чего вы добьетесь, имея этот метод. Давайте представим, что вы каким-то образом реализовали такой метод (извините за C# в сообщении Vb.Net, но, надеюсь, вы поняли идею):
T Convert<T>(string strInput) { ... }
и вы будете использовать этот метод только для ограниченного диапазона типов: double, int, Int16 и т. д. Таким образом, вы будете использовать его следующим образом:
double x = Convert<double>(myStr);
Я не вижу никакой пользы в таком методе, потому что по той же причине без этого метода вы бы написали:
double x = double.Parse(myStr);
Итак, я пытаюсь сказать, что без вашего волшебного метода вы напишете столько же кода, чтобы использовать его. Я не вижу никакой пользы от этого метода. Я пропустил какой-то вариант использования?
Ответ 4 · Konrad Rudolph
У меня нет компилятора 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 и кэшировать этот динамический метод для каждого типа. Это будет означать большие накладные расходы для первого вызова (компиляция динамического метода), но последующие запуски будут быстрее.