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".

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:
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:
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
StringBuilderErrorCodes.h
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:
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).
VD: Với biểu thức str = "a" & "b".
- Xác định số lượng ký tự của hai chuỗi "a" và "b".
- Xin hệ điều hành cấp phát vùng nhớ vừa đủ để chứa hai chuỗi trên.
- Sao chép hai chuỗi trên vào vùng nhớ mới được cấp phát.

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
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.
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).
