Транспонировать элементы 2D SafeArray из Excel в C / C ++ DLL - PullRequest
0 голосов
/ 27 февраля 2019

В Excel VBA я могу использовать RtlCopyMemory, чтобы переместить каждый элемент в 2D SafeArray of Variants в другой SafeArray of Variants таким образом, чтобы элементы были транспонированы.

Следующий фрагмент VBA отлично работает дляэто (при условии, что переменные имеют соответствующие значения):

For i = 0 To TotalElems - 1
    CopyMemory ByVal ptrDest + i * LenElem, ByVal ptrSrc + (Elems1D * col + row) * LenElem, LenElem
    col = col + 1
    If col = Elems2D Then
        col = 0
        row = row + 1
    End If
Next

Однако, я был разочарован скоростью выполнения.Приведенный выше фрагмент примерно в пять раз медленнее, чем простое использование VBA для присваивания значений, по одному, второму массиву.

Массивы могут быть большими;в них может быть много тысяч элементов.

Я предполагаю, что тысячи вызовов RtlCopyMemory стоят дорого.

Поэтому я хотел проверить, переносит ли приведенный выше фрагмент на C / C ++DLL и вызов ее один раз (вместо тысяч раз) устранит узкое место и, возможно, будет даже быстрее, чем назначение значений непосредственно в VBA.

Я чрезвычайно заржавел на C / C ++, но справился со следующим.Но это не работает.Он возвращает «1», и Excel не падает.Но значения в массиве назначения не нарушаются кодом.Как будто ничего не произошло вообще.

long int __stdcall TransposeMemory(long *ptrDest, long *ptrSrc, long &LenElem, long &Elems1D, long &Elems2D, long &TotalElems) {

    long col = 0;
    long row = 0;

    for (long i = 0; i < TotalElems; i++) {

        memcpy (&ptrDest + (i * LenElem), &ptrSrc + (Elems1D * col + row) * LenElem, LenElem);

        col++;
        if (col == Elems2D) {
            col = 0;
            row++;
        }
    }

    return 1; 
}

В VBA я называю это так:

Result = TransposeMemory(array2(1, 1), array1(1, 1), LenElem, Elems1D, Elems2D, TotalElems)

Передача массивов таким образом дает функции DLL C / C ++ указательк первому элементу в каждом массиве.Все данные хранятся непрерывно, и каждый элемент имеет длину 16 байтов.

Можете ли вы показать мне, где я ошибаюсь?

В качестве дополнительного вопроса, какой метод будет выполнять эти данныесамое быстрое преобразование?

Чтобы прояснить расположение данных, представьте диапазон ячеек в Excel в строках и столбцах:

+---+---+---+
| a | e | i |
+---+---+---+
| b | f | j |
+---+---+---+
| c | g | k |
+---+---+---+
| d | h | l |
+---+---+---+

Приведенный выше 2D-массив состоит из четырех строк и трех столбцов иExcel отобразит его на рабочем листе по приведенному выше шаблону.

Однако VBA хранит значения непрерывно, начиная с первого элемента, как этот (каждый элемент имеет длину 16 байт):

+---+
| a |
+---+
| b |
+---+
| c |
+---+
| d |
+---+
| e |
+---+
| f |
+---+
| g |
+---+
| h |
+---+
| i |
+---+
| j |
+---+
| k |
+---+
| l |
+---+

Когда данные транспонированы правильно (и фрагмент кода VBA в верхней части этого вопроса делает именно это), Excel отображает их следующим образом:

+---+---+---+---+
| a | b | c | d |
+---+---+---+---+
| e | f | g | h |
+---+---+---+---+
| i | j | k | l |
+---+---+---+---+

Обратите внимание, что теперь у нас есть три строки и четыре столбца.

VBA хранит элементы в транспонированном массиве в следующем порядке, чего я и пытаюсь достичь:

+---+
| a |
+---+
| e |
+---+
| i |
+---+
| b |
+---+
| f |
+---+
| j |
+---+
| c |
+---+
| g |
+---+
| k |
+---+
| d |
+---+
| h |
+---+
| l |
+---+

Ответы [ 2 ]

0 голосов
/ 20 марта 2019

Второй ответ: мы узнали, что Office не является блестящим в вызове DLL, так почему бы не использовать тот факт, что индексирование выполняется сравнительно быстро:

Приведенный ниже код копирует данные массива a в одинаковоразмерный 2D типизированный массив a1 размера элемента варианта (16 байт в 32-битном, 24 байта в 64-битном), транспонирует его в другой типизированный массив b1 и копирует данные обратно в a.После этого размеры a переключаются путем настройки структуры защитного экрана.Обратите внимание, что это лучше, чем копировать его в другой массив b, поскольку варианты могут содержать строки, в которых в блоке данных массива хранится только указатель.В случае копирования вариантов с помощью RtlCopyMemory, строковые указатели b указывают на одну и ту же память, и в конце программы Excel дважды пытается освободить строки и может произойти сбой.Если вы копируете только числовые значения, тогда вы в безопасности.Присоединенная подпрограмма по скорости сопоставима с прямым присваиванием для числовых значений, но значительно превосходит строковые значения :-) И она решает проблему повторного измерения исходного массива.(Это все еще можно сделать быстрее, установив для указателя данных a1 исходные значения a, но это еще один потенциальный источник ошибок памяти.)

Option Explicit

#If Win64 Then
    Const PTR_LEN = 8
    Const VAR_LEN = 24
#Else
    Const PTR_LEN = 4
    Const VAR_LEN = 16
#End If

Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlCopyMemory" (hpvDest As Any, hpvSource As Any, ByVal cbCopy As LongPtr)
Private Declare PtrSafe Sub ZeroMemory Lib "kernel32" Alias "RtlZeroMemory" (hpvDest As Any, ByVal cbCopy As LongPtr)

Private Type SAFEARRAYBOUND
    cElements    As Long
    lLbound      As Long
End Type

Private Type SafeArray
    cDims        As Integer
    fFeatures    As Integer
    cbElements   As Long
    cLocks       As Long
    pvData       As LongPtr
    bounds(1 To 2) As SAFEARRAYBOUND
End Type

Type hh
    a(1 To VAR_LEN) As Byte
End Type


Sub TransposeVariantArrayInPlace(ByRef a As Variant)
    Dim ptrSA As LongPtr, SA As SafeArray, bound As SAFEARRAYBOUND
    Dim aPtr As LongPtr, a1Ptr As LongPtr, b1Ptr As LongPtr, a1() As hh, b1() As hh
    Dim j1 As Long, j2 As Long, j3 As Long, m As Long, n As Long, Totalsize As LongPtr

    'retrieve the pointer to the safearray
    ptrSA = VarPtr(a) + 8
    CopyMemory ptrSA, ByVal ptrSA, PTR_LEN

    'copy the safearray data
    CopyMemory SA, ByVal ptrSA, LenB(SA)
    ' Exit if not a 2D array
    If SA.cDims <> 2 Then Exit Sub

    ' generate typed arrays of equal dimensions
    m = SA.bounds(2).cElements
    n = SA.bounds(1).cElements
    ReDim a1(1 To m, 1 To n)
    ReDim b1(1 To n, 1 To m)

    ' retrieve pointers to array data
    aPtr = VarPtr(a(1, 1))
    a1Ptr = VarPtr(a1(1, 1))
    b1Ptr = VarPtr(b1(1, 1))

    Totalsize = m * n * VAR_LEN

    ' Copy the content of a to a1 and store the transposed array in b1
    CopyMemory ByVal a1Ptr, ByVal aPtr, Totalsize
    For j1 = 1 To m
        For j2 = 1 To n
            b1(j2, j1) = a1(j1, j2)
        Next
    Next

    ' write the transposed values back to a
    CopyMemory ByVal aPtr, ByVal b1Ptr, Totalsize

    ' change the dimensions of a in the safearray structure
    bound = SA.bounds(1)
    SA.bounds(1) = SA.bounds(2)
    SA.bounds(2) = bound
    CopyMemory ByVal ptrSA, SA, LenB(SA)

    ' important to empty the arrays in the case that string values have been assigned
    ' because Excel would try to free the space twice and might crash
    ZeroMemory ByVal a1Ptr, Totalsize
    ZeroMemory ByVal b1Ptr, Totalsize
End Sub

Function Transpose(ByRef a As Variant) As Variant
    Dim ptrSA As LongPtr, SA As SafeArray, bound As SAFEARRAYBOUND
    Dim b As Variant, j1 As Long, j2 As Long, m As Long, n As Long

    'retrieve the pointer to the safearray
    ptrSA = VarPtr(a) + 8
    CopyMemory ptrSA, ByVal ptrSA, PTR_LEN

    'copy the safearray data
    CopyMemory SA, ByVal ptrSA, LenB(SA)
    ' Exit if not a 2D array
    If SA.cDims <> 2 Then Exit Function

    ' generate typed arrays of equal dimensions
    m = SA.bounds(2).cElements
    n = SA.bounds(1).cElements
    ReDim b(1 To n, 1 To m)

    For j1 = 1 To m
        For j2 = 1 To n
            b(j2, j1) = a(j1, j2)
        Next
    Next

    Transpose = b

End Function

Sub TransposeComparison()
    Dim a, b As Variant
    Dim m As Long, n As Long, j1 As Long, j2 As Long, t1, t2, t3

    m = 2000
    n = 1000
    ReDim a(1 To m, 1 To n) As Variant

    'Filling the area with some test data
    For j1 = 1 To m
        For j2 = 1 To n
            a(j1, j2) = CLngLng(10 * j1 + j2)
            'a(j1, j2) = CLngLng(10 * j1 + j2) & "HH" ' for string assisnments
        Next
    Next

    t1 = Timer()

    ' classical indexing
    b = Transpose(a)

    t2 = Timer()

    TransposeVariantArrayInPlace a

    t3 = Timer()

    MsgBox t2 - t1 & " " & t3 - t2

End Sub

PS: я редактировал код дляreadibility.Обратите внимание, что массив a должен быть объявлен как dim a или dim a as Variant, в противном случае ReDim a(1 to m, 1 to n) работает, но вам нужен еще один CopyMemory ptrSA, ByVal ptrSA, PTR_LEN для получения указателя safearray.

0 голосов
/ 19 марта 2019

Я вижу очень сильную разницу в производительности в 64-разрядной версии Office между RtlCopyMemory и RtlMoveMemory, для 32-разрядной версии Office я не могу сказать, но она все еще может иметь значение.На многих форумах CopyMemory определяется как:

Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (hpvDest As Any, hpvSource As Any, ByVal cbCopy As LongPtr)

Эта подпрограмма проверяет перекрывающиеся части памяти источника и места назначения и, по-видимому, в Office 64 намного медленнее в Office 64, чем в Office 32 .Для совместного использования двух переменных VBA вы можете безопасно использовать

Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlCopyMemory" (hpvDest As Any, hpvSource As Any, ByVal cbCopy As LongPtr)

Это может решить вашу проблему с производительностью, так что вам не придется использовать обходной путь с C-кодом.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...