Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Faster way to convert from a String to generic type T when T is a valuetype?

Does anyone know of a fast way in VB to go from a string to a generic type T constrained to a valuetype (Of T as Structure), when I know that T will always be some number type?

This is too slow for my taste:

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

But it seems to be the only sane method of getting from a String --> T. I've tried using Reflector to see how Convert.ChangeType works, and while I can convert from the String to a given number type via a hacked-up version of that code, I have no idea how to jam that type back into T so it can be returned.

I'll add that part of the speed penalty I'm seeing (in a timing loop) is because the return value is getting assigned to a Nullable(Of T) value. If I strongly-type my class for a specific number type (i.e., UInt16), then I can vastly increase the performance, but then the class would need to be duplicated for each numeric type that I use.

It'd almost be nice if there was converter to/from T while working on it in a generic method/class. Maybe there is and I'm oblivious to its existence?


Conclusion:
Testing the three provided implementations below and my original DirectCast/ChangeType form, @peenut's approach of using a prepared delegate to fetch the Parse method from a basic type works. No error checking is done, however, so implementors need to remember to only use this with valuetypes that have a Parse method available. Or extend the below to do error checking.

All runs were done on a 32bit system running Windows Server 2003 R2 with 4GB of RAM. Each "run" is 1,000,000 executions (ops) of the method to be tested, timed with StopWatch and reported back in milliseconds.

Original 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


Using System.Reflection and calling InvokeMethod to get at the Parse method:

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


Konrad's approach to generate IL code to access the Parse method and store the call into a delegate:

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's approach of using a delegate to access the Parse method directly:

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



Comparatively, peenut's approach is almost 200ms faster when executed 1,000,000 times in a tight loop, so his approach wins out. Although, Konrad's wasn't far behind and is itself a fascinating study of things like ILGenerator. Props to all who contributed!

like image 493
Kumba Avatar asked Jan 06 '11 04:01

Kumba


1 Answers

Yes, I know about faster solution :-)

Faster solution is to use prepared delegate for given (generic) Type T. If you are only interested in String->(built-in numeric type), you can simply get Parse method with one argument (String).

Program to test speed of possibilities. Note that only first two methods are generic, 3rd and 4th methods are for comparison only.

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

You can change number of tested elements, if you want. I used 12345678 random integers. Program outputs for me:

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

Ratio of times: 3.5176071 / 2.9348792 = 1.20

like image 172
peenut Avatar answered Oct 22 '22 10:10

peenut