编辑:我不太关心我自己的例子。我可以理解为什么您想要一个可重现的示例,因为您可能不相信添加 no except 甚至可能会损害性能。我决定重写这篇文章并添加一个完整的示例。
我期望将关键字 noexcept 放在永远不会抛出异常的函数上,永远不会损害该函数的性能。就像您不会期望添加关键字 const 一样,会产生任何负面影响。
当在下面的例子中向函数 isVowel 添加 noexcept 时,它似乎损害了它的性能。
noexcept 是否会减慢不能抛出异常的函数的速度?
下面我生成一个仅包含元音布尔集的
std::array<bool,256>
。然后函数 bool isVowel(char c)
使用生成的数组查找字符是否为元音。这里向函数 isVowel 添加/删除 noexcept 似乎改变了它的性能。
#include <array>
#include <limits>
static constexpr unsigned char vowels[]{ 'a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U' };
auto constexpr genVowelTestMap() {
std::array<bool, std::numeric_limits<256> m{};
for (const unsigned char& c : vowels) {
m[c] = true;
}
return m;
}
static constexpr auto vowelTestMap = genVowelTestMap();
bool isVowel(const char c)noexcept/*The relevant noexcept*/{
return vowelTestMap[c];
}
在此复制中,我通过生成随机字母字符串来测试函数,然后使用字符串中的每个字符作为输入多次运行函数。完整代码可以在底部找到。
结果是以下时间测量:(在我最初的测试中,差异大约为 20%)
Time taken : noexcept version : normal version.
Time taken : 814786 microseconds:645568 microseconds. //The first is always slowest. Should prob be ignored.
Time taken : 675711 microseconds:612367 microseconds. //Run order was noexcept, normal and then repeat.
Time taken : 685613 microseconds:605072 microseconds.
Time taken : 655509 microseconds:607108 microseconds.
Time taken : 756300 microseconds:623599 microseconds.
Time taken : 718311 microseconds:605397 microseconds.
Time taken : 672052 microseconds:615306 microseconds.
Time taken : 703469 microseconds:608384 microseconds.
Time taken : 668540 microseconds:604204 microseconds.
Time taken : 667859 microseconds:605363 microseconds.
我使用带有 /O2、/Ob2、/Oi 和 /GL 的 MSVC 编译器使用 c++20 for x64 进行编译。 (对于此示例,我使用 Visual Studio 2022 创建了一个新的控制台项目,我更改的唯一设置是将版本更改为 c++20)。当切换到 /O3 时,两个功能执行相同。
#include <array>
#include <limits>
#include <string>
#include <cassert>
#include <chrono>
#include <iostream>
static constexpr unsigned char vowels[]{ 'a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U' };
auto constexpr genVowelTestMap() {
std::array<bool, 256> m{}; static_assert(sizeof(char) == 1);
for (const unsigned char& c : vowels) {
m[c] = true;
}
return m;
}
static constexpr auto vowelTestMap = genVowelTestMap();
inline bool isVowel(const char c){
return vowelTestMap[c];
}
inline bool isVowelNoExcept(const char c)noexcept{
return vowelTestMap[c];
}
std::string genRandomLetterString(size_t size) {
std::string s(size, '\0');
//24 + 24 options = 48 options.
for (size_t i = 0; i < size; i++) {
char& c = s[i];
c = std::rand() % 48;
if (c < 24) c += 'a';
else c += 'a' - 24;
assert(isalpha(c));
}
return s;
}
template<bool USE_NO_EXCEPT>
void runTest() {
const int runs = 1000;
auto start = std::chrono::high_resolution_clock::now();
//As people have pointed out this should have been done before the start. I don't agree that it was a bad idea to randomly generate them differently for both calls, because of caching and the likes.
//Though I could have reseeded the random number generator to still get the same strings.
const std::string s = genRandomLetterString(1u << 20);
//I measured the generator function, and it took 17'978ms, so not really significant. With it excluded the difference remains.
size_t a = 0;
for (int i = 0; i < runs; i++) {
for (const char& c : s) {
if constexpr (USE_NO_EXCEPT) {
if (isVowelNoExcept(c))a++;
}
else {
if (isVowel(c))a++;
}
}
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
std::cout << (USE_NO_EXCEPT?"NOEXCEPT":"\t") << "\tr(" << (a/runs) << ")Time taken : " << duration << " microseconds." << std::endl;
}
int main() {
for (int j = 0; j < 10; j++) {
runTest<true>();
runTest<false>();
}
}
我使用 godbolt 查看了此代码的一个细微变体,但发现两者之间没有区别。
对此类函数进行性能测试的最佳方法是查看其优化的非内联程序集,无论是否带有
noexcept
。
如果装配相同,那么性能也相同。
如果您看到异常表并在
terminate
版本中提到 noexcept
,则编译器被迫添加对 noexcept
的调用,以防函数尝试抛出异常。这至少会导致代码膨胀,这也会对性能产生负面影响。
我最好的猜测是
vowelTestMap
是一个简单的 256 字节数组,并且添加 noexcept
不会改变生成的程序集(至少在优化下)。
我在 godbolt 中投入了这么多:
#include <array>
static constexpr unsigned char vowels[]{ 'a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U' };
auto constexpr genVowelTestMap() {
std::array<bool, 256> m{}; static_assert(sizeof(char) == 1);
for (const unsigned char& c : vowels) {
m[c] = true;
}
return m;
}
static constexpr auto vowelTestMap = genVowelTestMap();
bool isVowel(const char c){
return vowelTestMap[c];
}
bool isVowelNoExcept(const char c)noexcept{
return vowelTestMap[c];
}
请注意,我取消了两个感兴趣的函数的内联。我将编译器设置为 x86 msvc v19.latest 并将选项设置为 /std:c++20 /O2。
生成的程序集对于两个功能来说是相同的:
bool isVowel(char) PROC ; isVowel, COMDAT
movsx eax, BYTE PTR _c$[esp-4]
mov al, BYTE PTR std::array<bool,256> const vowelTestMap[eax]
ret 0
bool isVowel(char) ENDP ; isVowel
_c$ = 8 ; size = 1
bool isVowelNoExcept(char) PROC ; isVowelNoExcept, COMDAT
movsx eax, BYTE PTR _c$[esp-4]
mov al, BYTE PTR std::array<bool,256> const vowelTestMap[eax]
ret 0
bool isVowelNoExcept(char) ENDP
演示。
结论:添加
noexcept
对该函数的性能没有影响。它可能会对调用isVowel
的代码产生影响,但这超出了这个问题的范围。
如果
isVowel
尝试执行可能引发异常的操作,例如调用此函数:
void f();
然后得到非常不同的结果:
_c$ = 8 ; size = 1
bool isVowel(char) PROC ; isVowel, COMDAT
call void f(void) ; f
movsx eax, BYTE PTR _c$[esp-4]
mov al, BYTE PTR std::array<bool,256> const vowelTestMap[eax]
ret 0
bool isVowel(char) ENDP ; isVowel
__$EHRec$ = -12 ; size = 12
_c$ = 8 ; size = 1
bool isVowelNoExcept(char) PROC ; isVowelNoExcept, COMDAT
push ebp
mov ebp, esp
push -1
push __ehhandler$bool isVowelNoExcept(char)
mov eax, DWORD PTR fs:0
push eax
mov eax, DWORD PTR ___security_cookie
xor eax, ebp
push eax
lea eax, DWORD PTR __$EHRec$[ebp]
mov DWORD PTR fs:0, eax
call void f(void) ; f
movsx eax, BYTE PTR _c$[ebp]
mov al, BYTE PTR std::array<bool,256> const vowelTestMap[eax]
mov ecx, DWORD PTR __$EHRec$[ebp]
mov DWORD PTR fs:0, ecx
pop ecx
mov esp, ebp
pop ebp
ret 0
int 3
int 3
int 3
int 3
int 3
__ehhandler$bool isVowelNoExcept(char):
npad 1
npad 1
mov edx, DWORD PTR [esp+8]
lea eax, DWORD PTR [edx+12]
mov ecx, DWORD PTR [edx-4]
xor ecx, eax
call @__security_check_cookie@4
mov eax, OFFSET __ehfuncinfo$bool isVowelNoExcept(char)
jmp ___CxxFrameHandler3
bool isVowelNoExcept(char) ENDP ; isVowelNoExcept
isVowelNoExcept
现在必须设置代码来捕获 f()
可能抛出的任何异常,然后调用 std::terminate
。这显然会影响性能和代码大小。
演示。