Externally visible member takes a multidimensional array parameter
ID |
vbnet.performance.no_multidim_array_parameter |
Severity |
critical |
Remediation Complexity |
medium |
Remediation Risk |
medium |
Remediation Effort |
medium |
Resource |
Performance |
Language |
VB.NET |
Description
Reports a parameter declared as a multidimensional (rectangular) array - a type whose array
suffix contains a comma, such as Double(,) or Byte(,,) - on a member that forms part of a
type’s externally visible surface: a Public or Protected method or function, a method
declared on an Interface, and a MustOverride member. Jagged arrays (Double()()) and
single-dimension arrays (Double()) are not reported, and neither are multidimensional arrays
used only as locals, fields or property types - the rule is about what crosses the API
boundary. Methods that are Private or Friend are not examined.
Rationale
A rectangular array is stored as one contiguous block addressed through a computed offset, and
the runtime has no dedicated instruction for that computation: every element access multiplies
the row index by the row length, adds the column index, and performs a bounds check against the
dimension descriptors held in the array header. Single-dimension access, by contrast, is one of
the operations the JIT optimizes hardest - it recognises the pattern, hoists the bounds check
out of a counted loop, and emits a direct scaled index. A jagged array is a single-dimension
array of single-dimension arrays, so it gets that treatment on both levels, which is why the
same nested loop typically runs measurably faster over Double()() than over Double(,) once
the loop is hot. The cost is paid by the caller as well as the implementation. Rectangular
arrays cannot be built up a row at a time, so a caller assembling data from a query, a file or
a service has to count the rows first and then copy them into a preallocated block, and a
caller that already holds jagged data - the normal shape when rows arrive one at a time or have
different lengths - must flatten it into a rectangle purely to satisfy the signature. Because
the shape is fixed at allocation, resizing means allocating a new block and copying everything.
Putting the rectangular form in a signature also propagates it: it is the one place the
decision cannot be revisited later without a breaking change, since every caller and every
override is written against it. Rectangular arrays are additionally awkward for consumers
outside Visual Basic and C# - several .NET languages and most interop and serialization layers
handle only single-dimension arrays.
The following code illustrates the pattern detected by this rule:
Public Class MatrixOperations
' FLAGGED: Externally visible member takes a multidimensional array parameter
Public Sub Normalize(samples As Double(,))
Rescale(samples, 1.0)
End Sub
Remediation
Declare the parameter as a jagged array - Double()() instead of Double(,) - and index it in
two steps, grid(row)(column). Where the data is genuinely rectangular and that invariant
matters, prefer a small type that owns a single-dimension backing array and exposes row and
column accessors, which keeps the fast indexing and lets the type enforce the shape. Keep any
rectangular array that is required by an external contract inside the implementation and
convert at the boundary.
' Before: rectangular array in the public signature
Public Sub Normalize(samples As Double(,))
For row = 0 To samples.GetUpperBound(0)
For column = 0 To samples.GetUpperBound(1)
samples(row, column) /= Scale
Next
Next
End Sub
' After
Public Sub Normalize(samples As Double()())
For row = 0 To samples.Length - 1
Dim values = samples(row)
For column = 0 To values.Length - 1
values(column) /= Scale
Next
Next
End Sub