Dra*_*rgy 7 c++ sse simd intrinsics avx
我希望这不会成为一个非常愚蠢的问题我稍后会感到尴尬,但我总是对SIMD内在函数感到困惑,以至于我发现汇编代码比内在函数更容易合理化.
所以我的主要问题是使用SIMD内部数据类型__m256
.只是为了跳到这一点,我的问题是做这样的事情:
class PersistentObject
{
...
private:
std::vector<__m256, AlignedAlloc<__m256, 32>> data;
};
Run Code Online (Sandbox Code Playgroud)
在生成最有效的代码时,它是否可以接受,是否会使编译器绊倒?这就是我现在困惑的部分.我处于缺乏经验的水平,当我有一个热点并且已经筋疲力尽所有其他直接选择时,我给SIMD内在函数一个镜头,并且如果它们不能提高性能,我总是希望退出我的更改(并且我已经退出这么多与SIMD相关的变化).
但是这个关于存储SIMD内部类型的问题和困惑一直让我意识到我并不真正理解这些内在函数如何在基本编译器级别上工作.我的想法想要__m256
像一个抽象的YMM
寄存器(尚未分配).当我看到加载和存储指令时,它开始与我点击.我认为它们是编译器执行其寄存器分配的提示.
我以前是没有把更多的想进入这个不是,因为我一直使用SIMD类型的临时办法:_mm256_load_ps
对__m256
,做一些操作,结果存储回32位SPFP 256位对齐排列float[8]
.我想到__m256
像YMM寄存器一样.
摘要YMM注册?
但是最近我正在实现一个数据结构,试图围绕SIMD处理(一个简单的代表SoA方式的一组向量),这里如果我可以主要工作__m256
而不经常从浮点数组加载而变得方便之后将结果存回.在一些快速测试中,MSVC至少似乎发出了将我的内在函数映射到汇编的适当指令(以及当我从向量中访问数据时正确对齐的加载和存储).但这打破了我__m256
作为抽象YMM
寄存器思考的概念模型,因为存储这些东西会持续地暗示更像常规变量的东西,但在那时,负载/运动和存储是什么?
所以我对我在脑海中构建的关于如何思考所有这些内容的概念模型略微喋喋不休,我的希望是,也许经验丰富的人可以立即认识到我正在考虑这些东西的方式被破坏了什么我调查我脑子的尤里卡回答.我希望这个问题不是太愚蠢(我有一种不安的感觉,但是我试图在其他地方发现答案,但仍然发现自己很困惑).所以最终,是否可以永久地直接存储这些数据类型(这意味着我们在不使用它已经从YMM寄存器溢出之后的某个时刻重新加载内存_mm_load*
),如果是这样,我的概念模型有什么问题?
如果这是一个如此愚蠢的问题,请道歉!这个东西我耳朵后面真的很湿.
更多细节
非常感谢您迄今为止提供的有用评论!我想我应该分享更多细节,以使我的问题不那么模糊.基本上我正在尝试创建一个数据结构,它只是以SoA形式存储的向量集合:
xxxxxxxx....
yyyyyyyy....
zzzzzzzz....
Run Code Online (Sandbox Code Playgroud)
...主要是为了用于热点,其中关键循环具有顺序访问模式.但与此同时,非关键执行路径可能想要以AoS形式(x/y/z)随机访问第5个3向量,此时我们不可避免地进行标量访问(如果不是这样的话,那就非常好了)如此高效,因为它们不是关键路径).
在这个特例中,我发现从实现的角度来看,只是持久存储和使用__m256
而不是更方便float*
.这将阻止我洒了很多的垂直糊涂的代码_mm_loads*
,并_mm_stores*
在此情况下(无论是在关键的执行和大部分代码的换算)SIMD内部函数实现,因为通常情况下.但是我不确定这是否只是保留__m256
短暂的临时数据,一些函数本地,将一些浮点数加载到__m256,执行某些操作,并按照我通常所做的那样存储结果过去.它会更加方便,但我有点担心这种方便的实现方式可能会阻塞一些优化器(尽管我还没有发现它的情况).如果他们没有绊倒优化器,那么我一直在思考这些数据类型的方式有点过时了.
所以在这种情况下,就好像做完这些东西并且我们的优化器一直处理得非常好,然后我很困惑,因为我正在思考这些东西的方式,并认为我们需要那些明确的_mm_load
和_mm_store
短暂的上下文(函数的本地,即)帮助我们的优化器是错的!而这种情况让我感到不安,因为我觉得这应该没问题!:-D
答案
来自Mysticial的一些评论确实对我来说很有帮助,并且帮助我修复了我的大脑,并且给了我一些保证,我想做的事情是正确的.它是以评论的形式而不是答案的形式给出的,所以我会在这里引用它,以防任何人碰巧遇到类似的混淆.
如果它有帮助,我有大约200k LOC写得完全像这样.IOW,我将SIMD类型视为一等公民.没关系.编译器处理它们与任何其他基本类型没有区别.所以没有问题.
优化者并不那么脆弱.他们确实在C/C++标准的合理解释中保持正确性.除非您需要特殊的(未对齐,非时间,蒙版等),否则不需要加载/存储内在函数.
也就是说,请随时写下自己的答案.更多信息更好!我真的希望能够更有信心地改进对如何编写SIMD代码的基本理解,因为我正处于对所有事情犹豫不决的阶段,并且仍然在猜测自己.
反思
再次感谢大家!我现在感觉非常清楚,并且对设计围绕SIMD构建的代码更有信心.出于某种原因,我对SIMD内在函数的优化器非常怀疑,认为我必须以尽可能最低级别的方式编写代码,并在有限的函数范围内尽可能将这些加载和存储作为本地.我认为我的一些迷信来自于几乎几十年前编写的SIMD内在函数最初针对较旧的编译器,也许当时优化器可能需要更多的帮助,或者我可能一直都是非理性的迷信.我看着它有点像80年代人们如何看待C编译器,在register
这里和那里提出类似的提示.
有了SIMD,我总是有非常喜欢的结果,并且有一种趋势,尽管每次在蓝色的月亮里一次又一次地使用它,总是觉得自己像个初学者,也许只是因为混合的成功让我不愿意使用它严重拖延了我的学习过程.最近我想纠正这个问题,我非常感谢所有的帮助!
是的,__m256
作为常规类型; 它不必是仅限注册的.您可以__m256
通过引用非内联函数以及其他任何内容来创建数组.
主要的警告是它是一种"过度对齐"类型:编译器假定__m256
内存中的32字节对齐,但std::max_align_t
通常在主流C++实现上只有8或16字节对齐.因此,您需要自定义分配器std::vector
或其他动态分配,因为std::vector<__m256>
将分配内存不足以存储__m256
.谢谢,C++(虽然C++ 17显然最终会解决这个问题).
但这打破了我
__m256
作为抽象YMM寄存器思考的概念模型,因为存储这些东西会持续地暗示更像常规变量的东西,但在那时,负载/运动和存储是什么?
的__m128 _mm_loadu_ps(float*)
/ _mm_load_ps
内在主要存在于对准信息传送给编译器,和(对于FP内在)键入播送.使用整数你甚至不会这样做,你必须指向__m128i*
.
(尽管AVX512内在函数最终使用void*
而不是__m512i*
.)
_mm256_load_ps(fp)
基本上相当于*(__m256*)fp
:8个浮点数的对齐加载. __m256*
允许别名等类型,但(据我所知)相反是不正确的:它不能保证是安全的拿到的第三个元素__m256 my_vec
中包含的代码((float*)my_vec)[3]
.这将是严格违规的违规行为.虽然它在大多数编译器中至少大部分时间都在实践中起作用.
(请参阅索引获取__m128的成员?,并以可移植的方式打印__m128i变量:存储到tmp数组通常会优化掉.但如果你想要一个水平的总和或其他东西,通常最好使用矢量shuffle并添加内在函数,而不是希望编译器将自动矢量化存储+标量添加循环.)
也许在过去的某些时候,当内在函数是新的时候,movaps
每次你的C源包含时你确实得到了负载_mm_load_ps
,但是在这一点上它与*
运算符上的特别不同float*
; 编译器可以并且将优化相同数据的冗余负载,或者优化向量存储/标量重新加载到随机数中.
但与此同时,非关键执行路径可能希望随机访问AoS形式的第5个3向量(x/y/z),此时我们不可避免地进行标量访问.
这里最大的警告是,从__m256
对象中获取标量的代码将变得丑陋,并且可能无法有效编译.您可以使用包装函数隐藏丑陋,但效率问题可能不会轻易消失,具体取决于您的编译器.
如果您编写的是不使用gcc-style my_vec[3]
或MSVC的可移植代码,那么my_vec.m256_f32[3]
将数据存储__m256
到alignas(32) float tmp [8]
可能无法优化的数组中,您可能会加载到YMM寄存器和存储中.(然后是vzeroupper
).