我目前正在c++(带有c++11)中开发一个开源3D应用程序框架。我自己的数学库的设计类似于XNA math library,也考虑了SIMD。但是目前还不是很快,它在内存对齐方面存在问题,但是在另一个问题中有更多问题。
几天前,我问自己为什么要编写自己的SSE代码。启用优化后,编译器还可以生成高度优化的代码。我也可以使用vector extension的“GCC”。但这并不是真正可移植的。
我知道当我使用自己的SSE代码时,我拥有更多的控制权,但通常这种控制权是不必要的。
SSE的一个大问题是动态内存的使用,在内存池和面向数据的设计的帮助下,动态内存的使用受到了最大的限制。
现在我的问题是:
__m128 v1 = _mm_set_ps(0.5f, 2, 4, 0.25f);
__m128 v2 = _mm_set_ps(2, 0.5f, 0.25f, 4);
__m128 res = _mm_mul_ps(v1, v2);
float v1 = {0.5f, 2, 4, 0.25f};
float v2 = {2, 0.5f, 0.25f, 4};
float res[4];
res[0] = v1[0]*v2[0];
res[1] = v1[1]*v2[1];
res[2] = v1[2]*v2[2];
res[3] = v1[3]*v2[3];
load
和store
指令。Pear3D::Vector4f* v1 = new Pear3D::Vector4f(0.5f, 2, 4, 0.25f);
Pear3D::Vector4f* v2 = new Pear3D::Vector4f(2, 0.5f, 0.25f, 4);
Pear3D::Vector4f res = Pear3D::Vector::multiplyElements(*v1, *v2);
上面的示例使用一个内部使用
float[4]
的虚构类,并在每种方法(例如store
)中使用load
和multiplyElements(...)
。该方法使用内部SSE。 我不想使用其他库,因为我想了解有关SIMD和大规模软件设计的更多信息。但是欢迎使用示例库。
PS:这不是真正的问题,更多是设计问题。
最佳答案
好吧,如果您想使用SIMD扩展,一个很好的方法是使用SSE内部函数(当然,请不要使用内联汇编程序,但幸运的是,无论如何,您都没有将其列出来)。但是为了清洁起见,您应该将它们封装在带有重载运算符的漂亮 vector 类中:
struct aligned_storage
{
//overload new and delete for 16-byte alignment
};
class vec4 : public aligned_storage
{
public:
vec4(float x, float y, float z, float w)
{
data_[0] = x; ... data_[3] = w; //don't use _mm_set_ps, it will do the same, followed by a _mm_load_ps, which is unneccessary
}
vec4(float *data)
{
data_[0] = data[0]; ... data_[3] = data[3]; //don't use _mm_loadu_ps, unaligned just doesn't pay
}
vec4(const vec4 &rhs)
: xmm_(rhs.xmm_)
{
}
...
vec4& operator*=(const vec4 v)
{
xmm_ = _mm_mul_ps(xmm_, v.xmm_);
return *this;
}
...
private:
union
{
__m128 xmm_;
float data_[4];
};
};
现在的好处是,由于使用了匿名联合(UB,我知道,但请向我展示一个无法使用SSE的平台),您可以在需要时使用标准的float数组(例如
operator[]
或初始化(不要使用_mm_set_ps
)),仅在适当时使用SSE。使用现代的内联编译器,封装可能是免费的(我很惊讶VC10如何使用此 vector 类针对一堆计算优化SSE指令,而不必担心不必要地转移到临时内存变量中,就像VC8甚至喜欢无需封装)。唯一的缺点是,您需要注意正确的对齐方式,因为未对齐的 vector 不会给您带来任何好处,甚至可能比非SSE的速度慢。但是幸运的是,
__m128
的对齐要求将传播到vec4
(以及任何周围的类)中,您只需要注意动态分配,这是C++很好的手段。您只需要创建一个基类,其operator new
和operator delete
函数(当然在所有情况下)都正确地重载了, vector 类将从中派生。要在标准容器中使用您的类型,您当然还需要专门设置std::allocator
(为了完整起见,可能还需要std::get_temporary_buffer
和std::return_temporary_buffer
),否则它将使用全局operator new
。但是真正的缺点是,您还需要注意以SSE vector 为成员的任何类的动态分配,这可能很繁琐,但也可以通过从
aligned_storage
派生这些类并将其整体放入std::allocator
特化困惑成一个方便的宏。JamesWynn指出,这些操作通常在一些特殊的繁重计算块(例如纹理过滤或顶点转换)中组合在一起,但是另一方面,使用这些SSE vector 封装不会在标准
float[4]
上引入任何开销- vector 的实现类。为了进行任何计算,您需要将这些值从内存中获取到寄存器中(无论是x87堆栈还是标量SSE寄存器),所以为什么不一次全部获取em(IMHO应该不比移动单个对象慢一些)值(如果正确对齐)并并行计算。因此,您可以自由地为非SSE切换出SSE补充,而不会引起任何开销(如果我的推理错误,请纠正我)。但是,如果确保所有以
vec4
为成员的类的对齐方式对您来说过于繁琐(恕我直言,此方法的唯一缺点),则您还可以定义用于计算的专用SSE vector 类型,并使用标准的non -SSE vector 用于存储。编辑:好吧,来看一下这里的开销参数(起初看起来很合理),让我们进行一堆计算,由于运算符重载,它们看起来很干净:
#include "vec.h"
#include <iostream>
int main(int argc, char *argv[])
{
math::vec<float,4> u, v, w = u + v;
u = v + dot(v, w) * w;
v = abs(u-w);
u = 3.0f * w + v;
w = -w * (u+v);
v = min(u, w) + length(u) * w;
std::cout << v << std::endl;
return 0;
}
并查看VC10对它的看法:
...
; 6 : math::vec<float,4> u, v, w = u + v;
movaps xmm4, XMMWORD PTR _v$[esp+32]
; 7 : u = v + dot(v, w) * w;
; 8 : v = abs(u-w);
movaps xmm3, XMMWORD PTR __xmm@0
movaps xmm1, xmm4
addps xmm1, XMMWORD PTR _u$[esp+32]
movaps xmm0, xmm4
mulps xmm0, xmm1
haddps xmm0, xmm0
haddps xmm0, xmm0
shufps xmm0, xmm0, 0
mulps xmm0, xmm1
addps xmm0, xmm4
subps xmm0, xmm1
movaps xmm2, xmm3
; 9 : u = 3.0f * w + v;
; 10 : w = -w * (u+v);
xorps xmm3, xmm1
andnps xmm2, xmm0
movaps xmm0, XMMWORD PTR __xmm@1
mulps xmm0, xmm1
addps xmm0, xmm2
; 11 : v = min(u, w) + length(u) * w;
movaps xmm1, xmm0
mulps xmm1, xmm0
haddps xmm1, xmm1
haddps xmm1, xmm1
sqrtss xmm1, xmm1
addps xmm2, xmm0
mulps xmm3, xmm2
shufps xmm1, xmm1, 0
; 12 : std::cout << v << std::endl;
mov edi, DWORD PTR __imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A
mulps xmm1, xmm3
minps xmm0, xmm3
addps xmm1, xmm0
movaps XMMWORD PTR _v$[esp+32], xmm1
...
即使没有彻底分析每条指令及其用法,我也很自信地说没有任何不必要的加载或存储,除了开头的加载或存储(好吧,我未初始化它们),无论如何它们都是必需的。它们从内存到计算寄存器,最后,这是必需的,因为下面的表达式
v
将被输出。它甚至没有将任何内容存储回u
和w
中,因为它们只是临时变量,我不再使用。一切都完美地内联和优化。尽管dot
函数在float
之后使用实际的_mm_store_ss
返回了haddps
,但它甚至设法无缝地对点积的结果进行后续乘法,而无需离开XMM寄存器。因此,即使我通常对编译器的功能有些怀疑,也不得不说,将自己的内在函数手工制作为特殊函数并不比通过封装获得的清晰表达代码真正付出代价。尽管您可以创建一些杀手级的示例,在这些示例中编写内在函数确实可以省去您一些指令,但是随后您又必须首先超越优化程序。
编辑:好吧,Ben Voigt指出了除了(最可能不是问题的)内存布局不兼容之外,并集的另一个问题,这是它违反了严格的别名规则,并且编译器可能会以某种方式优化访问不同并集成员的指令使代码无效。我还没想过我不知道它在实践中是否会出现任何问题,当然需要进行调查。
如果确实存在问题,那么很遗憾,我们需要删除
data_[4]
成员并单独使用__m128
。为了进行初始化,我们现在必须再次诉诸_mm_set_ps
和_mm_loadu_ps
。 operator[]
变得更加复杂,可能需要_mm_shuffle_ps
和_mm_store_ss
的某种组合。但是对于非const版本,您必须使用某种代理对象来将分配委派给相应的SSE指令。然后必须调查编译器可以在特定情况下以哪种方式优化此额外开销。或者,您仅使用SSE-vector进行计算,而只是为整个非SSE vector 之间的转换提供一个接口(interface),然后将其用于计算的外围设备(因为您通常不需要访问内部的单个组件冗长的计算)。这似乎是glm处理此问题的方式。但是我不确定本征如何处理。
但是,无论您如何解决,仍然无需使用运算符(operator)重载的优势就无需手工制作SSE原理。
关于c++ - 我应该使用SIMD或 vector 扩展名还是其他?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/10718744/