0

[Rust/VBA] Cách sử dụng Rust từ VBA Phần 3 - Xử lý mảng (Chuỗi ký tự)

[Rust/VBA] Cách sử dụng Rust từ VBA Phần 3 - Xử lý mảng (Chuỗi ký tự)

Mục lục

  • Mở đầu
  • Bố cục bộ nhớ của mảng chuỗi
  • Thực hành 1: VBA -> Rust (Tham chiếu)
  • Thực hành 2: Rust -> VBA (Phương pháp nối chuỗi)
  • Kiểm tra hiệu năng (Benchmark Test)

Mở đầu

Nhìn lại Phần 2

Trong [Phần 2], chúng ta đã tìm hiểu bản chất của mảng trong VBA, các kiến thức cơ bản về truyền dữ liệu mảng, và thực hành truyền mảng dạng số. Có hai phương pháp chính: chỉ truyền nội dung dữ liệutruyền nguyên cấu trúc SafeArray. Phương pháp đầu tiên dễ cài đặt hơn, nhưng nếu muốn phía Rust lấy được nhiều thông tin chi tiết của mảng thì nên chọn phương pháp thứ hai.

Mục tiêu của Phần 3

Đối với mảng số, dữ liệu được sắp xếp tuyến tính, số byte của từng phần tử là cố định. Ngay cả khi giá trị số trở nên lớn, nó cũng không phá hủy vùng nhớ lân cận, do đó chúng ta có thể thay đổi nội dung một cách an toàn. Tuy nhiên, lần này chúng ta sẽ xử lý mảng chuỗi ký tự (String Array). Chuỗi là dữ liệu có độ dài thay đổi (variable length), nên nếu trực tiếp thay đổi nội dung, nó trở thành một kiểu dữ liệu rắc rối có khả năng phá hủy các vùng nhớ khác. Mặc dù vậy, trong thực tế công việc, việc xử lý chuỗi ký tự xuất hiện rất thường xuyên. Mục tiêu lần này là làm sao để có thể trao đổi mảng chuỗi ký tự giữa VBA và Rust.

Bố cục bộ nhớ của mảng chuỗi

Trước khi bắt đầu viết code, chúng ta cần hiểu mảng chuỗi được bố trí như thế nào trong bộ nhớ. Hình dưới đây là sơ đồ bố trí khi VBA cấp phát một mảng động kiểu String gồm 3 phần tử và gán các chuỗi Hello, From, và Rust vào đó.

Trong trường hợp mảng số, dữ liệu số thực sự nằm tuyến tính tại address1, address2, address3. Nhưng lần này, dữ liệu thực tế nằm rải rác ở các vị trí khác nhau, còn address1, address2, address3 chỉ đang giữ các con trỏ trỏ đến dữ liệu thực tế. Không cần phải nhớ quá kỹ, nhưng thực ra con trỏ này được định nghĩa là kiểu dữ liệu phức hợp gọi là BSTR (Basic string or binary string). Thực tế, ngay cả chuỗi đơn lẻ được cấp phát trong VBA mà chúng ta đã xử lý ở [Phần 1], bản chất con trỏ của nó cũng là BSTR này.

Dưới đây là trang tài liệu MSDN về BSTR:

https://learn.microsoft.com/ja-jp/previous-versions/windows/desktop/automat/bstr

Như tài liệu có viết, kiểu dữ liệu phức hợp này đảm bảo rằng tại vị trí mà con trỏ trỏ tới, sẽ luôn là một khối nhớ bao gồm: Độ dài dữ liệu (Length prefix), Dữ liệu thực tế (Data string), và Ký tự kết thúc null (Terminator).

Điều này có nghĩa là — bạn hãy vừa nhìn vào hình ảnh lúc nãy vừa hình dung nhé — tại vị trí Dữ liệu thực tế (Data string), ta xác định địa chỉ bắt đầu của dữ liệu, và chỉ cần nắm được Độ dài dữ liệu (Length prefix) nằm ngay phía trước đó là có thể trích xuất dữ liệu ngay lập tức.

Và mối quan hệ lồng nhau từ Con trỏ của con trỏ (LPBSTR) -> Con trỏ (BSTR) -> Ký tự (OLECHAR) được ghi trong MSDN như sau:

typedef WCHAR OLECHAR; // Ký tự
typedef OLECHAR* BSTR; // Con trỏ
typedef BSTR* LPBSTR; // Con trỏ của con trỏ

Mối quan hệ này trong mảng chuỗi rất quan trọng, nếu hiểu được nó thì việc cài đặt sẽ dễ dàng hơn.

Nói một cách đơn giản, khi lấy một phần tử từ mảng chuỗi, cái chúng ta nhận được không phải là dữ liệu chuỗi thô, mà là một BSTR. Trong phần này, chúng ta hãy code với ý thức về điều đó!

Thực hành 1: VBA -> Rust (Tham chiếu)

Đầu tiên, chúng ta sẽ thực hiện một logic đơn giản: truyền mảng chuỗi từ VBA sang Rust và tham chiếu (bất biến) nó.

Trước đó, hãy thêm dòng sau vào Cargo.toml:

[dependencies.windows-sys]
version = "0.61"
features = ["Win32_Foundation"]

Crate windows-sys này giống với crate windows, nhưng nó là crate dành cho các thao tác cấp thấp (low-level) mang tính thủ công hơn. Mặc dù khá nguy hiểm vì bộ nhớ không tự động được giải phóng khi biến ra khỏi phạm vi (scope), nhưng nó cực kỳ hữu ích khi truyền dữ liệu ra bên ngoài Rust như trường hợp này.

Bây giờ chúng ta sẽ cài đặt xử lý phía Rust. Đây là logic nhận vào một mảng chuỗi từ VBA và trả về số lượng phần tử có chứa một chuỗi ký tự chỉ định.

use std::slice;
use windows_sys::Win32::Foundation::SysStringLen;

/// Nhận vào mảng chuỗi và đếm số lượng phần tử có chứa từ khóa cụ thể.
///
/// ## Args
/// - `strings_ptr` - Địa chỉ bắt đầu của mảng chứa các BSTR (con trỏ đến chuỗi UTF-16).
/// - `len` - Độ dài của mảng
/// - `keyword_ptr` - Con trỏ của chuỗi từ khóa tìm kiếm (BSTR).
///
/// ## Returns
/// - `i32` - Kết quả số lượng đếm được.
#[unsafe(no_mangle)]
pub extern "system" fn count_contains(
    strings_ptr: *const *const u16,
    len: i32,
    keyword_ptr: *const u16,
) -> i32 {
    // Guard clause (Kiểm tra điều kiện đầu vào)
    if strings_ptr.is_null() || len <= 0 {
        return 0;
    }
    // Chuyển đổi từ khóa sang String của Rust
    let keyword_len = unsafe { SysStringLen(keyword_ptr) as usize };
    let keyword_slice = unsafe { slice::from_raw_parts(keyword_ptr, keyword_len) };
    let keyword = String::from_utf16_lossy(keyword_slice);

    // Nhận diện mảng chuỗi (mảng các con trỏ) dưới dạng slice
    let raw_ptr_slice = unsafe { slice::from_raw_parts(strings_ptr, len as usize) };

    // Duyệt qua từng phần tử (con trỏ) của mảng -> tăng biến count
    let mut count = 0;
    for &s_ptr in raw_ptr_slice {
        // Kiểm tra null
        if s_ptr.is_null() {
            continue;
        }
        // Khôi phục chuỗi từ con trỏ và độ dài
        let s_len = unsafe { SysStringLen(s_ptr) as usize };
        let s_slice = unsafe { slice::from_raw_parts(s_ptr, s_len) };
        let s_val = String::from_utf16_lossy(s_slice);

        // Nếu có chứa từ khóa thì tăng biến đếm
        if s_val.contains(&keyword) {
            count += 1;
        }
    }
    count
}

Điểm đáng chú ý là chỗ sử dụng SysStringLen. Như đã giải thích ở trên, ta cần lấy độ dài dữ liệu nằm ngay trước dữ liệu thực tế rồi mới tạo dữ liệu, những xử lý như vậy đã được SysStringLen của crate windows-sys che giấu (encapsulate) đi giúp ta. Nhờ vậy, ngay cả những chuỗi có chứa ký tự null ở giữa cũng có thể được xử lý đúng.

Ở đây, phía Rust không nắm quyền sở hữu (ownership) dữ liệu thực tế. Trách nhiệm giải phóng bộ nhớ thuộc về phía VBA. Nghĩa là, theo cơ chế reference counting (đếm tham chiếu), khi biến ra khỏi phạm vi và không còn ai tham chiếu đến, nó sẽ tự động được giải phóng.

Phía VBA sẽ cài đặt như sau:

#If Win64 Then
    Private Declare PtrSafe Function rs_count_contains Lib "C:\my_rust_lib.dll" Alias "count_contains" ( _
        ByVal strArrayPtr As LongPtr, _
        ByVal arrLength As Long, _
        ByVal keyword As LongPtr _
    ) As Long
#End If

Sub TestStringArrayRead()
    Dim arr(2) As String
    arr(0) = "Apple Juice"
    arr(1) = "Orange Juice"
    arr(2) = "Apple Pie"

    ' Đếm các phần tử có chứa "Apple"
    Dim cnt As Long
    ' StrPtr("Apple") để truyền con trỏ từ khóa,
    ' VarPtr(arr(0)) để truyền địa chỉ đầu của mảng (mảng các con trỏ)
    cnt = rs_count_contains(VarPtr(arr(0)), 3, StrPtr("Apple"))

    Debug.Print "Count: " & cnt
End Sub

Code này gần giống với phần xử lý chuỗi ở [Phần 1] nên chắc không quá khó. Lần này chỉ khác một chút là chúng ta ý thức về BSTR nên đã kiểm tra độ dài dữ liệu. Thực tế thì việc chuỗi chứa null ở giữa là rất hiếm, nên cách tìm điểm kết thúc null như đã làm ở [Phần 1] cũng có thể bao quát hầu hết các trường hợp.

Thực hành 2: Rust -> VBA (Phương pháp nối chuỗi)

Tiếp theo, tôi sẽ giới thiệu cách truyền mảng chuỗi được tạo ở phía Rust sang VBA. Nếu Rust tạo SafeArray và truyền sang thì phía VBA sẽ rất nhàn (vì mảng VBA chính là SafeArray), nhưng việc cài đặt nó có độ khó rất cao. Vì vậy trước tiên, tôi sẽ giới thiệu phương pháp đơn giản hơn: nối các phần tử của mảng chuỗi lại thành 1 chuỗi duy nhất bằng ký tự phân cách, sau đó phía VBA sẽ dùng hàm Split để tách nó trở lại thành mảng. Hàm Split của VBA cũng khá nhanh nên về mặt thực tế thì không có vấn đề gì.

use std::{panic, ptr, slice};
use windows_sys::Win32::Foundation::{SysAllocStringLen, SysStringLen};

/// Nhận mảng chuỗi, tạo mảng các phần tử chứa từ khóa, sau đó nối thành 1 chuỗi bằng ký tự phân cách và trả về con trỏ.
///
/// ## Args
/// - `strings_ptr` - Địa chỉ bắt đầu của mảng BSTR.
/// - `len` - Độ dài mảng.
/// - `keyword_ptr` - Con trỏ từ khóa tìm kiếm (BSTR).
///
/// ## Returns
/// - `*mut u16` - Con trỏ của chuỗi kết quả (BSTR). Trả về con trỏ null nếu panic.
#[unsafe(no_mangle)]
pub extern "system" fn filter_join(
    strings_ptr: *const *const u16,
    len: i32,
    keyword_ptr: *const u16,
) -> *mut u16 {
    let result = panic::catch_unwind(|| {
        // Guard clause
        if strings_ptr.is_null() || len <= 0 {
            return ptr::null_mut();
        }

        // Chuyển đổi từ khóa sang String Rust
        let keyword_len = unsafe { SysStringLen(keyword_ptr) as usize };
        let keyword_slice = unsafe { slice::from_raw_parts(keyword_ptr, keyword_len) };
        let keyword = String::from_utf16_lossy(keyword_slice);

        // Tạo slice cho mảng
        let raw_ptr_slice = unsafe { slice::from_raw_parts(strings_ptr, len as usize) };

        // Duyệt từng phần tử -> thu thập vào results
        let mut results = Vec::new();
        for &s_ptr in raw_ptr_slice {
            // Kiểm tra null
            if s_ptr.is_null() {
                continue;
            }
            // Khôi phục chuỗi
            let s_len = unsafe { SysStringLen(s_ptr) as usize };
            let s_slice = unsafe { slice::from_raw_parts(s_ptr, s_len) };
            let s_val = String::from_utf16_lossy(s_slice);

            // Nếu thỏa mãn điều kiện thì thêm vào vector
            if s_val.contains(&keyword) {
                results.push(s_val);
            }
        }

        // Nối bằng ký tự Tab "\t"
        let joined = results.join("\t");
        // Chuyển sang UTF-16
        let joined_utf16: Vec<u16> = joined.encode_utf16().collect();
        // Cấp phát bộ nhớ cho BSTR & trả về con trỏ
        unsafe { SysAllocStringLen(joined_utf16.as_ptr(), joined_utf16.len() as u32) as *mut u16 }
    });

    match result {
        Ok(ptr) => ptr,
        Err(_) => ptr::null_mut(),
    }
}
' ==================================================
' Declare
' ==================================================
#If Win64 Then
    ' Rust API
    Private Declare PtrSafe Function rsFilterJoin Lib "C:\workspace\vba-rust-article\target\debug\vba_rust_article.dll" Alias "filter_join" ( _
        ByVal stringsPtr As LongPtr, _
        ByVal stringsLen As Long, _
        ByVal keyword As LongPtr _
    ) As LongPtr

    ' API copy bộ nhớ
    Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" ( _
        ByVal Destination As LongPtr, _
        ByVal Source As LongPtr, _
        ByVal Length As LongPtr _
    )

    ' API lấy độ dài BSTR
    Private Declare PtrSafe Function SysStringLen Lib "oleaut32" (ByVal bstr As LongPtr) As Long

    ' API giải phóng BSTR
    Private Declare PtrSafe Sub SysFreeString Lib "oleaut32" (ByVal bstr As LongPtr)
#End If


' ==================================================
' Main Function
' ==================================================
Sub TestSafeFilter()
    ' Cấp phát mảng chuỗi
    Dim arr(2) As String
    arr(0) = "Apple Juice"
    arr(1) = "Orange Juice"
    arr(2) = "Apple Pie"

    ' Chuỗi tìm kiếm
    Dim targetText As String
    targetText = "Apple"

    ' Gọi hàm Rust để tìm kiếm trong mảng
    Dim ptr As LongPtr
    ptr = rsFilterJoin(VarPtr(arr(0)), 3, StrPtr(targetText)) ' Con trỏ phần tử đầu, số lượng phần tử, chuỗi tìm kiếm

    ' Tạo biến String do VBA quản lý từ con trỏ (BSTR) Rust trả về
    Dim fullStr As String
    fullStr = GetStringFromBSTR(ptr) ' Hàm này sẽ giải phóng dữ liệu của ptr (sau đó ptr không dùng được nữa)

    ' Trường hợp không tìm thấy chuỗi
    If fullStr = "" Then Debug.Print "Target text " & targetText & " not found.": Exit Sub

    ' Tách ra và đưa trở lại mảng
    Dim result() As String
    result = Split(fullStr, vbTab)

    Debug.Print result(0) ' -> Apple Juice
    Debug.Print result(1) ' -> Apple Pie
End Sub


' ==================================================
' Util Function
' ==================================================
' Hàm lấy chuỗi BSTR được cấp phát phía Rust.
' Copy chuỗi từ ptr và tạo biến String do VBA quản lý. Sau đó giải phóng chuỗi tại ptr.
' Nếu gọi hàm này 2 lần trên cùng 1 ptr sẽ gây lỗi giải phóng bộ nhớ 2 lần (double free).
'
' Args:
'   `ptr` - Con trỏ chuỗi (BSTR).
'
' Returns:
'   `String` - Chuỗi lấy được từ con trỏ.
Function GetStringFromBSTR(ByVal ptr As LongPtr) As String
    If ptr = 0 Then GoTo FINALLY

    On Error GoTo FINALLY

    ' Lấy độ dài chuỗi
    Dim strLen As Long
    strLen = SysStringLen(ptr)

    ' Cấp phát buffer (Tạo String của VBA)
    GetStringFromBSTR = String$(strLen, vbNullChar)

    ' Copy bộ nhớ (BSTR của Rust -> biến String của VBA)
    ' Vì là Unicode nên số byte = số ký tự * 2
    CopyMemory StrPtr(GetStringFromBSTR), ptr, strLen * 2


FINALLY:
    If ptr = 0 Then
        GetStringFromBSTR = ""
    Else
        SysFreeString ptr
        ptr = 0
    End If
End Function

Kết quả sẽ hiển thị như hình dưới đây.

Nếu tìm kiếm một chuỗi không tồn tại trong mảng, thông báo sau sẽ hiện ra.

Trong [Phần 1], tôi đã nói truyền buffer là tốt nhất, nhưng trong trường hợp như thế này - ví dụ số lượng mảng ban đầu hoặc số ký tự của mỗi phần tử thay đổi tùy theo tình huống, và ta không đoán trước được độ lớn của chuỗi trả về từ Rust - thì thay vì truyền buffer, việc để Rust tự cấp phát chuỗi có lẽ sẽ tốt hơn.

Ngoài ra, ở [Phần 1] khi dùng CString để cấp phát chuỗi, ta phải tạo hàm giải phóng bên phía Rust và VBA phải gọi hàm đó cuối cùng. Nhưng lần này, nếu Rust tạo chuỗi dưới dạng BSTR, phía VBA có thể sử dụng Windows API SysFreeString để giải phóng dữ liệu mà Rust đã cấp phát. Dù sao đi nữa, so với truyền buffer thì ta cần cẩn thận hơn một chút trong việc giải phóng bộ nhớ.

Như đã nói, Split của VBA khá nhanh, nên hầu hết trường hợp trả về theo kiểu nối chuỗi này là ổn. Tuy nhiên, nếu muốn tối ưu hóa triệt để, hãy cân nhắc việc tạo SafeArray phía Rust và truyền sang VBA. Độ khó cài đặt sẽ cao hơn, nhưng code phía VBA sẽ gọn gàng hơn và hiệu suất cũng tăng lên vài lần. Tuy nhiên bài viết này sẽ không đề cập đến nó. Có thể tôi sẽ viết một bài riêng về nó trong tương lai.

Kiểm tra hiệu năng (Benchmark Test)

Cuối cùng, để khép lại [Phần 3], chúng ta sẽ thực hiện benchmark test. Code Rust giữ nguyên, hãy viết lại code VBA như sau:

' ==================================================
' Declare
' ==================================================
#If Win64 Then
    ' Rust API
    Private Declare PtrSafe Function rsFilterJoin Lib "C:\workspace\vba-rust-article\target\debug\vba_rust_article.dll" Alias "filter_join" ( _
        ByVal stringsPtr As LongPtr, _
        ByVal stringsLen As Long, _
        ByVal keyword As LongPtr _
    ) As LongPtr

    ' API copy bộ nhớ
    Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" ( _
        ByVal Destination As LongPtr, _
        ByVal Source As LongPtr, _
        ByVal Length As LongPtr _
    )

    ' API lấy độ dài BSTR
    Private Declare PtrSafe Function SysStringLen Lib "oleaut32" (ByVal bstr As LongPtr) As Long

    ' API giải phóng BSTR
    Private Declare PtrSafe Sub SysFreeString Lib "oleaut32" (ByVal bstr As LongPtr)

    Private Declare PtrSafe Function QueryPerformanceFrequency Lib "kernel32" (lpFrequency As Currency) As Long
    Private Declare PtrSafe Function QueryPerformanceCounter Lib "kernel32" (lpPerformanceCount As Currency) As Long
#End If


' ==================================================
' Main Function
' ==================================================
Sub BenchmarkTest()
    ' Số lượng phần tử mảng.
    Dim maxCount As Long
    maxCount = 50000

    ' Cấp phát mảng chuỗi.
    Dim data() As String
    data = CreateTestData(maxCount)

    ' Chuỗi tìm kiếm.
    Dim targetText As String
    targetText = "Target"

    Dim timeStart As Currency, timeEnd As Currency, freq As Currency
    QueryPerformanceFrequency freq

    ' ----- VBA Process -----
    Debug.Print "---------- [VBA] ----------"
    Dim fullStrVba As String
    ' Bắt đầu đo (VBA)
    QueryPerformanceCounter timeStart
    ' Xử lý chính
    For i = 0 To maxCount - 1
        If InStr(data(i), targetText) > 0 Then
            If Len(fullStrVba) = 0 Then
                fullStrVba = data(i)
            Else
                fullStrVba = fullStrVba & vbTab & data(i)
            End If
        End If
    Next i
    If fullStrVba = "" Then Debug.Print "(VBA)Target text " & targetText & " not found.": Exit Sub
    Dim vbaResult() As String
    vbaResult = Split(fullStrVba, vbTab)
    ' Kết thúc đo (VBA)
    QueryPerformanceCounter timeEnd
    Debug.Print "Time  : " & Format$((timeEnd - timeStart) / freq, "0.0000") & " sec"
    Debug.Print "Result Len: " & UBound(vbaResult)


    ' ----- Reset Timer -----
    QueryPerformanceFrequency freq


    ' ----- Rust Procces -----
    Debug.Print "---------- [Rust] ----------"
    ' Bắt đầu đo (Rust)
    QueryPerformanceCounter timeStart
    ' Xử lý chính
    Dim ptr As LongPtr
    ptr = rsFilterJoin(VarPtr(data(0)), maxCount, StrPtr(targetText))
    Dim fullStrRust As String
    fullStrRust = GetStringFromBSTR(ptr)
    If fullStrRust = "" Then Debug.Print "(Rust)Target text " & targetText & " not found.": Exit Sub
    Dim rustResult() As String
    rustResult = Split(fullStrRust, vbTab)
    ' Kết thúc đo (Rust)
    QueryPerformanceCounter timeEnd

    Debug.Print "Time  : " & Format$((timeEnd - timeStart) / freq, "0.0000") & " sec"
    Debug.Print "Result Len: " & UBound(rustResult)
End Sub


' ==================================================
' Util Function
' ==================================================
' Hàm tạo dữ liệu test benchmark.
'
' Args:
'   `maxCnt` - Số lượng mảng cần tạo.
'
' Returns:
'   `String()` - Mảng chuỗi.
Function CreateTestData(maxCnt As Long) As String()
    Dim i As Long
    Dim data() As String
    ReDim data(maxCnt - 1)
    ' Định dạng "Data_0_Value" ~. Điều chỉnh để một nửa số lượng khớp điều kiện.
    For i = 0 To maxCnt - 1
        If i Mod 2 = 0 Then
            data(i) = "Target_" & i & "_Hit"
        Else
            data(i) = "Ignore_" & i & "_Pass"
        End If
    Next i
    CreateTestData = data
End Function

' Hàm lấy chuỗi BSTR được cấp phát phía Rust.
' Copy chuỗi từ ptr và tạo biến String do VBA quản lý. Sau đó giải phóng chuỗi tại ptr.
' Nếu gọi hàm này 2 lần trên cùng 1 ptr sẽ gây lỗi giải phóng bộ nhớ 2 lần (double free).
'
' Args:
'   `ptr` - Con trỏ chuỗi (BSTR).
'
' Returns:
'   `String` - Chuỗi lấy được từ con trỏ.
Function GetStringFromBSTR(ByVal ptr As LongPtr) As String
    If ptr = 0 Then GoTo FINALLY

    On Error GoTo FINALLY

    ' Lấy độ dài chuỗi
    Dim strLen As Long
    strLen = SysStringLen(ptr)

    ' Cấp phát buffer (Tạo String của VBA)
    GetStringFromBSTR = String$(strLen, vbNullChar)

    ' Copy bộ nhớ (BSTR của Rust -> biến String của VBA)
    ' Vì là Unicode nên số byte = số ký tự * 2
    CopyMemory StrPtr(GetStringFromBSTR), ptr, strLen * 2


FINALLY:
    If ptr = 0 Then
        GetStringFromBSTR = ""
    Else
        SysFreeString ptr
        ptr = 0
    End If
End Function

Code này cài đặt cùng một logic xử lý bằng VBA để so sánh thời gian. Trước khi đo thời gian, nó sẽ tạo một mảng với số lượng chỉ định. Đầu tiên, hãy thử thiết lập số lượng mảng là 50,000! Kết quả lần 1 hiển thị như hình dưới.

VBA tốn khoảng 8 giây, trong khi Rust chỉ mất khoảng 0.1 giây để hoàn thành. Chênh lệch khoảng 80 lần (gốc là 8 lần, nhưng theo hình là 8s vs 0.1s).

Giữ nguyên số lượng mảng và chạy lại lần nữa.

Ủa? Tốc độ của VBA rút xuống còn khoảng 1 giây. Rust thì không thay đổi mấy. Nhấn chạy lại lần nữa thì VBA vẫn giữ ở mức 1 giây. Thế là tôi thử khởi động lại Excel rồi chạy lại, nó lại quay về mức 8 giây ban đầu!

Nguyên nhân có thể là do tái sử dụng phân bổ bộ nhớ trước đó hoặc do cache gì đó tôi không rõ, nhưng hóa ra lại có chuyện như vậy. Tuy nhiên, hành vi không ổn định thế này thực sự không tốt khi xây dựng ứng dụng nghiệp vụ. Nếu mảng nhiều hơn thì sao? Tôi khởi động lại Excel, đổi số lượng mảng thành 500,000 và chạy thử.

Lần này VBA tốn tận 1,171 giây. Tức là 20 phút. Đây là thời gian chờ đợi chí mạng đối với một ứng dụng nghiệp vụ. Hơn nữa, việc thời gian xử lý bị dao động tùy thuộc vào hoàn cảnh chạy thì không thể coi là chất lượng ứng dụng tốt được.

Ngược lại, xử lý phía Rust dù chạy bao nhiêu lần với cùng số lượng mảng cũng đều ổn định. Xét về khía cạnh thời gian xử lý, chất lượng ứng dụng nghiệp vụ được đảm bảo.

Lần chạy thứ 2 với 500,000 phần tử:

1,017 giây. Tức là khoảng 17 phút. Vẫn quá chậm...

Lý do để đưa Rust vào sử dụng: không chỉ Nhanh mà còn Đảm bảo tốc độ luôn ổn định, đây chẳng phải là lợi thế cực lớn để đảm bảo chất lượng hệ thống nghiệp vụ hay sao?

Lần này chúng ta đã đề cập đến mảng chuỗi. Vì không tạo trực tiếp SafeArray và phía Rust phải tạo String khá nhiều nên chưa thể nói là tối ưu hiệu năng nhất, nhưng vẫn có thể kỳ vọng hiệu năng như kết quả benchmark. Lưu ý: Nếu Rust tạo SafeArray trực tiếp thì xử lý mảng 500,000 phần tử vừa rồi chắc chỉ mất dưới 0.1 giây.

Dù có hơi vất vả một chút nhưng việc gọi Rust từ VBA quả thật rất đáng giá nhỉ!

Hả? Nếu phải code như thế này thì thà dùng Python còn hơn á? Chà.... đúng là vậy thật 😅

Nhưng mà, khi phân phối ứng dụng, người dùng không cần phải thiết lập môi trường Python trên máy tính, và việc thực thi cũng như phản hồi diễn ra ngay khi Excel đang mở, nên đối với người dùng thì khá tiện lợi đấy chứ.

Giá mà có thể che giấu được các thao tác bộ nhớ phức tạp này thì tốt biết mấy...

Hẹn gặp lại các bạn trong bài tới, chúng ta sẽ bàn về Cấu trúc (Struct). Hãy đón chờ nhé!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí