VB.Net slow down on array size increase - vb.net
In my array processing intensive code, the rate of processing items slows dramatically after around 12,000 items with 48,000 operating at <50% of the speed.
The code undertakes maths on the array items and repeats that maths say 1000 times. At 12,000 items in the array I can process at a rate of 40,000 per second at 48,000 items in the array I can only process at a rate of 15,000 per second.
The code reads the contents of an Excel sheet then hands off to a DLL file with all the arrays passed to the DLL, which then processes them and hands the adjusted arrays back to Excel. Once running the DLL code, there is no interfacing with Excel. No "objects" (as in an Excel object) are created inside the VB.Net.
This was VBA code that I've transferred to VB.Net (first time using it) so bound to be something dumb I haven't realised.
Just to confuse matters...
In the Net code you'll see I've used % for integer values, which I changed from the original &, as 32-bit versions of Excel threw errors in passing their & types to the DLL which was written on a 64-bit platform, if that makes sense. Aware of the change in data types from VBA to VB.Net, that is & in vba is I think the same as % in VB.Net.
Advanced apology....
I am an amateur coder and this is a project which went from a 'could I?' project and grew and grew. It does work and is fast, I just noticed this performance drop off.
Tried switching off the calling of the ANNIE_TEST function which occurs every 50 iterations through the main loop. Makes no difference in performance.
Tried changing arrays to ByVal instead of ByRef, even though I read VB.Net ignores that for arrays - no difference.
Wondering if this is garbage collection (not that I really understand that) or perhaps the use of % integers instead of & integers.
<ComClass(clsAnnie.ClassId, clsAnnie.InterfaceId, clsAnnie.EventsId)>
Public Class clsAnnie
Public Const ClassId As String = "00E45698-E5C1-4F2C-8828-3F7404EF83A2"
Public Const InterfaceId As String = "2A1D9402-78DA-41F0-8DE3-1CD573AAF72C"
Public Const EventsId As String = "346E24CC-C9E1-4915-B254-BA80E6E310C4"
Public Sub New()
MyBase.New
End Sub
'This routine is called repeatedly from the entry point
'sub-routine Public Sub Train_NNT
Private Sub Calculate_NNT(ByVal Num_Layers%, ByVal Pattern%, ByVal Num_Hidden%, ByVal Num_Inputs%, ByRef Hidden_Neuron_Value As Double(,,), ByRef Prediction_Output As Double(,), ByRef Pattern_Error As Double(), ByRef Training_Input As Double(,), ByRef Training_Output As Double(), ByRef Input_Weights As Double(,), ByRef Output_Weights As Double(,), ByRef Inter_Weights As Double(,,), ByVal drop_out As Boolean, ByRef doi As Integer(), ByRef dol As Integer(,), ByVal activation_function%)
On Error GoTo exity
Dim alldropped As Boolean, n#, a#, c#, m%, s%, Lay%, i%, j%
'The main code is removed to keep the post short, but all it does is mathematically
'loop through the arrays and undertake maths on the values before updating one
'particular array (Training_Output) and passing that back to the main routine.
'None of the other arrays or variables have their values changed.
'Last few lines as follows for context as follows....
Prediction_Output(Pattern%, 1) = 0
For m% = 1 To Num_Hidden% : Prediction_Output(Pattern%, 1) = Prediction_Output(Pattern%, 1) + (Hidden_Neuron_Value(Pattern%, Num_Layers%, m%) * Output_Weights(m%, 1)) : Next
Pattern_Error(Pattern%) = Prediction_Output(Pattern%, 1) - Training_Output(Pattern%)
exity:
End Sub
'This is the main routine that is called from Excel VBA. It's called only once.
Public Sub Train_NNT(ByRef doi As Integer(), ByRef dol As Integer(,), ByRef OF_Array As Double(,), ByRef Cyc%, ByRef Hidden_Neuron_Value As Double(,,), ByRef Pattern_Error As Double(), ByRef Inter_Weights As Double(,,), ByRef Prediction_Output As Double(,), ByRef Training_Input As Double(,), ByRef Training_Output As Double(), ByRef Training_Input_Test As Double(,), ByRef Training_Output_Test As Double(), ByRef Input_Weights As Double(,), ByRef Output_Weights As Double(,), ByVal activation_function%, ByVal Num_Patterns%, ByVal Cycles%, ByVal Num_Layers%, ByVal Num_Hidden%, ByVal Num_Inputs%, ByVal Num_Patterns_Test%, ByVal Learn_Rate#, ByVal Maximum_Data_Value As Double, ByVal Minimum_Data_Value As Double, ByVal DOLLR As Boolean, ByVal Overfitting_CF As Boolean, ByVal Lregression As Boolean, ByVal cosine_annealing As Boolean, ByVal ADAMW As Boolean, ByVal Nesterov As Boolean, ByVal drop_out As Boolean, ByVal random_sampling As Boolean, ByVal animations As Boolean)
On Error GoTo exit_train
'These declarations shown in case it's something to do with garbage collection?
Dim Min_CET#, Best_Error#, Best_Error_Test#, Current_Error#, Current_Error_Test#, Learn_Rate_Original#, Learn_Rate_Min#, Learn_Mult#, Learn_Rate_Const#, lamda#
Dim beta_1#, beta_1_1#, beta_2#, beta_2_2#, mom_1#, mom_2#, mom_3#, damp#, cos_tmp#, beta_1_2#, tmp#, n#, P_Pat_Error#, Learn_Rate_Inv#, beta_sqr#, epsilon#
Dim Pat_Error#, Pat_Error_M#, tmp_5#, dw#, derivative#, tmp_1#, MaDV#, MiDV#
Dim Picture_Count As Integer, Picture_VE As Integer
Dim error_counter%, Snaps%, Pattern%, i%, s%, m%, o%, Pat_Test%, Pat%, q%, Lay%, j%
Dim Error_Array(0 To 100) As Double, HNV(0 To Num_Layers%, 0 To Num_Hidden%) As Double, neuron_error(0 To Num_Layers%, 0 To Num_Hidden%) As Double
Dim vdw_o() As Double, sdw_o() As Double, vdw_i() As Double, sdw_i() As Double, Output_Momentum() As Double, Input_Momentum(,) As Double, Inter_Momentum(,,) As Double
ReDim vdw_o(0 To (Num_Hidden% + 1) * (Num_Inputs% + Num_Layers% + 2)), sdw_o(0 To (Num_Hidden% + 1) * (Num_Inputs% + Num_Layers% + 2)), vdw_i(0 To (Num_Hidden% + 1) * (Num_Inputs% + Num_Layers% + 2)), sdw_i(0 To (Num_Hidden% + 1) * (Num_Inputs% + Num_Layers% + 2))
ReDim Output_Momentum(0 To Num_Hidden%), Input_Momentum(0 To (Num_Inputs% + 1), 0 To Num_Hidden%), Inter_Momentum(0 To Num_Layers%, 0 To Num_Hidden%, 0 To Num_Hidden%)
'Here we enter a loop which processes the arrays
For Cyc% = 1 To Cycles%
'Code removed here as just altering some variables and performing maths on them.
'I call the routine above...
Call Calculate_NNT(Num_Layers%, Pattern%, Num_Hidden%, Num_Inputs%, Hidden_Neuron_Value, Prediction_Output, Pattern_Error, Training_Input, Training_Output, Input_Weights, Output_Weights, Inter_Weights, drop_out, doi, dol, activation_function%)
'Lot of code removed, uses the answer from the routine above to process and alter
'a number of arrays. The array passed back (or is used) is
'Training_Output
'Every 50 cycles I need to call a function called ANNIE_TEST
'which independently verifies whether the maths hasn't exceeded
'certain thresholds, which if it has then stops the whole process
'and hands back to Excel.
'Code below just for context
If Cyc% Mod 50 = 0 Then
If Overfitting_CF Then
Current_Error_Test# = 0
For Pat_Test% = 1 To Num_Patterns_Test% : Current_Error_Test# = Current_Error_Test# + (ANNIE_TEST(Pat_Test%, Num_Hidden%, Num_Inputs%, Num_Layers%, activation_function%, Output_Weights, Inter_Weights, Training_Input_Test, Input_Weights, HNV) - Training_Output_Test(Pat_Test%)) ^ 2 : Next
Current_Error_Test# = Math.Sqrt(Current_Error_Test# / Num_Patterns_Test%)
OF_Array(q%, 1) = Cyc% : OF_Array(q%, 2) = Current_Error_Test# : q% = q% + 1
Min_CET# = 0
For o% = 1 To error_counter% : Min_CET# = Min_CET# + Error_Array(o%) : Next
Min_CET# = Min_CET# / error_counter%
If Current_Error_Test# <= Min_CET# Then
For o% = error_counter% To 2 Step -1 : Error_Array(o%) = Error_Array(o% - 1) : Next
Error_Array(1) = Current_Error_Test#
Else
Exit For
End If
End If
End If
Next 'cycles
exit_train:
End Sub
Private Function ANNIE_TEST(ByVal Pat_Test%, ByVal Num_Hidden%, ByVal Num_Inputs%, ByVal Num_Layers%, ByVal activation_function%, ByRef Output_Weights As Double(,), ByRef Inter_Weights As Double(,,), ByRef Training_Input_Test As Double(,), ByRef Input_Weights As Double(,), ByRef HNV As Double(,))
Dim m%, j%, s%, Lay%
Dim a#, c#, n#, prediction#
'Code removed as it just loops through arrays and undertakes maths on them.
'None of the arrays have values altered, just used
prediction# = 0
For j% = 1 To Num_Hidden% : prediction# = prediction# + (HNV(Num_Layers%, j%) * Output_Weights(j%, 1)) : Next
ANNIE_TEST = prediction#
End Function
End Class
OK turns out nothing to do with VB.Net and everything to do with how the arrays were handled after they arrived back in Excel, and it was really dumb. They ended up in a function which needed to load the arrays in, which had a constant so it could tell on a successive run of the function that the arrays had been loaded. I forgot to declare that constant as a public boolean, and therefore Excel just saw "null" instead of true/false.
I want to sincerely thank all of you for helping and especially TnTinMn, for making me think (like I should have done from the start) about where the timings were being taken.
Going from VBA to VB.Net gave a 7 to 10x speed increase, the fix to this has given me a further 7x speed increase on top.
Related
How do I perform unicode normalization for password storage in VBA?
I want to store and compare hashed passwords in VBA. I've read How do I properly implement Unicode passwords?, but I have no clue about where to start. How do I normalize a unicode string in VBA? Preferably, I'd do this without downloading the ICU the linked post refers to, because I'd like my project not to be dependent on external code.
Windows provides a built-in for normalizing strings, the NormalizeString function. However, it can be a bit tricky to use. Here is an implementation, based on the C example in the docs provided above: 'Declare the function Public Declare PtrSafe Function NormalizeString Lib "Normaliz.dll" (ByVal NormForm As Byte, ByVal lpSrcString As LongPtr, ByVal cwSrcLength As Long, ByVal lpDstString As LongPtr, ByVal cwDstLength As Long) As Long 'And a relevant error code Const ERROR_INSUFFICIENT_BUFFER = 122 'And a helper enum Public Enum NORM_FORM NormalizationC = &H1 NormalizationD = &H2 NormalizationKC = &H5 NormalizationKD = &H6 End Enum 'Available normalization forms can be found under https://learn.microsoft.com/en-us/windows/win32/api/winnls/ne-winnls-norm_form 'KD normalization is preferred(https://stackoverflow.com/a/16173329/7296893) when hashing characters 'If you already have hashes stored, C normalization is least likely to break them Public Function UnicodeNormalizeString(str As String, Optional norm_form As Byte = NormalizationKD) As String If Len(str) = 0 Then 'Zero-length strings can't be normalized UnicodeNormalizeString = str Exit Function End If Dim outlenestimate As Long 'Get an initial length estimate for the string outlenestimate = NormalizeString(norm_form, StrPtr(str), Len(str), 0, 0) Dim i As Long 'Try 10 times For i = 1 To 10 'Initialize buffer UnicodeNormalizeString = String(outlenestimate, vbNullChar) 'Get either the normalized string, or a new length estimate outlenestimate = NormalizeString(norm_form, StrPtr(str), Len(str), StrPtr(UnicodeNormalizeString), outlenestimate) If outlenestimate > 0 Then 'We got the normalized string 'Truncate off the unused characters UnicodeNormalizeString = Left(UnicodeNormalizeString, outlenestimate) Exit Function Else If Err.LastDllError <> ERROR_INSUFFICIENT_BUFFER Then Exit For 'An unexpected error occurred End If outlenestimate = outlenestimate * -1 'Use the new length estimate, try again End If Next Err.Raise 5000, Description:="Failure to normalize unicode string" End Function Once you have declared the normalization function, always run your password through it before hashing: If SomeHashFun(UnicodeNormalizeString(MyPassword)) = SomeHashedPassword Then 'We are in! End If
How to perform face recognition using Dahua NVR from VB.NET
Has anyone used VB.NET to get face recognition data from a Dahua NVR using VB.NET? I am facing two problems that I just can't figure out. I can connect to the NVR and set up a callback for video using bDeviceInitialized = CLIENT_Init(AddressOf DeviceDisconnected, 0) Dim lSDKVersion As Long lSDKVersion = CLIENT_GetSDKVersion() console.writeline( "SDK: " + lSDKVersion.ToString.Substring(0, 1) + "." + lSDKVersion.ToString.Substring(1, 2) + " " + lSDKVersion.ToString.Substring(3) ) fAnalyzer = AddressOf AnalyzerDataCallBack 'Set reconnect callback CLIENT_SetAutoReconnect(AddressOf DeviceReconnected, 0) ' Set device connection timeout And trial times. ' Optional operation Dim nWaitTime As Integer = 5000 ' Timeout Is 5 seconds. Dim nTryTimes As Integer = 3 ' If timeout, it will try to log in three times. CLIENT_SetConnectTime(nWaitTime, nTryTimes) 'A wait is required System.Threading.Thread.Sleep(1000) netInLoginWithHighLevelSecurity.dwSize = Marshal.SizeOf(netInLoginWithHighLevelSecurity) Array.Copy(System.Text.Encoding.Default.GetBytes(My.Settings.DeviceIP), netInLoginWithHighLevelSecurity.szIP, My.Settings.DeviceIP.Length) netInLoginWithHighLevelSecurity.nPort = CInt(My.Settings.DevicePort) Array.Copy(System.Text.Encoding.Default.GetBytes(My.Settings.DeviceUserId), netInLoginWithHighLevelSecurity.szUserName, My.Settings.DeviceUserId.Length) Array.Copy(System.Text.Encoding.Default.GetBytes(My.Settings.DevicePassword), netInLoginWithHighLevelSecurity.szPassword, My.Settings.DevicePassword.Length) netInLoginWithHighLevelSecurity.emSpecCap = EM_LOGIN_SPAC_CAP_TYPE.EM_LOGIN_SPEC_CAP_TCP netOutLoginWithHighLevelSecurity.stuDeviceInfo = devInfo netOutLoginWithHighLevelSecurity.dwSize = Marshal.SizeOf(netOutLoginWithHighLevelSecurity) 'Create a pointer for the structure 'pNetOutLoginWithHighLevelSecurity = Marshal.AllocHGlobal(Marshal.SizeOf(netOutLoginWithHighLevelSecurity)) hLoginId = CLIENT_LoginWithHighLevelSecurity(netInLoginWithHighLevelSecurity, netOutLoginWithHighLevelSecurity) ' Get the serial number of the NVR Dim i As Integer = 0 Dim strSerial As String = "" While i < 64 And netOutLoginWithHighLevelSecurity.stuDeviceInfo.sSerialNumber(i) <> 0 strSerial &= Chr(netOutLoginWithHighLevelSecurity.stuDeviceInfo.sSerialNumber(i)) i += 1 End While ' Enable cameras Dim dwUser As Int64 = 0 Dim oReserved As IntPtr = 0 Dim nChannel As Int16 =0 Dim iNeedPicture As Int32 = 1 ' pbChannel1 is a PictureBox lMonitorChannel(nChannel) = CLIENT_RealPlayEx(hLoginId, nChannel, pbChannel1.Handle, EM_REAL_PLAY_TYPE.EM_REAL_PLAY_REALPLAY) lAlarmFaceDetection(nChannel) = CLIENT_RealLoadPictureEx(hLoginId, nChannel, EVENT_IVS_ALL, iNeedPicture, fAnalyzer, Nothing, Nothing) This works fine and I can see the video from channel 1. The problems are in the analyzer callback which is defined as Public Delegate Sub AnalyzerDataCallBackDelegate(lAnalyzerHandle As Int64, dwAlarmType As UInt32, AlarmInfo As IntPtr, pBuffer As IntPtr, dwBufferSize As UInt32, dwUser As Int64, nSequence As Int16, Reserved As IntPtr) Public Shared Sub AnalyzerDataCallBack(ByVal lAnalyzerHandle As Int64, ByVal dwAlarmType As Int32, ByVal AlarmInfo As IntPtr, ByVal pBuffer As IntPtr, ByVal dwBufferSize As Int32, ByVal dwUser As Int64, ByVal nSequence As Int16, ByVal Reserved As IntPtr) Firstly, I am not getting any event other than motion detect and face recognition, even though I subscribed to EVENT_IVS_ALL. Secondly, I am not getting complete face recognition information. I copied from the unmanaged buffer into a (complex) structure. structFaceRecognitionInfo = CType(Marshal.PtrToStructure(AlarmInfo, GetType(DEV_EVENT_FACERECOGNITION_INFO)), DEV_EVENT_FACERECOGNITION_INFO) But I don't get complete information. Only the first few elements are filled with the rest have garbage or zeros. I tried copying the buffer into a byte array to inspect the data during debug and see that it is indeed filled with zeros. Dim lenBuffer As Int32 = Marshal.SizeOf(Of DEV_EVENT_FACERECOGNITION_INFO) ReDim bData(lenBuffer) Dim gchBuffer As GCHandle = GCHandle.Alloc(bData, GCHandleType.Pinned) Marshal.Copy(AlarmInfo, bData, 0, lenBuffer) gchBuffer.Free() I've been stuck at this point for a week.
undefined behaviour[s] with interface members
This is the story: I want to make a vb.NET code from the DiretX 10 C++ code, the function D3DX10CreateDeviceAndSwapChain() works fine with me : Public Declare Function D3DX10CreateDeviceAndSwapChain Lib "d3dx10_43.dll" _ (ByVal pAdapter As IDXGIAdapter, ByVal drivertype As D3D10_DRIVER_TYPE, ByVal software As IntPtr, ByVal flags As UInteger, ByRef pswapchaindesc As DXGI_SWAP_CHAIN_DESC, ByRef ppSwapChain As IntPtr, ByRef ppDevice As IntPtr) As Integer` The rest of code is: Dim swapChainDesc As New DXGI_SWAP_CHAIN_DESC() swapChainDesc.BufferCount = 1 swapChainDesc.BufferDesc.Width = Me.Width swapChainDesc.BufferDesc.Height = Me.Height swapChainDesc.BufferDesc.Format = DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM swapChainDesc.BufferUsage = 1L << (1 + 4) swapChainDesc.OutputWindow = Me.Handle swapChainDesc.SampleDesc.Count = 1 swapChainDesc.SampleDesc.Quality = 0 swapChainDesc.Windowed = True Dim sc As IntPtr Dim d As IntPtr D3DX10CreateDeviceAndSwapChain(Nothing,D3D10_DRIVER_TYPE.D3D10_DRIVER_TYPE_HARDWARE,IntPtr.Zero,0,swapChainDesc,sc,d) and to convert intptr to a valid interface object i use (it works fine): Dim s As IDXGISwapChain = CType(Marshal.GetTypedObjectForIUnknown(sc, GetType(IDXGISwapChain)), IDXGISwapChain) Dim device As ID3D10Device = CType(Marshal.GetTypedObjectForIUnknown(d, GetType(ID3D10Device)), ID3D10Device) Dim surface As ID3D10Texture2D s.GetBuffer(0, Marshal.GenerateGuidForType(GetType(ID3D10Texture2D)), surface) until here the code returns success. Dim rtv As ID3D10RenderTargetView Dim hresult As Integer = device.CreateRenderTargetView(surface, Nothing, rtv) surface.Release() ' this works fine MsgBox(rtv IsNot Nothing) the question is : when i change the location (the ranking of the members between them into the interface ID3D10Device), i get either AccessViolationException or ArgumentException for the line Dim hresult as ... why this undefined behaviours? REMARKS : the interface ID3D10Device contains more than 80 members, i have also change some members locations and they work fine.
VBA and GetRawInputDeviceList
I am working in Access 2013 and try to get GetRawInputDeviceList, GetRawInputDeviceInfo, RegisterRawInputDevices and GetRawInputData equivalents for VBA with no success. I have also searched in vain for a procedure, function or module to get a list of connected HID devices to a computer to pick out a barcode scanner. This is the beginning of the third week so I am on my knees begging for assistance. Do any of you all have a module you're willing to share, a link to a website where this is dealt with? Any help is greatly appreciated.
Using the GetRawInputDeviceList API from VBA would be pretty tricky because of the pRawInputDeviceList parameter. Unless you're willing to jump through a ton of hoops to manage your own memory and manually handle the resulting array of RAWINPUTDEVICELIST in raw memory, you'll be better off coming at this from another direction. Most barcode scanners I've dealt with present themselves to Windows as a keyboard. One possible solution would be to use a WMI query to enumerate attached Win32_Keyboard devices: Private Sub ShowKeyboardInfo() Dim WmiServer As Object Dim ResultSet As Object Dim Keyboard As Object Dim Query As String Query = "SELECT * From Win32_Keyboard" Set WmiServer = GetObject("winmgmts:root/CIMV2") Set ResultSet = WmiServer.ExecQuery(Query) For Each Keyboard In ResultSet Debug.Print Keyboard.Name & vbTab & _ Keyboard.Description & vbTab & _ Keyboard.DeviceID & vbTab & _ Keyboard.Status Next Keyboard End Sub Note: If it doesn't turn up there, you can enumerate all of the USB devices by querying CIM_USBDevice: Query = "SELECT * From Win32_Keyboard" EDIT: Per the comments, the above code won't return the handle needed to register to receive raw input events. This should get you started though - the RegisterRawInputDevices and GetRawInputData aspects are beyond the scope of what will easily go in an answer. Take a hack at it, and if you run into any problems post your code in another question. Declarations: Private Type RawInputDeviceList hDevice As Long dwType As Long End Type Private Type RidKeyboardInfo cbSize As Long dwType As Long dwKeyboardMode As Long dwNumberOfFunctionKeys As Long dwNumberOfIndicators As Long dwNumberOfKeysTotal As Long End Type Private Enum DeviceType TypeMouse = 0 TypeKeyboard = 1 TypeHID = 2 End Enum Private Enum DeviceCommand DeviceName = &H20000007 DeviceInfo = &H2000000B PreParseData = &H20000005 End Enum Private Declare Function GetRawInputDeviceList Lib "user32" ( _ ByVal pRawInputDeviceList As Long, _ ByRef puiNumDevices As Long, _ ByVal cbSize As Long) As Long Private Declare Function GetRawInputDeviceInfo Lib "user32" Alias "GetRawInputDeviceInfoW" ( _ ByVal hDevice As Long, _ ByVal uiCommand As Long, _ ByVal pData As Long, _ ByRef pcbSize As Long) As Long Private Declare Function GetLastError Lib "kernel32" () As Long Sample of retrieving device names with GetRawInputDeviceInfo: Private Sub SampleCode() Dim devices() As RawInputDeviceList devices = GetRawInputDevices Dim i As Long For i = 0 To UBound(devices) 'Inspect the type - only looking for a keyboard. If devices(i).dwType = TypeKeyboard Then Dim buffer As String Dim size As Long 'First call with a null pointer returns the string length in size. If GetRawInputDeviceInfo(devices(i).hDevice, DeviceName, 0&, size) = -1 Then Debug.Print "GetRawInputDeviceInfo error " & GetLastError() Else 'Size the string buffer. buffer = String(size, Chr$(0)) 'The second call copies the name into the passed buffer. If GetRawInputDeviceInfo(devices(i).hDevice, DeviceName, StrPtr(buffer), size) = -1 Then Debug.Print "GetRawInputDeviceInfo error " & GetLastError() Else Debug.Print buffer End If End If End If Next i End Sub Private Function GetRawInputDevices() As RawInputDeviceList() Dim devs As Long Dim output() As RawInputDeviceList 'First call with a null pointer returns the number of devices in devs If GetRawInputDeviceList(0&, devs, LenB(output(0))) = -1 Then Debug.Print "GetRawInputDeviceList error " & GetLastError() Else 'Size the output array. ReDim output(devs - 1) 'Second call actually fills the array. If GetRawInputDeviceList(VarPtr(output(0)), devs, LenB(output(0))) = -1 Then Debug.Print "GetRawInputDeviceList error " & GetLastError() Else GetRawInputDevices = output End If End If End Function Sorry about the side scrolling.
VBA check if array is one dimensional
I have an array (that comes from SQL) and can potentially have one or more rows. I want to be able to figure out if the array has just one row. UBound doesn't seem to be helpful. For 2-dimensional arrays UBound(A,1) and UBound(A,2) returns the number of rows and columns respectively, but when the array has only one row, UBound(A,1) returns the number of columns and UBound(A,2) returns a <Subscript out of range>. I have also seen this Microsoft help page for determining the number of dimensions in an array. It is a very horrifying solution that involves using the error handler. How can I determine whether the array has just one row (hopefully without using the error handler)?
If you REALLY want to avoid using On Error, you can use knowledge of the SAFEARRAY and VARIANT structures used to store arrays under the covers to extract the dimension information from where it's actually stored in memory. Place the following in a module called mdlSAFEARRAY Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (ByVal Destination As Long, ByVal Source As Long, ByVal Length As Integer) Private Type SAFEARRAY cDims As Integer fFeatures As Integer cbElements As Long cLocks As Long pvData As Long End Type Private Type ARRAY_VARIANT vt As Integer wReserved1 As Integer wReserved2 As Integer wReserved3 As Integer lpSAFEARRAY As Long data(4) As Byte End Type Private Enum tagVARENUM VT_EMPTY = &H0 VT_NULL VT_I2 VT_I4 VT_R4 VT_R8 VT_CY VT_DATE VT_BSTR VT_DISPATCH VT_ERROR VT_BOOL VT_VARIANT VT_UNKNOWN VT_DECIMAL VT_I1 = &H10 VT_UI1 VT_UI2 VT_I8 VT_UI8 VT_INT VT_VOID VT_HRESULT VT_PTR VT_SAFEARRAY VT_CARRAY VT_USERDEFINED VT_LPSTR VT_LPWSTR VT_RECORD = &H24 VT_INT_PTR VT_UINT_PTR VT_ARRAY = &H2000 VT_BYREF = &H4000 End Enum Public Function GetDims(VarSafeArray As Variant) As Integer Dim varArray As ARRAY_VARIANT Dim lpSAFEARRAY As Long Dim sArr As SAFEARRAY CopyMemory VarPtr(varArray.vt), VarPtr(VarSafeArray), 16& If varArray.vt And (tagVARENUM.VT_ARRAY Or tagVARENUM.VT_BYREF) Then CopyMemory VarPtr(lpSAFEARRAY), varArray.lpSAFEARRAY, 4& If Not lpSAFEARRAY = 0 Then CopyMemory VarPtr(sArr), lpSAFEARRAY, LenB(sArr) GetDims = sArr.cDims Else GetDims = 0 'The array is uninitialized End If Else GetDims = 0 'Not an array - might want an error instead End If End Function Here is a quick test function to show usage: Public Sub testdims() Dim anotherarr(1, 2, 3) As Byte Dim myarr() As Long Dim strArr() As String ReDim myarr(9) ReDim strArr(12) Debug.Print GetDims(myarr) Debug.Print GetDims(anotherarr) Debug.Print GetDims(strArr) End Sub
I know you want to avoid using the error handler, but if it's good enough for Chip Pearson, it's good enough for me. This code (as well as a number of other very helpful array functions) can be found on his site: http://www.cpearson.com/excel/vbaarrays.htm Create a custom function: Function IsArrayOneDimensional(arr as Variant) As Boolean IsArrayOneDimensional = (NumberOfArrayDimensions(arr) = 1) End Function Which calls Chip's function: Public Function NumberOfArrayDimensions(arr As Variant) As Integer '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' ' NumberOfArrayDimensions ' This function returns the number of dimensions of an array. An unallocated dynamic array ' has 0 dimensions. This condition can also be tested with IsArrayEmpty. ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' Dim Ndx As Integer Dim Res As Integer On Error Resume Next ' Loop, increasing the dimension index Ndx, until an error occurs. ' An error will occur when Ndx exceeds the number of dimension ' in the array. Return Ndx - 1. Do Ndx = Ndx + 1 Res = UBound(arr, Ndx) Loop Until Err.Number <> 0 Err.Clear NumberOfArrayDimensions = Ndx - 1 End Function
I realized that my original answer can be simplified - rather than having the VARIANT and SAFEARRAY structures defined as VBA Types, all that is needed is a few CopyMemorys to get the pointers and finally the Integer result. UPDATE: This version should work on both 32 bit and 64 bit systems (original preserved below the break for posterity): Option Explicit Private Declare PtrSafe Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (ByVal Destination As LongPtr, ByVal Source As LongPtr, ByVal Length As Integer) Public Function GetDims(VarSafeArray As Variant) As Integer Dim variantType As Integer Dim pointer As LongPtr Dim arrayDims As Integer CopyMemory VarPtr(variantType), VarPtr(VarSafeArray), 2& 'the first 2 bytes of the VARIANT structure contain the type If (variantType And &H2000) > 0 Then 'Array (&H2000) 'If the Variant contains an array or ByRef array, a pointer for the SAFEARRAY or array ByRef variant is located at VarPtr(VarSafeArray) + 8 CopyMemory VarPtr(pointer), VarPtr(VarSafeArray) + 8, LenB(pointer) 'If the array is ByRef, there is an additional layer of indirection through another Variant (this is what allows ByRef calls to modify the calling scope). 'Thus it must be dereferenced to get the SAFEARRAY structure If (variantType And &H4000) > 0 Then 'ByRef (&H4000) 'dereference the pointer to pointer to get the actual pointer to the SAFEARRAY CopyMemory VarPtr(pointer), pointer, LenB(pointer) End If 'The pointer will be 0 if the array hasn't been initialized If Not pointer = 0 Then 'If it HAS been initialized, we can pull the number of dimensions directly from the pointer, since it's the first member in the SAFEARRAY struct CopyMemory VarPtr(arrayDims), pointer, 2& GetDims = arrayDims Else GetDims = 0 'Array not initialized End If Else GetDims = 0 'It's not an array... Type mismatch maybe? End If End Function Here is the simplest complete GetDims that checks the dimensions directly through the variables in memory: Option Explicit Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (ByVal Destination As Long, ByVal Source As Long, ByVal Length As Integer) Public Function GetDims(VarSafeArray As Variant) As Integer Dim variantType As Integer Dim pointer As Long Dim arrayDims As Integer CopyMemory VarPtr(variantType), VarPtr(VarSafeArray), 2& 'the first 2 bytes of the VARIANT structure contain the type If (variantType And &H2000) > 0 Then 'Array (&H2000) 'If the Variant contains an array or ByRef array, a pointer for the SAFEARRAY or array ByRef variant is located at VarPtr(VarSafeArray) + 8 CopyMemory VarPtr(pointer), VarPtr(VarSafeArray) + 8, 4& 'If the array is ByRef, there is an additional layer of indirection through another Variant (this is what allows ByRef calls to modify the calling scope). 'Thus it must be dereferenced to get the SAFEARRAY structure If (variantType And &H4000) > 0 Then 'ByRef (&H4000) 'dereference the pointer to pointer to get the actual pointer to the SAFEARRAY CopyMemory VarPtr(pointer), pointer, 4& End If 'The pointer will be 0 if the array hasn't been initialized If Not pointer = 0 Then 'If it HAS been initialized, we can pull the number of dimensions directly from the pointer, since it's the first member in the SAFEARRAY struct CopyMemory VarPtr(arrayDims), pointer, 2& GetDims = arrayDims Else GetDims = 0 'Array not initialized End If Else GetDims = 0 'It's not an array... Type mismatch maybe? End If End Function
For a 2D array (or more dimensions), use this function: Function is2d(a As Variant) As Boolean Dim l As Long On Error Resume Next l = LBound(a, 2) is2d = Err = 0 End Function which gives : Sub test() Dim d1(2) As Integer, d2(2, 2) As Integer,d3(2, 2, 2) As Integer Dim b1, b2, b3 As Boolean b1 = is2d(d1) ' False b2 = is2d(d2) ' True b3 = is2d(d3) ' True Stop End Sub
I found Blackhawks's accepted and revised answer very instructive, so I played around with it and learned some useful things from it. Here's a slightly modified version of that code that includes a test sub at the bottom. Option Explicit Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" ( _ ByVal Destination As Long, ByVal Source As Long, ByVal Length As Integer) Public Function GetDims(VarSafeArray As Variant) As Integer Dim variantType As Integer Dim pointer As Long Dim arrayDims As Integer 'The first 2 bytes of the VARIANT structure contain the type: CopyMemory VarPtr(variantType), VarPtr(VarSafeArray), 2& If Not (variantType And &H2000) > 0 Then 'It's not an array. Raise type mismatch. Err.Raise (13) End If 'If the Variant contains an array or ByRef array, a pointer for the _ SAFEARRAY or array ByRef variant is located at VarPtr(VarSafeArray) + 8: CopyMemory VarPtr(pointer), VarPtr(VarSafeArray) + 8, 4& 'If the array is ByRef, there is an additional layer of indirection through_ 'another Variant (this is what allows ByRef calls to modify the calling scope). 'Thus it must be dereferenced to get the SAFEARRAY structure: If (variantType And &H4000) > 0 Then 'ByRef (&H4000) 'dereference the pointer to pointer to get actual pointer to the SAFEARRAY CopyMemory VarPtr(pointer), pointer, 4& End If 'The pointer will be 0 if the array hasn't been initialized If Not pointer = 0 Then 'If it HAS been initialized, we can pull the number of dimensions directly _ from the pointer, since it's the first member in the SAFEARRAY struct: CopyMemory VarPtr(arrayDims), pointer, 2& GetDims = arrayDims Else GetDims = 0 'Array not initialized End If End Function Sub TestGetDims() ' Tests GetDims(). Should produce the following output to Immediate Window: ' ' 1 One ' 2 Two ' Number of array dimensions: 2 Dim myArray(2, 2) As Variant Dim iResult As Integer myArray(0, 0) = 1 myArray(1, 0) = "One" myArray(0, 1) = 2 myArray(1, 1) = "Two" Debug.Print myArray(0, 0), myArray(1, 0) Debug.Print myArray(0, 1), myArray(1, 1) iResult = GetDims(myArray) Debug.Print "Number of array dimensions: " & iResult End Sub
Identify 1-row arrays without Error handling or API functions "I want to be able to figure out if the array has just one row." To solve OP's requirement focussing on arrays already dimensioned as 1- and 2-dim arrays, it isn't necessary to determine the array's actual dimension, it suffices to get the number of its "rows". So I came across the following surprisingly simple solution considering the following: It's possible to slice 1-dim or 2-dim arrays to isolate their first "column" via Application.Index(arr, 0, 1). An eventual UBound now will show the correct number of rows, especially for the asked one-row case. Function UBndOne(arr) As Long 'Purp: get rows count of (array) input 'Note: returns 1 as the function result for ' a) one-dimensional arrays ' b) 2-dim arrays with only one row ' UBound(arr,1) isn't helpful for 1-dim array as it would return the number of elements UBndOne = UBound(Application.Index(arr, 0, 1)) End Function Side note: The combined code UBound(Application.Index(arr, 0, 1)) could be applied even upon other data types than arrays, returning as well 1 as function result.