分析 _mm_setzero_ps 和 {0.0f,0.0f,0.0f,0.0f}

Yol*_*ola 2 c++ profiling sse visual-studio

编辑:正如 Cody Gray 在他的评论中指出的那样,禁用优化的分析完全是浪费时间。那么我应该如何进行这个测试呢?


微软在其XMVectorZero情况下定义了_XM_SSE_INTRINSICS_使用_mm_setzero_ps{0.0f,0.0f,0.0f,0.0f}不使用。我决定看看胜利有多大。因此,我在 Release x86 中使用了以下程序,并将配置属性>C/C++>优化>优化设置为Disabled (/Od)

constexpr __int64 loops = 1e9;
inline void fooSSE() {
    for (__int64 i = 0; i < loops; ++i) {
        XMVECTOR zero1 = _mm_setzero_ps();
        //XMVECTOR zero2 = _mm_setzero_ps();
        //XMVECTOR zero3 = _mm_setzero_ps();
        //XMVECTOR zero4 = _mm_setzero_ps();
    }
}
inline void fooNoIntrinsic() {
    for (__int64 i = 0; i < loops; ++i) {
        XMVECTOR zero1 = { 0.f,0.f,0.f,0.f };
        //XMVECTOR zero2 = { 0.f,0.f,0.f,0.f };
        //XMVECTOR zero3 = { 0.f,0.f,0.f,0.f };
        //XMVECTOR zero4 = { 0.f,0.f,0.f,0.f };
    }
}
int main() {
    fooNoIntrinsic();
    fooSSE();
}
Run Code Online (Sandbox Code Playgroud)

我第一次运行该程序两次,仅使用 01,第二次运行该程序,所有行均未注释。在第一种情况下,内在因素会失败,在第二种情况下,内在因素显然是赢家。所以,我的问题是:

  • 为什么内在并不总是获胜?
  • 我使用的分析器是否适合进行此类测量?

在此输入图像描述

在此输入图像描述

Cod*_*ray 5

在禁用优化的情况下分析事物会给您带来毫无意义的结果,并且完全是浪费时间。如果您禁用优化,否则优化器会注意到您的基准测试实际上没有做任何有用的事情并完全删除它,那么欢迎迎接微基准测试的困难!

\n\n

通常很难编造一个测试用例,该测试用例实际上做了足够多的实际工作,以至于不会被足够智能的优化器删除,但该工作的成本不会压倒你的结果并使你的结果变得毫无意义。例如,很多人的第一反应是使用类似的方法打印增量结果printf,但那是行不通的,因为速度printf非常慢,并且绝对会毁掉你的基准测试。使收集中间值的变量volatile有时会起作用,因为它有效地禁用了该特定变量的加载/存储优化。尽管这依赖于不明确的语义,但这对于基准测试来说并不重要。另一种选择是对中间结果执行一些无意义但相对便宜的操作,例如将它们加在一起。这依赖于优化器不会比您更聪明,并且为了验证您的基准测试结果是否有意义,您必须检查编译器发出的目标代码并确保代码实际上正在执行该操作。不幸的是,没有制作微基准的灵丹妙药。

\n\n

最好技巧通常是隔离函数内部代码的相关部分,将其参数化为一个或多个不可预测的输入值,安排返回结果,然后将此函数放入外部模块中,以便优化器可以不要让肮脏的爪子踩到它。

\n\n

由于无论如何您都需要查看反汇编来确认您的微基准测试用例是否合适,因此这通常是一个不错的起点。如果您有足够的能力阅读汇编语言,并且您已经充分提炼了相关代码,这甚至足以让您对代码的效率做出判断。如果您无法弄清楚代码,那么它可能已经足够复杂,您可以继续对其进行基准测试。

\n\n

这是一个很好的例子,说明对生成的目标代码进行粗略检查就足以回答问题,甚至不需要制定基准。

\n\n

按照我上面的建议,让我们编写一个简单的函数来测试内在函数。在这种情况下,我们没有任何输入来参数化,因为代码实际上只是将寄存器设置为 0。所以让我们返回从函数

\n\n
DirectX::XMVECTOR ZeroTest_Intrinsic()\n{\n    return _mm_setzero_ps();\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

这是另一个以看似 na\xc3\xafve 方式执行初始化的候选者:

\n\n
DirectX::XMVECTOR ZeroTest_Naive()\n{\n    return { 0.0f, 0.0f, 0.0f, 0.0f };\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

这是编译器为这两个函数生成的目标代码(无论哪个版本,无论您是针对 x86-32 还是 x86-64 进行编译,或者是否针对大小或速度进行优化;结果都是相同的) :

\n\n
ZeroTest_Intrinsic\n    xorps  xmm0, xmm0\n    ret\n
Run Code Online (Sandbox Code Playgroud)\n\n\n\n
ZeroTest_Naive\n    xorps  xmm0, xmm0\n    ret\n
Run Code Online (Sandbox Code Playgroud)\n\n

(如果支持 AVX 或 AVX2 指令,则它们都将为vxorps xmm0, xmm0, xmm0。)

\n\n

即使对于无法阅读汇编代码的人来说,这是非常明显的。他们都是一样的!我想说,这相当明确地回答了哪个更快的问题:它们将是相同的,因为优化器识别出看似 na\xc3\xafve 初始化程序并将其转换为单个优化的汇编语言指令,用于清除登记。

\n\n

现在,在某些情况下,它肯定可能被深深嵌入到各种复杂的代码结构中,从而阻止优化器识别它并发挥其魔力。换句话说,“你的测试功能太简单了!” 异议。这很可能就是为什么库的实现者选择在可用时显式使用内在函数的原因。它的使用保证了代码生成器将发出所需的指令,因此代码将被尽可能优化。

\n\n

显式使用内在函数的另一个可能的好处是确保您获得所需的指令,即使代码是在没有 SSE/SSE2 支持的情况下编译的。正如我想象的那样,这并不是一个特别引人注目的用例,因为如果可以接受使用这些指令,那么如果没有 SSE/SSE2 支持,您就不会进行编译。如果您明确尝试禁用 SSE/SSE2 指令的生成,以便可以在旧系统上运行,则内在函数会毁掉您的一天,因为它会强制xorps发出指令,并且旧系统会抛出无效的操作异常点击此指令后立即。

\n\n

不过,我确实看到了一个有趣的案例。xorps是该指令的单精度版本,并且仅需要 SSE 支持。但是,如果我仅使用 SSE 支持(无 SSE2)编译上面显示的函数,则会得到以下结果:

\n\n
ZeroTest_Intrinsic\n    xorps  xmm0, xmm0\n    ret\n
Run Code Online (Sandbox Code Playgroud)\n\n\n\n
ZeroTest_Naive\n    push   ebp\n    mov    ebp, esp\n    and    esp, -16\n    sub    esp, 16\n\n    mov    DWORD PTR [esp],    0\n    mov    DWORD PTR [esp+4],  0\n    mov    DWORD PTR [esp+8],  0\n    mov    DWORD PTR [esp+12], 0\n    movaps xmm0, XMMWORD PTR [esp]\n\n    mov    esp, ebp\n    pop    ebp\n    ret\n
Run Code Online (Sandbox Code Playgroud)\n\n

显然,由于某种原因,当 SSE2 指令支持不可用时,优化器无法优化应用于初始化程序的使用,即使xorps它将使用的指令不需要 SSE2 指令支持!这可以说是优化器中的一个错误,但显式使用内部函数可以解决它。

\n