Vấn đề về hiệu năng khi thực hiện ghép chuỗi trong VBA với tần suất lớn (5 người xem)

Người dùng đang xem chủ đề này

  • Tôi tuân thủ nội quy khi đăng bài

    nguyendang95

    Thành viên hoạt động
    Tham gia
    25/5/22
    Bài viết
    174
    Được thích
    162
    Kiểu chuỗi trong VBA là kiểu dữ liệu không biến đổi được (immutable string), tức là chuỗi một khi đã được tạo ra thì kích thước của nó sẽ luôn cố định và không thể thay đổi được. Như vậy khi ghép hai chuỗi với nhau, VBA phải thực hiện những công việc sau đây:
    VD: Với biểu thức str = "a" & "b".
    1. Xác định số lượng ký tự của hai chuỗi "a" và "b".
    2. Xin hệ điều hành cấp phát vùng nhớ vừa đủ để chứa hai chuỗi trên.
    3. Sao chép hai chuỗi trên vào vùng nhớ mới được cấp phát.
    Kiểu chuỗi trong VBA là kiểu BSTR của COM, kiểu dữ liệu này không khác gì kiểu chuỗi wchar_t của C/C++ ngoại trừ ngay trước mảng chuỗi là 4 byte kiểu long đại diện cho số byte cần dùng để biểu diễn chuỗi và con trỏ trỏ vào phần tử đầu tiên của mảng chuỗi (hay trỏ đến ký tự đầu tiên).

    1769177441880.png

    Khi thực hiện ghép chuỗi với tần suất không quá nhiều và phép ghép chuỗi đơn giản, lập trình viên có thể không nhận ra sự khác biệt về hiệu năng vì VBA thực hiện công việc nói trên rất nhanh. Tuy nhiên khi cần ghép chuỗi với tần suất lớn thì mọi chuyện sẽ khác:
    Mã:
    Option Explicit
    
    Private Sub ConcatenateString()
        Dim str As String
        Dim startTime As Date, endTime As Date, elapsedTime As Long
        startTime = Now()
        Dim i As Long
        For i = 1 To 1000000
            str = str & "a"
        Next
        endTime = Now()
        elapsedTime = DateDiff("s", startTime, endTime, vbUseSystemDayOfWeek, vbUseSystem)
        Debug.Print "Elapsed time: " & CStr(elapsedTime) & " seconds"
    End Sub
    Rõ ràng, khi thực hiện 1 triệu lần ghép chuỗi, trong vòng lặp VBA thực hiện liên tục thao tác xin cấp phát vùng nhớ mới, sao chép chuỗi hiện tại vào vùng nhớ mới và nối chuỗi mới vào vùng nhớ mới chứa chuỗi hiện tại thì thời gian thực hiện kéo dài đáng kể và rất lâu (trong trường hợp này theo phép đo chủ quan phải mất 209 giây để chạy xong mã VBA trên).
    Nhược điểm này không chỉ tồn tại ở VBA mà ngay cả những ngôn ngữ lập trình hiện đại hơn như Java lẫn ngôn ngữ nền .NET như C#, Visual Basic và F# cũng thế. Cho nên để giải quyết tình trạng này, người ta thiết kế một cơ chế đặc biệt gọi là string builder (trình xây dựng chuỗi), cái này có nhiệm vụ như sau:
    • Chuẩn bị trước vùng nhớ lớn để chứa chuỗi thay vì gặp chuỗi nào thì xin cấp phát vùng nhớ để chứa thêm chuỗi mới đó.
    • Mỗi ghi có chuỗi mới cần ghép thì sao chép chuỗi đó vào vùng nhớ đã chuẩn bị trước, khi vùng nhớ sắp đầy thì mới xin cấp phát thêm từ hệ điều hành, từ đó giảm thiểu đáng kể số lần xin cấp phát thêm vùng nhớ từ hệ điều hành, giúp hoạt động ghép chuỗi diễn ra nhanh chóng.
    Để biểu diễn vấn đề trên, trong bài viết này trình bày mã viết bằng Visual C++ về một trình string builder đơn giản. Thật ra C++ hỗ trợ một lớp (class) tên là std::wstringstream nằm trong tập tin tiêu đề (header file) sstream (#include<sstream>) có chức năng tương tự, tuy nhiên mã trong bài viết này không sử dụng nó, chủ yếu nhằm mục đích diễn giải cách hoạt động của string builder mà thôi.
    Khởi chạy Visual Studio, tạo một dự án mới kiểu Dynamic-Link Library (DLL) và chèn vào đoạn mã dưới đây:
    dllmain.cpp
    C++:
    // dllmain.cpp : Defines the entry point for the DLL application.
    #include "pch.h"
    
    BOOL APIENTRY DllMain( HMODULE hModule,
                           DWORD  ul_reason_for_call,
                           LPVOID lpReserved
                         )
    {
        switch (ul_reason_for_call)
        {
        case DLL_PROCESS_ATTACH:
        case DLL_THREAD_ATTACH:
        case DLL_THREAD_DETACH:
        case DLL_PROCESS_DETACH:
            break;
        }
        return TRUE;
    }
    
    typedef struct _STRINGBUILDER {
        wchar_t* buffer;
        DWORD dwccharacters;
        DWORD dwCapacity;
    }STRINGBUILDER, *LPSTRINGBUILDER;
    
    extern "C" _declspec(dllexport) LPSTRINGBUILDER WINAPI StringBuilderInitialize() {
        LPSTRINGBUILDER sb = (LPSTRINGBUILDER)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (SIZE_T)sizeof(STRINGBUILDER));
        if (!sb) {
            SetLastError(STRINGBUILDER_ERROR_OUT_OF_MEMORY);
            return NULL;
        }
        DWORD dwCapacity = ((1024 * 1024) * 1) * sizeof(wchar_t);
        sb->buffer = (wchar_t*)HeapAlloc(GetProcessHeap(), 0, (SIZE_T)dwCapacity);
        if (!sb->buffer) {
            HeapFree(GetProcessHeap(), 0, sb);
            SetLastError(STRINGBUILDER_ERROR_OUT_OF_MEMORY);
            return NULL;
        }
        sb->dwccharacters = 0;
        sb->buffer[sb->dwccharacters] = '\0';
        sb->dwCapacity = dwCapacity;
        return sb;
    }
    
    extern "C" _declspec(dllexport) BOOL WINAPI StringBuilderUninitialize(LPSTRINGBUILDER hObject) {
        if (!hObject || !hObject->buffer) {
            SetLastError(STRINGBUILDER_ERROR_INVALID_POINTER);
            return FALSE;
        }
        if (!HeapFree(GetProcessHeap(), 0, hObject->buffer)) {
            SetLastError(GetLastError());
            return FALSE;
        }
        if (!HeapFree(GetProcessHeap(), 0, hObject)) {
            SetLastError(GetLastError());
            return FALSE;
        }
        return TRUE;
    }
    
    extern "C" _declspec(dllexport) BOOL WINAPI StringBuilderAppend(LPSTRINGBUILDER hObject, const wchar_t* lpwstrValue) {
        if (!hObject || !lpwstrValue) {
            SetLastError(STRINGBUILDER_ERROR_INVALID_PARAMETER);
            return FALSE;
        }
        size_t len = wcslen(lpwstrValue);
        if (!len) {
            SetLastError(STRINGBUILDER_ERROR_EMPTY_STRING);
            return FALSE;
        }
        if (((hObject->dwccharacters * sizeof(wchar_t)) + (len * sizeof(wchar_t)) >= hObject->dwCapacity)) {
            DWORD dwNewCapacity = (hObject->dwCapacity + (len * sizeof(wchar_t))) * 1.5;
            wchar_t* ptr = (wchar_t*)HeapReAlloc(GetProcessHeap(), 0, hObject->buffer, (SIZE_T)dwNewCapacity);
            if (!ptr) {
                SetLastError(STRINGBUILDER_ERROR_OUT_OF_MEMORY);
                return FALSE;
            }
            hObject->buffer = ptr;
            hObject->dwCapacity += dwNewCapacity;
        }
        memcpy_s(&(hObject->buffer[hObject->dwccharacters]), (hObject->dwCapacity / sizeof(wchar_t)) - hObject->dwccharacters, lpwstrValue, len * sizeof(wchar_t));
        hObject->dwccharacters += (DWORD)len;
        hObject->buffer[hObject->dwccharacters] = '\0';
        return TRUE;
    }
    
    extern "C" _declspec(dllexport) VARIANT StringBuilderToString(LPSTRINGBUILDER hObject) {
        VARIANT varResult = {};
        if (!hObject || !hObject->buffer) {
            SetLastError(STRINGBUILDER_ERROR_INVALID_POINTER);
            varResult.vt = VT_EMPTY;
            return varResult;
        }
        BSTR ptr = SysAllocString(hObject->buffer);
        if (!ptr) {
            SetLastError(STRINGBUILDER_ERROR_OUT_OF_MEMORY);
            varResult.vt = VT_EMPTY;
            return varResult;
        }
        varResult.vt = VT_BSTR;
        varResult.bstrVal = ptr;
        return varResult;
    }

    StringBuilderErrorCodes.h
    C++:
    #pragma once
    #define STRINGBUILDER_ERROR_INVALID_POINTER 20000 | ((DWORD)1 << 29)
    #define STRINGBUILDER_ERROR_INVALID_PARAMETER 20001 | ((DWORD)1 << 29)
    #define STRINGBUILDER_ERROR_OUT_OF_MEMORY 20002 | ((DWORD)1 << 29)
    #define STRINGBUILDER_ERROR_EMPTY_STRING 20003 | ((DWORD)1 << 29)

    pch.h

    C++:
    // pch.h: This is a precompiled header file.
    // Files listed below are compiled only once, improving build performance for future builds.
    // This also affects IntelliSense performance, including code completion and many code browsing features.
    // However, files listed here are ALL re-compiled if any one of them is updated between builds.
    // Do not add files here that you will be updating frequently as this negates the performance advantage.
    
    #ifndef PCH_H
    #define PCH_H
    
    // add headers that you want to pre-compile here
    #include "framework.h"
    #include "StringBuilderErrorCodes.h"
    #include <Windows.h>
    #include <comdef.h>
    
    #endif //PCH_H

    Tiến hành biên dịch ra DLL tương ứng với phiên bản của VBA (32 bit hoặc 64 bit). Khi sử dụng DLL trong VBA để ghép chuỗi:
    Mã:
    Option Explicit
    
    Private Declare PtrSafe Function StringBuilderInitialize Lib "VBAStringBuilder.dll" () As LongPtr
    Private Declare PtrSafe Function StringBuilderUninitialize Lib "VBAStringBuilder.dll" (ByVal hObject As LongPtr) As Long
    Private Declare PtrSafe Function StringBuilderAppend Lib "VBAStringBuilder.dll" (ByVal hObject As LongPtr, ByVal lpwstrValue As LongPtr) As Long
    Private Declare PtrSafe Function StringBuilderToString Lib "VBAStringBuilder.dll" (ByVal hObject As LongPtr) As Variant
    Private Declare PtrSafe Function GetLastError Lib "Kernel32" () As Long
    
    Private Sub ConcatenateString()
        Dim sb As LongPtr, errorCode As Long
        sb = StringBuilderInitialize()
        If sb = 0 Then
            errorCode = GetLastError()
            Debug.Print "Error: " & CStr(errorCode)
            Exit Sub
        End If
        Dim i As Long
        Dim startTime As Date, endTime As Date, elapsedTime As Long
        startTime = Now()
        For i = 1 To 1000000
            If StringBuilderAppend(sb, StrPtr("a")) = 0 Then
                errorCode = GetLastError()
                Debug.Print "Error: " & CStr(errorCode)
                Exit Sub
            End If
        Next
        endTime = Now()
        elapsedTime = DateDiff("s", startTime, endTime, vbUseSystemDayOfWeek, vbUseSystem)
        Debug.Print "Elapsed time: " & CStr(elapsedTime) & " seconds"
        Call StringBuilderUninitialize(sb)
    End Sub

    Thời gian thực hiện 1 triệu lần ghép chuỗi đã giảm đáng kể so với phép ghép chuỗi thông thường của VBA (trong trường hợp này theo phép đo chủ quan, chỉ mất vỏn vẹn 4 giây để chạy xong mã trên).
     

    Bài viết mới nhất

    Back
    Top Bottom