为什么我的 BSTR 到 std::wstring 的转换这么慢?我的测试仪不好吗?

Why is my BSTR to std::wstring conversion so slow? Is my tester bad?

本文关键字:我的 测试仪 转换 BSTR std wstring 为什么      更新时间:2023-10-16

我经常需要将BSTR字符串转换为std::wstringNULLBSTR算作空BSTR

我曾经这样做过:

#define CHECKNULLSTR(str) ((str) ? (str) : L"")
std::wstring wstr(CHECKNULLSTR(bstr));

它不处理内部''字符,但它还需要在分配足够的内存之前计算字符数,因此它应该很慢。我想到了这个优化,它应该处理每种情况,不会截断,也不需要计数:

std::wstring wstr(bstr, bstr + ::SysStringLen(bstr));

为了测试此更改的影响,我编写了以下测试器。它表明,在大多数情况下,优化所需的时间是原来的两倍以上。在调试和发布配置中都可以观察到此更改,并且我使用的是VC++ 2013。

因此,我的问题是,这是怎么回事?"指针对"迭代器构造函数怎么会比 C 字符串构造函数慢得多?

完整的测试仪

#include <windows.h>
#include <stdio.h>
#include <tchar.h>
#include <strsafe.h>
#include <iostream>
#define CHECKNULLSTR(str) ((str) ? (str) : L"")
ULONGLONG bstrAllocTest(UINT iterations = 10000)
{
ULONGLONG totallen = 0;
ULONGLONG start, stop, elapsed1, elapsed2;    
BSTR bstr = ::SysAllocString( // 15 * 50 = 750 chars
L"01234567890123456789012345678901234567890123456789" //  1
L"01234567890123456789012345678901234567890123456789" //  2
L"01234567890123456789012345678901234567890123456789" //  3
L"01234567890123456789012345678901234567890123456789" //  4
L"01234567890123456789012345678901234567890123456789" //  5
L"01234567890123456789012345678901234567890123456789" //  6
L"01234567890123456789012345678901234567890123456789" //  7
L"01234567890123456789012345678901234567890123456789" //  8
L"01234567890123456789012345678901234567890123456789" //  9
L"01234567890123456789012345678901234567890123456789" // 10
L"01234567890123456789012345678901234567890123456789" // 11
L"01234567890123456789012345678901234567890123456789" // 12
L"01234567890123456789012345678901234567890123456789" // 13
L"01234567890123456789012345678901234567890123456789" // 14
L"01234567890123456789012345678901234567890123456789" // 15
);
start = ::GetTickCount64();
for (UINT i = 1; i <= iterations; ++i)
{
std::wstring wstr(CHECKNULLSTR(bstr));
size_t len;
::StringCchLengthW(wstr.c_str(), STRSAFE_MAX_CCH, &len);
totallen += len;
}
stop = ::GetTickCount64();
elapsed1 = stop - start;
start = ::GetTickCount64();
for (UINT i = 1; i <= iterations; ++i)
{
std::wstring wstr(bstr, bstr + ::SysStringLen(bstr));
size_t len;
::StringCchLengthW(wstr.c_str(), STRSAFE_MAX_CCH, &len);
totallen += len;
}
stop = ::GetTickCount64();
elapsed2 = stop - start;
wprintf_s(L"Iter:t%un"
L"Elapsed (CHECKNULLSTR):t%10llu msn"
L"Elapsed (Ptr iter pair):t%10llu msn"
L"Speed difference:t%f %%n",
iterations,
elapsed1,
elapsed2,
(static_cast<double>(elapsed2) / elapsed1 * 100));
::SysFreeString(bstr);
return totallen;
}
int wmain(int argc, char* argv[])
{
ULONGLONG dummylen = bstrAllocTest(100 * 1000);
wprintf_s(L"nTotal length:t%llu", dummylen);
getchar();
return 0;
}

我的系统上的输出

Iter:   100000
Elapsed (CHECKNULLSTR):        296 ms
Elapsed (Ptr it pair):         577 ms
Speed difference:       194.932432 %
Total length:   150000000

有趣,确实有点令人惊讶。Visual C++ 2013 Update 4 的性能差异在于两个std::wstring构造函数在其标准库中的实现方式。一般来说,采用一对迭代器的构造函数必须处理更多情况,因为这些迭代器不一定是指针,它们可以指向字符串字符类型以外的其他数据类型(字符类型只需要从迭代器指向的类型构造)。但是,我希望实现能够使用优化的代码单独处理您的情况。

std::wstring wstr(CHECKNULLSTR(bstr));确实扫描字符串以查找结束0,然后分配,然后使用memcpy以最快的方式复制字符串数据,这是使用汇编代码实现的。

std::wstring wstr(bstr, bstr + ::SysStringLen(bstr));确实因为::SysStringLen而避免了扫描(这非常快,只是读取存储的长度),然后分配,但随后使用以下循环复制字符串数据:

for (; _First != _Last; ++_First)
append((size_type)1, (_Elem)*_First);

VC12决定不内联append呼叫(可以理解,身体很大),正如您可以想象的那样,与炽热的memcpy相比,所有这些都承担了相当多的开销。


一种解决方案是使用接受指针和计数的std::basic_string构造函数(Ben Voigt 在他的评论中也提到了),如下所示:

std::wstring wstr(CHECKNULLSTR(bstr), ::SysStringLen(bstr));

我刚刚测试了它,它确实在Visual C++ 2013上带来了预期的好处 - 有时只需要第一个版本的一半时间,在最坏的情况下大约需要75%的时间(无论如何,这些都是近似的测量值)。


Visual C++ 2015 CTP6 中的标准库实现具有优化的代码路径,当迭代器实际上是指向与要构造的字符串相同的字符类型时,构造函数采用迭代器对,从而生成与上述指针和计数变体基本相同的代码。因此,在此版本上,将这两个构造函数变体中的哪一个用于您的情况并不重要 - 它们都比仅使用指针的版本更快。