VBA-tools / VBA-Web

VBA-Web: Connect VBA, Excel, Access, and Office for Windows and Mac to web services and the web
http://vba-tools.github.io/VBA-Web/
MIT License
2k stars 493 forks source link

CreateKeyValue performance #207

Closed Sophist-UK closed 8 years ago

Sophist-UK commented 8 years ago

Tim - I was wondering whether you could enlighten me as to why CreateKeyValue exists.

From reading the code, CreateKeyValue seems to create a Dictionary object with a single entry, and this is then stored in a Collection. But surely you could just use a Dictionary rather than a Collection to store in, and just add it. Creating and destroying objects is a relatively costly process in VBA - so one of the following alternative approaches would seem to me to potentially perform better:

a. Use a Dictionary to store the item - i.e. instead of

Dim Headers As New Collection
Headers.Add WebHelpers.CreateKeyValue("Authorization", "Bearer ...")

you use

Dim Headers As New Dictionary
Headers("Authorization") = "Bearer"

Note: This seems far cleaner, though would be likely to create backwards compatibility issues - or you would need to code to handle both collections of Key/Value Dictionaries and pure Dictionaries.

b. Use a Variant Array store the Key/Value Pair - i.e. instead of

Public Function CreateKeyValue(Key As String, Value As Variant) As Dictionary
    Dim web_KeyValue As New Dictionary

    web_KeyValue("Key") = Key
    web_KeyValue("Value") = Value
    Set CreateKeyValue = web_KeyValue
End Function

you use

Public Function CreateKeyValue(Key As String, Value As Variant) As Variant
    Dim web_KeyValue(2) As Variant

    web_KeyValue(LBound(web_KeyValue)) = Key
    web_KeyValue(UBound(web_KeyValue)) = Value
    CreateKeyValue = web_KeyValue
End Function

Likely to be more compatible.

Just to give an idea, I ran a quick performance test using the following code:

Private Sub Test()
    Dim Coll As New Collection
    Dim Dict As New Dictionary
    StartColl = Timer
    For i = 1 To 100000
        Coll.Add WebHelpers.CreateKeyValue(CStr(i), i)
    Next i
    FinishColl = Timer

    StartDict = Timer
    For i = 1 To 100000
        Dict(CStr(i)) = i
    Next i
    FinishDict = Timer

    Debug.Print "Coll", FinishColl - StartColl
    Debug.Print "Dict", FinishDict - StartDict
End Sub

and the results were

Coll           31.20703 
Dict           1.285156 

So Option a. is c. 24 times faster. Switching Dictionary (VBA-Dict) to Scripting.Dictionary in the above code is a bit faster still - c. 30 times faster. I have tried a few variants to eliminate e.g. calling webhelpers, and it definitely seems to be the object creation which is the killer.

I should also add that a few seconds after running this - Outlook hangs for several seconds - presumably as it does garbage collection for all the Dictionary objects which have been freed.

timhall commented 8 years ago

I had previously used a Dictionary for headers, querystring params, and cookies, but I changed them to Collection since there can by multiple values for the same key.

While I recognize creating a Dictionary for the key-value is more intensive than an array, I felt like the tradeoff for readability and not having to mess with array indices was worth it.

Based on these issues, I think the best approach (for VBA-Web 5) would be to treat them as array of values for a single key:

' Cache-Control: no-cache
' Cache-Control: no-store
Response.Headers("Cache-Control") = Array("no-cache", "no-store")

' a[]=1&a[]=2
Request.AddQuerystringParam "a", Array(1, 2)

' Set-cookie: a=1&path=/;Path=/;Version=1
' Set-cookie: a=2&path=/example;Path=/example;Version=1
Response.Cookies("a") = Array("1&path=/", "2&path=/")

This would require a good bit of work in ParseUrlEncoded/ConvertToUrlEncoded (encoding arrays and objects are a contentious subject) and the various Request and Response methods and I don't think it's a good idea to jump to this approach at this time, but I'll keep it mind for the next major release.

Sophist-UK commented 8 years ago

Hmm - in my own code I was trying to store a user defined type in a collection (which worked) and retrieve it - which gave a compile error (about coercing a variant (type stored in a collection) to a UDT). You can use an object just fine (like the current saving a Dict to a Collection), but not a UDT. Something to bear in mind.

timhall commented 8 years ago

Yep, definitely tried that. I have also run into the issue of not being able to use UDT in class functions (as parameters/return values) repeatedly, a very frustrating limitation of VBA.