NRVO 与未受益于移动语义的类型的提前返回(GCC 14 -Wnrvo)

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

GCC 14 引入了新的

-Wnrvo
标志

新的

-Wnrvo
警告,如果未执行指定的返回值优化,则发出警告,尽管[class.copy.elision]允许这样做。请参阅手册了解更多信息。

我决定看看如果我将标志添加到开源多媒体库SFML 3.x,会弹出什么警告,并且出现了一些警告,所有警告都与此处显示的情况类似:

struct JoystickCaps
{
    unsigned int buttonCount{};
    std::array<bool, 8> axes{}
};

// Early return version (original)
JoystickCaps JoystickImpl::getCapabilities() const
{
    if (directInput)
        return getCapabilitiesDInput(); // <- early return

    JoystickCaps caps;

    caps.buttonCount = m_caps.wNumButtons;
    if (caps.buttonCount > Joystick::ButtonCount)
        caps.buttonCount = Joystick::ButtonCount;

    // ...set more members in 'caps'...

    return caps;
}

此代码被 GCC 的新警告标记为:

./SFML/src/SFML/Window/Win32/JoystickImpl.cpp: 
    In member function 'JoystickCaps JoystickImpl::getCapabilities() const':

./SFML/src/SFML/Window/Win32/JoystickImpl.cpp:342:12: 
    warning: not eliding copy on return in 
   'JoystickCaps JoystickImpl::getCapabilities() const' [-Wnrvo]
  342 |     return caps;
      |            ^~~~

我决定更改代码,使警告静音,但我找到的唯一解决方案是这个:

// NRVO version
JoystickCaps JoystickImpl::getCapabilities() const
{
    JoystickCaps caps;

    if (directInput)
        caps = getCapabilitiesDInput();
    else 
    {
        caps.buttonCount = m_caps.wNumButtons;
        if (caps.buttonCount > Joystick::ButtonCount)
            caps.buttonCount = Joystick::ButtonCount;

        // ...set more members in 'caps'...
    }

    return caps;
}

不太满意,因为该解决方案的可读性较差,恕我直言,我决定对其进行基准测试,看看有什么好处,使用quick-bench.com

事实证明,使用

-O3
,早期返回版本比 NRVO 版本快 3.8 倍。

问题:

  1. 在这种情况下,GCC 的警告是否具有误导性? 在可以提前返回的情况下,有目的地避免 NRVO 是否有意义?

  2. 为什么 NRVO 版本慢这么多? 我的基准测试有错误吗?有没有一种方法可以重写该函数,使其既支持 NRVO,又不会比原来的效率低?

c++ gcc optimization return-value-optimization nrvo
1个回答
0
投票

TL;DR 这是误导性微基准的噩梦,被复杂的优化和初始化归零所混淆。该警告没有任何帮助,如果您处于性能关键环境中,则保留未初始化的值会产生影响。


我将放弃您在 Quickbench 中提供的

JoystickCaps
的定义,而不是 SFML 定义,因为这似乎与问题更相关

struct JoystickCaps
{
    unsigned int buttonCount{};
    int axes[16]{}; 
};

您要进行基准测试的循环是

bool b = true;
for (auto _ : state) 
{
    auto result = /* one of the functions passed with b as condition */;
    benchmark::DoNotOptimize(result);
    b = !b;
}

这里需要注意的重要一点是,进行基准测试的函数始终内联到循环本身中。此时,优化器看透

b
变量,然后将两个循环展开为一个循环并完全忽略
b = !b

由于我不太明白的原因,此时优化器放弃了将

result
的两个值构造
b
的公共部分折叠在一起,而是为每个值发出单独的指令集。这实际上是您观察到的主要放缓。

如果您改为对这个循环进行基准测试

bool b = true;
for (auto _ : state) 
{
    benchmark::DoNotOptimize(/* one of the functions passed with b as condition */);
    b = !b;
}

优化器的麻烦更少,并且函数的两个版本编译为相同的程序集,这与您看到的“提前返回”版本一样快。

Quickbench 链接


为了观察实际的 NRVO 或不生效,您可以强制函数不内联。在这种情况下,你会看到NRVO版本会先无条件清零

result
,然后根据分支条件决定填充什么值

f1(bool):
        pxor    xmm0, xmm0
        mov     DWORD PTR [rdi+64], 0
        mov     rax, rdi
        movups  XMMWORD PTR [rdi], xmm0
        movups  XMMWORD PTR [rdi+16], xmm0
        movups  XMMWORD PTR [rdi+32], xmm0
        movups  XMMWORD PTR [rdi+48], xmm0
        test    sil, sil

相比之下,早期返回版本首先分支并直接填写正确的值。同样,这是由于一些我不太理解的优化器怪癖造成的,就 C++ 标准而言,两者都是合法的。

对于 NRVO 版本来说,这累积起来速度会减慢约 1.2 倍。 与上面相同的快速工作台链接


最后,解决初始化时的归零问题。如果您要将

JoystickCaps
定义为

struct JoystickCaps
{
    unsigned int buttonCount;
    int axes[16]; 
};

然后当且仅当需要时手动将

axes
清零,然后您将看到
result
对象是否存储为局部变量不再阻碍优化器,并且每个人都被编译为相同的快速版本。

此外,如果我们在这种情况下强制使用 noinline,NRVO 实际上会快约 1.7 倍。这种加速是无法保证的,您不应该仅仅因为看到警告就进行重构,因此警告是没有帮助的。

Quickbench 链接

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