为什么添加“noexcept”关键字会损害函数性能?

问题描述 投票:0回答:1

编辑:我不太关心我自己的例子。我可以理解为什么您想要一个可重现的示例,因为您可能不相信添加 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 查看了此代码的一个细微变体,但发现两者之间没有区别。

c++ performance visual-c++ noexcept
1个回答
2
投票

对此类函数进行性能测试的最佳方法是查看其优化的非内联程序集,无论是否带有

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
。这显然会影响性能和代码大小。

演示

© www.soinside.com 2019 - 2024. All rights reserved.