我正在浏览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在堆栈上。将它们更改为堆上(因此写入必须发生),并且机器代码现在相同。通常情况如此吗?使用存储类确实没有负面影响吗?除了完全优化的版本,我想。
答案 0 :(得分:0)
首先,不要将Visual Studio的“高分辨率时钟”用于执行定时。您应该使用QueryPerformanceCounter。请参阅Connect。
在这些微测试中很难测量SIMD性能,因为加载矢量数据的开销通常可以通过这种简单的ALU使用来支配。您确实需要对数据做一些实质性的事情才能看到好处。还要记住,根据您的编译器设置,编译器本身可能正在使用“标量”SIMD功能,甚至可以自动引导这些简单的代码循环。
您还会看到生成测试数据的方式存在一些问题。您应该在堆上创建大于单个向量的内容,并将其用作源/目标。
PS:创建“静态”XMVECTOR数据的最佳方法是使用XMVECTORF32类型。
static const DirectX::XMVECTORF32 v1 = { 1, 2, 3, 4 };
请注意,如果您希望
XMVECTOR
和XMFLOATx
之间的加载/保存转化为“自动”,请查看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
获得比第二部分代码更快的代码