Connected objects are 20x slower in Excel 2019 than 2010 - excel

In general, Excel 2019 is around twice slower than Excel 2010 on the same hardware. That factor is fine because the hardware improvement more than compensates.
However, I noticed that the use of connected objects is around 20 times slower in Excel 2019 than 2010. The problem is not in memory allocation and release, but in the handling of connected objects. In the example shown below, the object has just one member, of the same type. A chained series of such an object takes 4 seconds to build with Excel 2010 and 100 with Excel 2019 (x 25) on my current computer (Dell XPS 5). Hopefully, it is possible to have both versions on the same machine. The memory release is slower by a similar ratio.
Specifically the example has two modules:
A class module "SmallListItem" with this code:
Public NextItem As SmallListItem
And a standard module with the following code:
' measure the time taken to allocate the list and delete it
Public Sub TimeForAllSizes()
Dim r As Range
Dim i As Integer, maxI As Integer
Dim first As SmallListItem
Dim startTime As Single, elapsedTime As Single
Set r = Range("LengthTimeRange")
Let maxI = r.Rows.Count
Call Range(r.Cells(1, 2), r.Cells(maxI, 3)).ClearContents
Let i = 1
Do
' build object
Let startTime = Timer()
Set first = BuildList(r.Cells(i, 1).Value)
Let elapsedTime = Timer() - startTime
Let r.Cells(i, 2).Value = elapsedTime
' release object
Let startTime = Timer()
Call ReleaseList(first)
Let elapsedTime = elapsedTime + Timer() - startTime
Let r.Cells(i, 3).Value = Timer() - startTime
Let i = i + 1
Loop Until i > maxI Or elapsedTime > 100
Call MsgBox("Done!")
End Sub
' build list of SmallListItem
Private Function BuildList(ByVal length As Long) As SmallListItem
Dim j As Long
Dim index As SmallListItem
Set index = New SmallListItem
Set BuildList = index
For j = 1 To length - 1
Set index.NextItem = New SmallListItem
Set index = index.NextItem
Next j
End Function
' deletes memory connected to first
Private Sub ReleaseList(ByRef first As SmallListItem)
Dim index As SmallListItem, prec As SmallListItem
Set index = first
' go to the end and release the preceding item
Do Until index.NextItem Is Nothing
Set prec = index
Set index = index.NextItem
Set prec.NextItem = Nothing
Loop
Set first = Nothing
End Sub
The spreadsheet has just one named range, visible below (the button triggers the macro TimeForAllSizes):
I tried finding workarounds, but wasn't able.
The run time ratio of x25 makes the user experience very different. That macro is several times faster on my previous computer with Excel 2010 than on my current one with Excel 2019.

Related

Passing a different function into the same iteration loop (VBA)

My overall goal is to translate an Excel based financial model into VBA. The financial model has a number of scenarios (e.g. 3) and development types (e.g. 3 - residential, commercial, industrial). There are two components of the model - revenue and costs - at present but my solution needs to be massively scalable.
The revenue calculation will be the same for each of the nine instances but the inputs will change. I am taking the inputs from the workbook and placing them in collections. I add the result of the calculation to another collection. The costs calculation will be different but will use the exact same iteration loops.
What I'm trying to do is write the iteration code once but pass a different calculation to the loop. I've done it as follows by coding the formula in a Class Object and then passing the Class Object into a function. Please see below a sandbox example.
' Passing different classes into iteration loop
Sub Main()
Dim clsAdd As CAdd
Dim colAdd As Collection
Set clsAdd = New CAdd
Set colAdd = New Collection
Set colAdd = Iteration(clsAdd)
Dim clsMul As CMult
Dim colMul As Collection
Set clsMul = New CMult
Set colMul = New Collection
Set colMul = Iteration(clsMul)
End Sub
' Same iteration loop required for different calculations
Function Iteration(ByRef colClass As Object) As Collection
Dim varArray01() As Variant
Dim varArray02() As Variant
Dim itA As Integer
Set Iteration = New Collection
varArray01 = Array(1, 2, 3, 4)
varArray02 = Array(11, 12, 13, 14)
For itA = 0 To UBound(varArray01)
Iteration.Add colClass.ICalculation_Calculation(varArray01(itA), varArray02(itA))
Next itA
End Function
'Add Class
Public Function Calculation(ByVal intA As Integer, ByVal intB As Integer) As Integer
Calculation = intA + intB
End Function
'Multiply Class
Public Function Calculation(ByVal intA As Integer, ByVal intB As Integer) As Integer
Calculation = intA * intB
End Function
Although this works, I feel that there must be a better solution than creating a new Class Object for each formula I want because every formula must be calculated using the a function called 'Calculation'. Your suggestions are most welcome.
You apparently already have an interface defined for your calculation classes. Just change the parameter of Iteration to take the interface directly.
You have implemented the Command design pattern. Small, single purpose Command objects (your CAdd and CMult) are (IMO) good design..I wouldn't try to convince you to take a different approach.
' Same iteration loop required for different calculations
Function Iteration(ByRef calculator As ICalculation) As Collection
Dim varArray01() As Variant
Dim varArray02() As Variant
Dim itA As Integer
Set Iteration = New Collection
varArray01 = Array(1, 2, 3, 4)
varArray02 = Array(11, 12, 13, 14)
For itA = 0 To UBound(varArray01)
Iteration.Add calculator.Calculation(varArray01(itA), varArray02(itA))
Next itA
End Function
Sub Main becomes:
' Passing different classes into iteration loop
Sub Main()
Dim colAdd As Collection
Set colAdd = Iteration(New CAdd)
Dim colMul As Collection
Set colMul = Iteration(New CMult)
End Sub
If creating several small, 'stateless' classes is not the kind of solution you are looking for, then the Command pattern can also be implemented using a single Class that contains a minimal amount of 'state'.
using a class like Calculator below:
Option Explicit
Implements ICalculation
Public Enum CalcID
Add
Multiply
End Enum
Private mCalculationID As CalcID
Private Sub Class_Initialize()
mCalculationID = Add
End Sub
Public Property Get CalculationID() As CalcID
CalculationID = mCalculationID
End Property
Public Property Let CalculationID(ByVal RHS As CalcID)
Select Case RHS
Case Add, Multiply
mCalculationID = RHS
Case Else
Err.Raise -1, "Invalid calculationID: " & CStr(RHS)
End Select
End Property
Private Function ICalculation_Calculation(ByVal intA As Integer, ByVal intB As Integer) As Integer
Select Case mCalculationID
Case Multiply
ICalculation_Calculation = intA * intB
Case Add
ICalculation_Calculation = intA + intB
Case Else
'Raise an error
End Select
End Function
Now you have one calculator class to manage...and the calling module(s) are responsible for managing the calculator's configuration. The Main subroutine changes slightly and the Iteration function remains unchanged.
Sub Main()
Dim calc As Calculator
Set calc = New Calculator
calc.CalculationID = CalcID.Add
Dim colAdd As Collection
Set colAdd = Iteration(calc)
calc.CalculationID = CalcID.Multiply
Dim colMul As Collection
Set colMul = Iteration(calc)
End Sub

Setting variable values dynamically using VBA

I dont work with VBA much, I have a number of global integer variables, such as DFromRow1, DFromRow2, DFromRow3 up until DFromRow20. I want to set each of these in sequential order in this manner(This is just a very simple example to help you understand what I am trying to do) :
Dim Count as integer
Count = 0
Begin Loop(loop here for 20 times)
Count = Count + 1
DFromRow & Count = Count
End Loop
The idea here is that the global variables will be populated as such:
DFromRow1 = 1
DFromRow2 = 2
DFromRow3 = 3
... up until 20
However I get an error on the & :
Expected: expression
Is there a way to do this or am I being too ambitious with what I want ?
Thanks appreciate any help.
Instead of declaring 20 variables DFromRow you can use array
Something like this
Option Explicit
Public ArrDFromRow(20)
Sub Test()
Dim i As Long
For i = 1 To 20
ArrDFromRow(i) = i
Next i
For i = 1 To 20
Debug.Print ArrDFromRow(i)
Next i
End Sub
You can use an Array to store your values like this:
Dim DRowArray(1 to 20) as Long
Sub PopulateDFromRowArray()
Dim i as Integer
For i = LBound(DRowArray) to UBound(DRowArray)
DRowArray(i) = i
Next
End Sub
Or a Dictionary, albeit you need to set a VBA reference to Microsoft Scripting Runtime:
Dim DRowDict as Scripting.Dictionary
Sub PopulateDFromRowDictionary()
Dim i as Integer
Set DRowDict = New Scripting.Dictionary
For i = 1 To 20
DRowDict.Add "DFromRow" & i, i
Next
End Sub

Goal seek Run-time 1004 error when using application.run from another Workbook

I have a run-time error that I can't solve despite having searched deeply into many forum.
Here is the problem: I am using a Macro in a Model looking for a optimal allocation through a Goal-Seek function.
When I use it within that model (let's call it Model 1), the macro works perfectly.
However, i need to work in that model and in another one, to get data that will fill a table located in a 3rd excel file, which is then my "main file" (let's call it Model 3).
Thus, I need to call that macro located in Model 1 via an Application.run from Model 3.
And when I do so, I get a "Run-time error '1004': Reference isn't valid." which relates to the Goal-seek function that I use in Model 1.
Furthermore, if after that I come back to Model 1 and try to use the macro, I get the exact same error message while it was working before.
Here are my codes:
Model 3 (main):
Sub Test_Optim()
Dim JFMPath As String
Dim MacroPath As String
JFMPath = Sheets("Inputs").Range("D2")
MacroPath = Sheets("Inputs").Range("A3")
Workbooks.Open JFMPath
Application.Run (MacroPath)
End Sub
Model 1:
Option Explicit
Option Base 1
Sub Optimization_Alloc()
'Definitions
Dim i As Integer
Dim k As Integer
Dim IRR As Single
Dim IRRDelta As Single
Dim vIRR() As Single
Dim MinIRR As Single
Dim vPPA() As Single
Dim PPALevel As Single
Dim MinPPA As Single
Dim Position As Integer
Dim FlipYearIterations As Integer
Dim IterTF As Boolean
Dim IterStep As Single
Dim FlipYear As Integer
Dim OptFlipYear As Integer
Dim vFlipYear() As Integer
Dim xDistribution As Single
Dim OptxDistribution As Single
Dim vXDistribution() As Single
'enables goal-seek iterations, MaxIterations is the number you would wanna change
IterTF = Application.Iteration = True
IterStep = Application.MaxIterations
With Application
.Iteration = True
.MaxIterations = 500
.MaxChange = 0.0001
End With
FlipYearIterations = 10
ReDim vPPA(FlipYearIterations) As Single
ReDim vFlipYear(FlipYearIterations) As Integer
ReDim vXDistribution(FlipYearIterations) As Single
ReDim vIRR(FlipYearIterations) As Single
For i = 1 To FlipYearIterations
vFlipYear(i) = 2027 + i - 1
Next i
'Loops through different FlipYears. For every year, optimal allocation of cash/tax is calculated by goalseek.
For k = 1 To FlipYearIterations
Worksheets("As_Yr").Cells(345, 9) = vFlipYear(k) 'Optimal flip year
Worksheets("As_Yr").Cells(350, 9).GoalSeek Goal:=0, ChangingCell:=Worksheets("As_Yr").Cells(346, 9)
'Here is the Goalseek function from which I get the run-time error
xDistribution = Worksheets("As_Yr").Cells(346, 9)
PPALevel = Worksheets("Cockpit & As_Gen").Cells(48, 9)
vXDistribution(k) = xDistribution
vPPA(k) = PPALevel
vIRR(k) = Worksheets("As_Yr").Cells(348, 9)
Next k
'Determines optimal FlipYear
MinIRR = WorksheetFunction.Max(vIRR)
Position = WorksheetFunction.Match(MinIRR, vIRR, False)
OptFlipYear = vFlipYear(Position)
OptxDistribution = vXDistribution(Position)
'Prints optimal setting to Excel
Worksheets("As_Yr").Cells(345, 9) = OptFlipYear 'Optimal flip year
Worksheets("As_Yr").Cells(346, 9) = OptxDistribution 'Optimal CF distrib
'restores to the original goal-seek iterations setting
IterTF = Application.Iteration = True
IterStep = Application.MaxIterations
With Application
.MaxIterations = IterStep
.Iteration = IterTF
.MaxChange = 0.001
End With
End Sub
Is this something that one of you could help me with?
I guess it is a dummy mistake, but I can't figure that out.
Many thanks,
Alice

VBA - Object Required Error, Altering Object from Dictionary

I am programming a kind of parser which reads an Excel table and then creates a
List of processes with some properties like Name, StartTime, EndTime etc.
For this I have a class Process and in the main file, I have a processList (Scripting.Dictionary), where I put the processes as I read the lines... For this assignment, the key is a String called MSID.
Now the problem is that for some reason, I am only able to access the Object from the Dictionary and alter its parameters inside one part of my If-ElseIf statement. In the other case, it throws 424-object required error and I have no idea why.
Here is the code
Sub ParseMessages()
' ITERATOR VARIABLES
Dim wb As Workbook, ws As Worksheet
Dim rowIter As Long, row As Variant
Dim A As Variant, B As Variant, C As Variant, D As Variant, E As Variant, F As Variant ' A,B,C,D,E,F variables for the cells of each row
' PROCESS PARAMETERS
Dim MSID As Variant
Dim StartTime As Variant
Dim EndTime As Variant
' OBJECTS
Dim process As process
Dim processList As Scripting.Dictionary ' DICTIONARY where the error happens
Set processList = New Scripting.Dictionary
Worksheets(1).Activate
'####### MAIN LOOP ######################################################
For rowIter = 1 To 11
row = Rows(rowIter)
A = row(1, 1)
B = row(1, 2)
C = row(1, 3)
D = row(1, 4)
E = row(1, 5)
F = row(1, 6)
Dim startIndex As Long, endIndex As Long, count As Long
' ------ PROCESSSTART -> MSID, processName, startTime
If (.....) Then
Debug.Print (vbNewLine & "Process start")
If (...) Then ' --MSID
startIndex = InStr(F, "Nr") + 3 '3 to skip "Nr "
endIndex = InStr(startIndex, F, "(")
count = endIndex - startIndex
MSID = Mid(F, startIndex, count)
StartTime = B
Debug.Print (StartTime & " -> " & MSID)
' **** MAKE new Process object, add to collection
Set process = New process
process.StartTime = StartTime
process.MSID = MSID
processList.Add MSID, process ' Add to the dictionary, KEY, VALUE
ElseIf (...) Then ' --ProcessName
startIndex = InStr(F, "=") + 2
endIndex = InStr(F, "<") - 1
count = endIndex - startIndex
processName = Mid(F, startIndex, count)
Debug.Print (processName)
' **** Add Name to the last element of the dictionary
processList(processList.Keys(processList.count - 1)).Name = processName 'get last Process Object
processList(MSID).Name = "Just Testing" ' !!!! here it works
Else
End If
' ------ END OF PROCESS ->
ElseIf (......) Then
startIndex = InStr(D, "MSID") + 5
endIndex = InStr(startIndex, D, "]")
count = endIndex - startIndex
MSID = Mid(D, startIndex, count)
EndTime = B
Debug.Print (EndTime & " End of process " & MSID)
' **** Add End time for the process from the collection, specified by MSID
Debug.Print ("Test of " & processList(MSID).Name) ' !!!!! Doesn't work
processList(MSID).Name = "Just Prooooooving" ' !!!!! Here doesn't work
processList(MSID).EndTime = EndTime ' !!!!! Does not work
End If
Next
End Sub
So to specify the question - why is it that this works:
processList(MSID).Name = "Just Testing" ' !!!! here it works
And this doesn't:
processList(MSID).Name = "Just Prooooooving" ' !!!!! Here doesn't work
If I first prove if the Object with the MSID key exists in the dictionary,
it's not found.
If processList.Exists(MSID) Then
Debug.Print ("Process exists, altering it...")
processList(MSID).Name = "Just Prooooooving" ' !!!!! Here doesn't work
processList(MSID).EndTime = EndTime
End If
But on the very first line where the condition is evaluated, I get something different by debug. It's THERE! See picture below
Debugging - MSID there but not able to access Dictionary entry with this as a key
Can you suggest how to solve this issue?
Thank you very much in advance for your help!
So... It's a bit shameful but after some hours of trying to solve this problem I found out,
that I added the Object to the list with MSID="124 " as Key.
When I tried to access, I of course used MSID with value "124".
Notice the difference? Yes, that space at the end.
The tricky part is - VBA debugger trims spaces at the end of Strings,
so it's actually impossible to see it. The same situation is if you print this out - impossible to see...
So in the end, I spent many hours looking for the answer, which is so simple :/
All I can do is to laugh about this.

VBa Looping through RecordSets is too slow for my program

I have a RecordSet loop inside another RecordSet loop. It'd work well if it didn't take 45 secs for the .OpenRecordSet to run, and the table it'll open has 445k registers.
The reason for the inside loop is because I need to filter results obtained from another RecordSet, and then get these new results and compare.
Would it be better to use other methods, or other way? Is there another way to get specific data from a table(a faster way, of course)? Should I try multithreading?
Since people might need my code:
Private Sub btnGetQ_Click()
Dim tabEQ As DAO.Recordset: Dim tabT7 As DAO.Recordset: Dim tabPesqC As DAO.Recordset: Dim PesqCqdf As DAO.QueryDef
Dim index As Integer: Dim qtdL As Long: Dim qtdL2 As Long
Dim arrC() As String: Dim arrC2() As String: Dim arrC3() As String
Set tabEQ = dbC.OpenRecordset("EQuery", dbOpenSnapshot)
Set tabT7 = dbC.OpenRecordset("T7Query", dbOpenSnapshot)
If Not tabEQ.EOF Then
tabEQ.MoveFirst
qtdL = tabEQ.RecordCount - 1
ReDim arrC(qtdL): ReDim arrC2(qtdL)
If Not tabT7.EOF Then
tabT7.MoveFirst: index = 0
Do Until tabT7.EOF
arrC(index) = tabT7.Fields("CCO"): arrC2(index) = tabT7.Fields("CCE")
Set PesqCqdf = dbC.QueryDefs("pesqCCO")
PesqCqdf.Parameters("CCO") = arrC(index)
Set tabPesqC = PesqCqdf.OpenRecordset(dbOpenSnapshot)
qtdL2 = tabPesqConj.RecordCount - 1
If qtdL2 > 0 Then
ReDim arrC3(qtdL2)
Dim i As Integer
For i = 0 To UBound(arrC3)
arrC3(i) = tabPesqC.Fields("CCE")
tabPesqC.MoveNext
Next
End If
On Error GoTo ERROR_TabT7
index = index + 1: tabT7.MoveNext
Loop
End If
ERROR_TabT7:
Set tabT7 = Nothing
End If
If IsObject(tabEQ) Then Set tabEQ = Nothing
End Sub
I created tables linked with what i wanted :/

Resources