加载/存储到XMFLOAT4比使用XMVECTOR更快?

时间:2014-05-02 01:11:40

标签: directx direct3d directx-11

我正在浏览DirectX Math / XNA Math库,当我读到XMVECTOR(现在DirectX::XMVECTOR)的对齐要求以及它的预期时,我很好奇您可以使用XMFLOAT*代替成员,在执行数学运算时使用XMLoad*XMStore*。我对这些权衡非常好奇,所以我做了一个实验,因为我确信其他人已经有了,并且经过测试,确切地知道你为每次操作加载和存储向量的确失去了多少。这是结果代码:

#include <Windows.h>

#include <chrono>
#include <cstdint>
#include <DirectXMath.h>
#include <iostream>

using std::chrono::high_resolution_clock;

#define TEST_COUNT          1000000000l

int main(void)
{
    DirectX::XMVECTOR v1 = DirectX::XMVectorSet(1, 2, 3, 4);
    DirectX::XMVECTOR v2 = DirectX::XMVectorSet(2, 3, 4, 5);
    DirectX::XMFLOAT4 x{ 1, 2, 3, 4 };
    DirectX::XMFLOAT4 y{ 2, 3, 4, 5 };

    std::chrono::system_clock::time_point start, end;
    std::chrono::milliseconds duration;

    // Test with just the XMVECTOR
    start = high_resolution_clock::now();
    for (uint64_t i = 0; i < TEST_COUNT; i++)
    {
        v1 = DirectX::XMVectorAdd(v1, v2);
    }
    end = high_resolution_clock::now();
    duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);

    DirectX::XMFLOAT4 z;
    DirectX::XMStoreFloat4(&z, v1);
    std::cout << std::endl << "z = " << z.x << ", " << z.y << ", " << z.z << std::endl;
    std::cout << duration.count() << " milliseconds" << std::endl;

    // Now try with load/store
    start = high_resolution_clock::now();
    for (uint64_t i = 0; i < TEST_COUNT; i++)
    {
        v1 = DirectX::XMLoadFloat4(&x);
        v2 = DirectX::XMLoadFloat4(&y);

        v1 = DirectX::XMVectorAdd(v1, v2);
        DirectX::XMStoreFloat4(&x, v1);
    }
    end = high_resolution_clock::now();
    duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);

    std::cout << std::endl << "x = " << x.x << ", " << x.y << ", " << x.z << std::endl;
    std::cout << duration.count() << " milliseconds" << std::endl;
}

运行调试版本会产生输出:

z = 3.35544e+007, 6.71089e+007, 6.71089e+007
25817 milliseconds

x = 3.35544e+007, 6.71089e+007, 6.71089e+007
84344 milliseconds

好的,那么三倍如此慢,但有没有人真的认真对调试版本进行性能测试?以下是我发布版本时的结果:

z = 3.35544e+007, 6.71089e+007, 6.71089e+007
1980 milliseconds

x = 3.35544e+007, 6.71089e+007, 6.71089e+007
670 milliseconds

像魔术一样,XMFLOAT4的速度几乎快三倍!不知何故,桌子已经转向。看看代码,这对我来说毫无意义;第二部分运行第一部分运行的命令的超集!肯定会出现问题,或者我没有考虑到的事情。很难相信编译器能够将第二部分优化为更简单,理论上更高效的第一部分的九倍。我唯一合理的解释是:(1)缓存行为,(2)XMVECTOR无法利用的一些疯狂乱序执行,(3)编译器正在进行一些疯狂的优化,或者(4)直接使用XMVECTOR有一些隐含的低效率,在使用XMFLOAT4时能够被优化掉。也就是说,编译器从内存加载和存储XMVECTOR的默认方式效率低于XMLoad*XMStore*。我试图检查反汇编,但我并不熟悉X86和/或SSE2,Visual Studio做了一些疯狂的优化,因此很难跟上源代码。我也尝试过Visual Studio性能分析工具,但这并没有帮助,因为我无法弄清楚如何让它显示反汇编而不是代码。我得出的唯一有用信息是,XMVectorAdd的第一次调用占所有周期的约48.6%,而第二次调用XMVectorAdd占所有周期的约4.4%。

修改 经过一些调试之后,这里是在循环内部运行的代码的程序集。第一部分:

002912E0  movups      xmm1,xmmword ptr [esp+18h]     <-- HERE
002912E5  add         ecx,1  
002912E8  movaps      xmm0,xmm2                      <-- HERE
002912EB  adc         esi,0  
002912EE  addps       xmm0,xmm1  
002912F1  movups      xmmword ptr [esp+18h],xmm0     <-- HERE
002912F6  jne         main+60h (0291300h)  
002912F8  cmp         ecx,3B9ACA00h  
002912FE  jb          main+40h (02912E0h)  

第二部分:

00291400  add         ecx,1  
00291403  addps       xmm0,xmm1  
00291406  adc         esi,0  
00291409  jne         main+173h (0291413h)  
0029140B  cmp         ecx,3B9ACA00h  
00291411  jb          main+160h (0291400h)

请注意,这两个循环确实几乎相同。唯一的区别是第一个for循环似乎是进行加载和存储的循环!看起来似乎Visual Studio进行了大量的优化,因为x和y在堆栈上。将它们更改为堆上(因此写入必须发生),并且机器代码现在相同。通常情况如此吗?使用存储类确实没有负面影响吗?除了完全优化的版本,我想。

2 个答案:

答案 0 :(得分:0)

首先,不要将Visual Studio的“高分辨率时钟”用于执行定时。您应该使用QueryPerformanceCounter。请参阅Connect

在这些微测试中很难测量SIMD性能,因为加载矢量数据的开销通常可以通过这种简单的ALU使用来支配。您确实需要对数据做一些实质性的事情才能看到好处。还要记住,根据您的编译器设置,编译器本身可能正在使用“标量”SIMD功能,甚至可以自动引导这些简单的代码循环。

您还会看到生成测试数据的方式存在一些问题。您应该在堆上创建大于单个向量的内容,并将其用作源/目标。

PS:创建“静态”XMVECTOR数据的最佳方法是使用XMVECTORF32类型。

static const DirectX::XMVECTORF32 v1 = { 1, 2, 3, 4 };
  

请注意,如果您希望XMVECTORXMFLOATx之间的加载/保存转化为“自动”,请查看SimpleMath中的DirectX Tool Kit。您只需在数据结构中使用SimpleMath::Vector4等类型,隐式转换运算符负责为您调用XMLoadFloat4 / XMStoreFloat4

答案 1 :(得分:0)

如果你定义

DirectX::XMVECTOR v3 = DirectX::XMVectorSet(2, 3, 4, 5);

并使用v3而不是v1作为结果: ...

 for (uint64_t i = 0; i < TEST_COUNT; i++)
    {
        v3 = DirectX::XMVectorAdd(v1, v2);
    }

使用XMLoadFloat4和XMStoreFloat4

获得比第二部分代码更快的代码