分析 _mm_setzero_ps 和 0.0f,0.0f,0.0f,0.0f

Posted

技术标签:

【中文标题】分析 _mm_setzero_ps 和 0.0f,0.0f,0.0f,0.0f【英文标题】:Profiling _mm_setzero_ps and 0.0f,0.0f,0.0f,0.0f分析 _mm_setzero_ps 和 0.0f,0.0f,0.0f,0.0f 【发布时间】:2016-12-21 17:00:26 【问题描述】:

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


Microsoft 在其XMVectorZero 中如果定义了_XM_SSE_INTRINSICS_,则使用_mm_setzero_ps,如果没有定义0.0f,0.0f,0.0f,0.0f。我决定检查一下胜利有多大。所以我在 Release x86 中使用了以下程序,并将 Configuration Properties>C/C++>Optimization>Optimization 设置为 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();

我第一次运行程序两次,只有 zero1 和第二次,所有行都未注释。在第一种情况下,内在输了,在第二种情况下,内在显然是赢家。所以,我的问题是:

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

【问题讨论】:

在禁用优化的情况下分析事物会给您带来毫无意义的结果,完全是浪费时间。 @CodyGray 非常感谢,我更新了问题 我不确定我是否理解您修改后的问题。你应该如何处理它?启用优化!我猜你已经发现优化器通过完全删除你的基准代码来胜过你? @CodyGray 是的,就是这样,所以我没有优化就做到了:) 我的意思是我如何重写这个测试 【参考方案1】:

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

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

最好的技巧通常是将代码的相关部分隔离在函数内部,将其参数化为一个或多个不可预测的输入值,安排返回结果,然后将其放入在一个外部模块中运行,这样优化器就无法得到它肮脏的爪子。

因为无论如何您都需要查看反汇编以确认您的微基准测试案例是否合适,所以这通常是一个不错的起点。如果你有足够的能力阅读汇编语言,并且你已经充分提炼了有问题的代码,这甚至可能足以让你对代码的效率做出判断。如果您无法确定代码的正面或反面,那么它可能已经足够复杂,您可以继续进行基准测试。

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

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

DirectX::XMVECTOR ZeroTest_Intrinsic()

    return _mm_setzero_ps();

这是另一个以看似天真的方式执行初始化的候选者:

DirectX::XMVECTOR ZeroTest_Naive()

    return  0.0f, 0.0f, 0.0f, 0.0f ;

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

ZeroTest_Intrinsic
    xorps  xmm0, xmm0
    ret
ZeroTest_Naive
    xorps  xmm0, xmm0
    ret

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

这很明显,即使对于无法阅读汇编代码的人来说也是如此。他们都是一样的!我会说这非常明确地回答了哪个更快的问题:它们将是相同的,因为优化器识别出看似幼稚的初始化程序并将其转换为单个优化的汇编语言指令以清除寄存器。

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

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

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

ZeroTest_Intrinsic
    xorps  xmm0, xmm0
    ret
ZeroTest_Naive
    push   ebp
    mov    ebp, esp
    and    esp, -16
    sub    esp, 16

    mov    DWORD PTR [esp],    0
    mov    DWORD PTR [esp+4],  0
    mov    DWORD PTR [esp+8],  0
    mov    DWORD PTR [esp+12], 0
    movaps xmm0, XMMWORD PTR [esp]

    mov    esp, ebp
    pop    ebp
    ret

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

【讨论】:

当人们通过在循环中添加结果来对单个循环体进行微基准测试时,他们通常最终会测试吞吐量,而不是延迟。根据您的实际程序如何使用您正在调整的操作,这可能是错误的优化对象。现代无序执行 CPU 使微基准测试变得困难。一旦你在没有分支错误预测的情况下受 CPU 限制,就需要考虑三个简单的维度:前端瓶颈(主要只是融合域 uop 计数)、执行单元瓶颈(OOO 吞吐量)和延迟瓶颈。 ***.com/a/40879258/224132 如果您使用 MSVC 显式启用 SSE 或 SSE2,那么您必须在 32 位模式下编译。以我的经验,微软在 32 位模式的优化上投入的精力要少得多。我已经回答了关于 SO 的多个问题,其中 MSVC 在 32 位模式下做了一些愚蠢的事情,但在 64 位模式下做出了最佳选择。不幸的是,即使是 64 位模式,MSVC 也默认为 32 位模式,因此您必须显式启用 64 位模式。如果 Microsoft 在 64 位 Windows 上默认为 64 位模式,那么关于 SO 上带有 MSVC 的 SIMD 的问题就会少得多。 是的,我在这里谈论的是 32 位代码。这就是原始问题中讨论的内容。这对于 64 位来说不是问题,因为始终支持 SSE2。 32 位优化器是更老的代码,并且经常做一些今天看起来“愚蠢”但很久以前就有意义的事情。如果不是基于全新的代码,64 位优化器似乎已在很大程度上被重写。我不同意,尽管默认为 64 位目标是有道理的。仍然有 很多 的 32 位处理器。并且将其基于您的开发机器,这违反了最小意外原则。 仅仅因为我在 AMD 处理器上进行开发并不意味着我希望我的所有构建都针对 AMD 等进行调整。而且对于它的价值,我还没有看到从32 位编译器与 64 位编译器相比,我做了很多并排比较。 MSVC 2015 的最后一次更新做了很多工作,进一步优化了 32 位编译器,使其与 VS 2010 附带的 64 位编译器中包含的许多优化增强保持同步。

以上是关于分析 _mm_setzero_ps 和 0.0f,0.0f,0.0f,0.0f的主要内容,如果未能解决你的问题,请参考以下文章

OpenGL模版小案例分析

对类使用 C 内在函数和内存对齐困难

OpenGL Two (Triangle__EBO)

如何在 3D 中围绕同一个枢轴/点旋转多个对象?

自然语言0.0_情感分析权威网站_政治经济地理

mybatis源码分析: